Skip to content

API authentication

Integrations authenticate with an organization-scoped token sent as a Bearer credential: a personal API token for scripts and API clients, or an organization app token for reporting from CI. The Probara web app continues to use session cookies plus X-Organization-Id for org switching.

An API token belongs to the user who created it, in one organization. It acts with that user’s current permissions, and it is revoked when the user leaves the organization. Use it for scripts and API clients; to report test results from CI, prefer an app token, which belongs to the organization.

Creating and revoking a token requires the Manage API tokens permission (api-tokens.manage), held by owners, admins and members. A viewer can read the token table and nothing more: creating or revoking returns 403 forbidden, and the screen offers no create or revoke control. While the organization requires two-factor authentication, creating a token also needs your account to have a confirmed authenticator app; otherwise the request returns 403 organization_two_factor_required. Tokens created earlier keep working.

  1. Sign in to Probara and open API tokens from the avatar menu (/user/api-tokens).
  2. Click New token at the top of the screen to open the create dialog, enter a name, and click Create token — the token is created for the organization currently in focus.
  3. Copy the secret from the one-time panel immediately — it is shown once and cannot be retrieved later.
  4. Review Active tokens in the table (Created, Last used or Never) and revoke with the trash icon when rotating credentials.

Token format: probara_ followed by base64url-encoded random bytes. Store tokens in your CI secret store, not in the repository.

Send the raw token on every protected /api/v1/* call (except session-only auth and token management routes):

Authorization: Bearer probara_xxxxxxxx

With a valid API token you do not need X-Organization-Id. The Worker resolves organization and membership from the token row.

Example:

Terminal window
curl -sS -H "Authorization: Bearer $PROBARA_API_TOKEN" \
"https://app.probara.net/api/v1/projects"

An app token belongs to the organization, not to a person. It is bound to one app of the Integrations catalog — JUnit XML, for the Probara CLI, Playwright, for the Probara Playwright reporter, Jest, for the Probara Jest reporter, Cypress, for the Probara Cypress reporter, Vitest, for the Probara Vitest reporter, or Cucumber, for the Probara Cucumber reporter and the CLI’s Gherkin sync — and it is the credential to use in CI:

API tokenApp token
Belongs toA user, in one organizationThe organization, bound to one app
Managed fromAvatar menu → API tokensThe app’s card in Workspace → Integrations
Create and revokeapi-tokens.manage (held by owners, admins and members)integrations.manage (held by owners and admins)
Secretprobara_…probara_app_…
Can callEverything its user can, except starting a plan trialOnly the six calls a reporter makes (below)
When its creator leavesRevokedKeeps working
Shown in activity asYou, marked “From an API token”The app and the token name, such as JUnit XML · nightly-ci
PlanAnyPaid plans only (below)

An app token uses no seat. Because it survives its creator, rotating it is up to you: revoke it and create a new one when it may have leaked, or when someone who could read the secret leaves your team.

  1. As an owner or admin, open Workspace → Integrations and select the card of the app that will use it: JUnit XML, Playwright, Jest, Cypress, Vitest or Cucumber.
  2. Open the Access tokens tab and select Create an app token.
  3. Enter a Token name — name it after what uses it, such as the repository or the pipeline — and select Create token.
  4. Copy the secret from Copy your new token. It starts with probara_app_ and is shown once: store it as the CI secret PROBARA_API_TOKEN right away.

The Access tokens tab lists the active tokens with who created each one, when, and when it was last used. Revoke (after a confirmation) stops it at once: the next request with it answers 401 unauthorized. Revoked tokens keep their name and app in the activity they left behind.

The card uses these routes. Like the rest of /api/v1/orgs/{orgUlid}/integrations, they accept a session cookie only; a Bearer credential is refused.

MethodPathBehavior
GET/api/v1/orgs/{orgUlid}/integrations/apps/{app}/tokens{ items: [{ ulid, app, name, createdAt, lastUsedAt, createdBy }] } — active tokens, never the secret. createdBy is { ulid, name }, or null once that account was deleted
POST/api/v1/orgs/{orgUlid}/integrations/apps/{app}/tokensBody { name } (1–100 characters). 201 { token, appToken }: token is the secret, returned only here
DELETE/api/v1/orgs/{orgUlid}/integrations/apps/{app}/tokens/{tokenUlid}Revokes the token. 204; 404 when it is already revoked or belongs to another organization or app

{app} is junit, playwright, jest, cypress, vitest or cucumber; any other value answers 404 not_found. Each app lists, creates and revokes only its own tokens. All six need the Manage integrations permission (integrations.manage, held by owners and admins), otherwise 403 forbidden. Creating also needs a paid plan and is limited to 10 per user per hour.

An app token may make exactly the calls the CLI and the framework reporters make:

MethodPathUsed for
POST/api/v1/projects/{projectId}/runsCreate a run — only with automated: true
POST/api/v1/projects/{projectId}/reportsSend a report
POST/api/v1/projects/{projectId}/gherkin-syncSync the scenarios of Gherkin feature files
POST/api/v1/runs/{runUlid}/results/{resultUlid}/attachments:stageStage result attachments
PATCH/api/v1/runs/{runUlid}/results/{resultUlid}/attachmentsCommit them — add only
POST/api/v1/runs/{runUlid}/closeClose the run
GET/api/v1/runs/{runUlid}/case-keysRead a run’s case keys, to run only its cases

Every other method and path — every other read included, and the MCP endpoint /mcp — answers 403 forbidden before anything runs. Within those calls:

  • Creating a run without automated: true answers 403 forbidden: an app token only creates automated runs.
  • A commit may only add attachments. A body that leaves out an attachment the result already has answers 403 forbidden and changes nothing, so send the committed files back as { ulid, position } alongside the new ones.
  • It acts with the run execution, test case write and result execution permissions, and nothing more. It is not exempt from a project’s assignee result lock (403 run_case_assignee_locked), and, like an API token, it is not evaluated against project access lists.
  • Its requests count toward the organization’s API rate limit, each result it records counts toward the monthly API result limit, and Idempotency-Key works as for any other credential.

Reporting from CI is included in the paid plans. On an organization without a paid plan:

  • every request with an app token answers 403 forbidden, and creating one is refused the same way;
  • POST /api/v1/projects/{projectId}/reports, and POST /api/v1/projects/{projectId}/runs with automated: true, answer 403 forbidden for every credential — API tokens and sessions included. Creating a manual run stays available;
  • listing and revoking app tokens still work.

Moving to the Free plan does not revoke app tokens: Probara keeps them and refuses them, and they work again as soon as the organization is back on a paid plan.

What an app token does is attributed to the token, and the web app shows the app’s logo and name beside the token name. In API responses, the api_token form of a run’s, a test plan’s or a test case’s author, of a test case’s updatedBy and of an audit event’s actor, and a result’s executedByApiToken, carry app — "junit", "playwright", "jest", "cypress", "vitest" or "cucumber" for an app token, null for an API token — and organization attachment items carry uploaderApp the same way.

The /mcp endpoint additionally accepts an OAuth 2.1 access token as a Bearer credential — this is the flow interactive MCP clients (Claude.ai, Claude Code, Cursor, ChatGPT) use instead of copy/pasting an API token. See the MCP connection guides for per-client setup steps.

Any client can discover the authorization server and this resource’s requirements without prior configuration:

Metadata documentPath
Authorization server metadata (RFC 8414)GET /.well-known/oauth-authorization-server
Protected-resource metadata (RFC 9728)GET /.well-known/oauth-protected-resource

Both documents advertise PKCE S256 only (no plain) and the exact scopes below. The authorization server metadata also sets authorization_response_iss_parameter_supported and client_id_metadata_document_supported to true.

Clients register themselves before the first authorize request — no manual “create an OAuth app” step:

POST /oauth/register
Content-Type: application/json
{ "redirect_uris": ["https://client.example.com/callback"] }

Returns a client_id and the registered redirect_uris. Registration is rate-limited and capped. A refused registration returns 429 with a Retry-After header, in seconds. Client expiry is two-tier: a client that never completes an authorization expires 24 hours after registration; a client that has authorized at least once expires 60 days after its last token activity (exchange or refresh).

A client can also identify itself with an https URL it controls, instead of registering. Host a JSON document at that URL and use the URL as the client_id:

{
"client_id": "https://app.example.com/oauth/client.json",
"client_name": "Example MCP Client",
"redirect_uris": ["http://127.0.0.1:3000/callback"],
"token_endpoint_auth_method": "none"
}

The server fetches the document when it first sees the URL, and again once its cache lapses. The cache lasts as long as the document’s Cache-Control: max-age (or Expires), between 5 minutes and 24 hours. The requirements:

  • The client_id in the document equals its URL exactly. The URL uses https and has a path. It has no query string, fragment, credentials, port, IP address, or localhost host, and its host name has no trailing dot.
  • client_name and redirect_uris are required. A redirect_uri in the authorize request must appear in the list.
  • token_endpoint_auth_method is none or absent. Client secrets are not accepted.
  • The document is application/json, at most 5 KiB, and must not redirect.

Refused requests return 429 with a Retry-After header, in seconds. A signed-in user can add 10 new client documents per day, every user together can add 20 per registrable domain per day, and one user can fetch unknown URLs at most 10 times per host and 30 times in total per hour. A registrable domain is example.com with its subdomains. Under a shared-hosting domain such as github.io, each site (alice.github.io) is a registrable domain of its own.

When the host cannot be reached while the server fetches or refreshes a document (a timeout, a server error, or a DNS failure), a client that has already been authorized keeps working from its last valid copy for up to 24 hours after its cache lapsed. A client that has never been authorized, or whose copy is older than that, gets 503 with error set to temporarily_unavailable and a Retry-After header. A document that is definitively invalid, for example one that redirects or names another client_id, is refused with 400 at once, and no copy is used in its place.

Expiry follows the two tiers described for registered clients above. An expired document client is refused until the hourly cleanup removes it. Then the same URL is admitted as a new client, and its users must authorize it again. The consent screen shows the host that served the document, so users can see where the application came from. Registration through /oauth/register keeps working for clients that prefer it.

  1. The client redirects the user to GET /oauth/authorize with a PKCE code_challenge (S256), the exact registered redirect_uri, the resource set to the MCP server’s URL (https://app.probara.net/mcp, RFC 8707), and an optional scope — see Scopes below for what happens when it is omitted or unrecognized. An omitted resource is read as that URL. Any other resource, or several, is refused with invalid_target. Spellings of the same URL (uppercase scheme or host, trailing slash) are accepted.
  2. Probara requires an active session; an unauthenticated visitor is sent to log in first, then returned to consent — no code is ever minted without a session.
  3. The user picks which organization to grant access to and approves or denies on the consent screen. Approval mints an authorization code bound to that organization, the negotiated (and possibly role-downgraded) scope, and the PKCE challenge. While the organization requires two-factor authentication, approval also needs the user’s account to have a confirmed authenticator app; otherwise consent returns 403 organization_two_factor_required and no code is minted.
  4. The client exchanges the code for tokens:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=...&redirect_uri=...&code_verifier=...&client_id=...&resource=https%3A%2F%2Fapp.probara.net%2Fmcp

Send the same resource on this request. If it names another resource, the response is 400 with error: invalid_target, and the code stays unused.

Once client_id and redirect_uri are validated, any further authorize error (unsupported response_type, invalid PKCE, or a resource that is not this server’s MCP URL) is delivered as a 302 redirect to the registered redirect_uri with error, error_description, and the echoed state in the query — per RFC 6749 §4.1.2.1 — not a raw JSON error. Every redirect to the registered redirect_uri, success or error, also carries iss, set to the issuer in the metadata document (RFC 9207). Check it before you use the code. An unknown client_id or an unregistered redirect_uri still returns a non-redirecting JSON 400 (no attacker-controlled redirect target is ever used).

ScopeGrants
mcp:readRead-only tool calls (list_*, get_*, etc.)
mcp:writeRead AND write tool calls (create_*, update_*, delete_*, etc.)

Both metadata documents list these two scopes in scopes_supported, so a client can discover them before it asks for a grant.

scope on /oauth/authorize is optional (RFC 6749 §3.3) and negotiated leniently, since some MCP clients omit it or send values this server doesn’t recognize:

  • Omitted or empty scope → grants the full supported set (mcp:read mcp:write).
  • Unrecognized-only values (e.g. scope=claudeai) → also fall back to the full supported set, identical to the omitted case.
  • A mix of known and unknown values (e.g. scope=mcp:read claudeai) → grants only the known subset (mcp:read).
  • A valid subset of supported scopes → granted unchanged.

The negotiated scope is what shows on the consent screen and what the token response’s scope field reflects. The effective scope at call time is still the minimum of the grant’s negotiated scope and the user’s current role in that organization — a role downgrade after the grant was issued takes effect immediately, without waiting for the user to re-consent.

When a tool call needs a scope the grant does not hold, /mcp refuses the call before the tool runs, with 403 and WWW-Authenticate: Bearer error="insufficient_scope", scope="mcp:write" (or scope="mcp:read"), so an OAuth client can ask for that scope again. When the grant holds the scope but your current role does not allow it, the call returns a forbidden tool error that names the scope and your role, and no insufficient_scope challenge is sent. Authorizing again does not change that: the role caps what a new grant can hold, so an organization owner or admin must change your role. An API token has no scopes and is never refused this way.

POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=...&client_id=...&resource=https%3A%2F%2Fapp.probara.net%2Fmcp

A resource that names another resource is refused with 400 and error: invalid_target, and the refresh token stays usable.

Each refresh rotates the token pair: the old refresh token stops working immediately. Reusing an already-rotated refresh token revokes the entire token family as a precaution — if this happens, the user must consent again.

Re-consenting does not require re-registering the client: as long as the client’s client_id is still inside its consented expiry window (see Dynamic Client Registration (DCR) above), the SAME client_id returns to GET /oauth/authorize to get a fresh grant. That window is guaranteed to outlast the refresh-token lifetime, so a client whose refresh token has just expired — or whose family was just revoked by reuse detection — can always re-authorize without a new DCR call.

Users manage their own authorized clients at Workspace → Authorized clients — revoking there invalidates every access and refresh token issued to that client for that organization immediately.

MechanismUsed byOrg context
Session cookie (tcms_session / __Host-tcms_session)Browser SPAX-Organization-Id header required on domain routes
Bearer API tokenScripts, API clientsFrom token; no org header
Bearer app tokenCI reportingFrom token; no org header

Session-only routes (Bearer is rejected): POST /api/v1/auth/logout, logout-all, GET /api/v1/auth/me, GET/PATCH /api/v1/profile, POST/DELETE /api/v1/profile/avatar, all of /api/v1/me/api-tokens, PATCH /api/v1/orgs/{orgUlid} (the organization rename), and the app token routes under /api/v1/orgs/{orgUlid}/integrations.

MethodPathBodyResponse
GET/api/v1/profile—{ firstName, lastName, positionTitle, avatarKey, avatarVersion }
PATCH/api/v1/profile{ firstName, lastName, positionTitle? }Same shape as GET; empty positionTitle stored as null
POST/api/v1/profile/avatarmultipart/form-data with one file (PNG/JPEG/WebP, max 5 MiB){ avatarKey, avatarVersion }
DELETE/api/v1/profile/avatar—{ avatarKey: null, avatarVersion }

Avatar images are normalized server-side to 256×256 WebP and stored in R2. Public reads use GET /__assets/{avatarKey}?v={avatarVersion} (no session). avatarKey is null when the user has no photo.

firstName and lastName are required and non-empty on PATCH. No X-Organization-Id header is required.

POST /api/v1/auth/signup requires firstName and lastName (trimmed, non-empty) in addition to email and password. Optional invitationToken is unchanged. CREATE and REPAIR persist the names on the users row; positionTitle remains profile-only (set later via PATCH /api/v1/profile).

After you have a token, you can list organization audit events (who changed what) with the same Bearer credential:

GET /api/v1/orgs/{orgUlid}/audit-events — see Audit events API for cursor pagination and response fields. Requires the audit read permission (held by owner/admin).

StatusCodeMeaning
401unauthorizedMissing/invalid cookie or Bearer token
403forbiddenAn app token called something outside its allowed calls, or the organization has no paid plan for CI reporting
403email_not_verifiedThe user has not verified their email address yet. Returned when creating API tokens, approving an MCP client, inviting members, accepting an invitation by email, connecting Google, or turning on two-factor authentication.
403organization_two_factor_requiredThe organization requires two-factor authentication and your account has no confirmed authenticator app. Returned when creating API tokens or approving an MCP client for that organization.
429too_many_requestsToken creation rate limit (10 per user per hour, for API tokens and app tokens alike)

Creating API tokens is limited to 10 requests per user per hour (configurable in the Worker), and creating app tokens has a budget of its own of the same size. POST /api/v1/auth/signup is rate-limited along two dimensions: 5 per hour per IP and 3 per hour per email. Excess requests return 429 too_many_requests.

POST /api/v1/auth/login is limited to 5 per minute per IP and 10 per hour per email. A browser that has already signed in to that address keeps its own budget of 10 per hour for it, so failed attempts from elsewhere cannot lock that browser out. Excess requests return 429 too_many_requests.