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.
API tokens (available)
Section titled “API tokens (available)”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.
- Sign in to Probara and open API tokens from the avatar menu (
/user/api-tokens). - 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.
- Copy the secret from the one-time panel immediately — it is shown once and cannot be retrieved later.
- 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.
Bearer requests
Section titled “Bearer requests”Send the raw token on every protected /api/v1/* call (except session-only auth and token management routes):
Authorization: Bearer probara_xxxxxxxxWith a valid API token you do not need X-Organization-Id. The Worker resolves organization and membership from the token row.
Example:
curl -sS -H "Authorization: Bearer $PROBARA_API_TOKEN" \ "https://app.probara.net/api/v1/projects"App tokens
Section titled “App tokens”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 token | App token | |
|---|---|---|
| Belongs to | A user, in one organization | The organization, bound to one app |
| Managed from | Avatar menu → API tokens | The app’s card in Workspace → Integrations |
| Create and revoke | api-tokens.manage (held by owners, admins and members) | integrations.manage (held by owners and admins) |
| Secret | probara_… | probara_app_… |
| Can call | Everything its user can, except starting a plan trial | Only the six calls a reporter makes (below) |
| When its creator leaves | Revoked | Keeps working |
| Shown in activity as | You, marked “From an API token” | The app and the token name, such as JUnit XML · nightly-ci |
| Plan | Any | Paid 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.
Create and revoke
Section titled “Create and revoke”- 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.
- Open the Access tokens tab and select Create an app token.
- Enter a Token name — name it after what uses it, such as the repository or the pipeline — and select Create token.
- Copy the secret from Copy your new token. It starts with
probara_app_and is shown once: store it as the CI secretPROBARA_API_TOKENright 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.
| Method | Path | Behavior |
|---|---|---|
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}/tokens | Body { 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.
Allowed calls
Section titled “Allowed calls”An app token may make exactly the calls the CLI and the framework reporters make:
| Method | Path | Used for |
|---|---|---|
POST | /api/v1/projects/{projectId}/runs | Create a run — only with automated: true |
POST | /api/v1/projects/{projectId}/reports | Send a report |
POST | /api/v1/projects/{projectId}/gherkin-sync | Sync the scenarios of Gherkin feature files |
POST | /api/v1/runs/{runUlid}/results/{resultUlid}/attachments:stage | Stage result attachments |
PATCH | /api/v1/runs/{runUlid}/results/{resultUlid}/attachments | Commit them — add only |
POST | /api/v1/runs/{runUlid}/close | Close the run |
GET | /api/v1/runs/{runUlid}/case-keys | Read 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: trueanswers403 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 forbiddenand 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-Keyworks 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, andPOST /api/v1/projects/{projectId}/runswithautomated: true, answer403 forbiddenfor 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.
Attribution
Section titled “Attribution”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.
OAuth 2.1 (MCP clients)
Section titled “OAuth 2.1 (MCP clients)”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.
Discovery
Section titled “Discovery”Any client can discover the authorization server and this resource’s requirements without prior configuration:
| Metadata document | Path |
|---|---|
| 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.
Dynamic Client Registration (DCR)
Section titled “Dynamic Client Registration (DCR)”Clients register themselves before the first authorize request — no manual “create an OAuth app” step:
POST /oauth/registerContent-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).
Client ID Metadata Documents (CIMD)
Section titled “Client ID Metadata Documents (CIMD)”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_idin the document equals its URL exactly. The URL useshttpsand has a path. It has no query string, fragment, credentials, port, IP address, orlocalhosthost, and its host name has no trailing dot. client_nameandredirect_urisare required. Aredirect_uriin the authorize request must appear in the list.token_endpoint_auth_methodisnoneor 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.
Authorize + consent
Section titled “Authorize + consent”- The client redirects the user to
GET /oauth/authorizewith a PKCEcode_challenge(S256), the exact registeredredirect_uri, theresourceset to the MCP server’s URL (https://app.probara.net/mcp, RFC 8707), and an optionalscope— see Scopes below for what happens when it is omitted or unrecognized. An omittedresourceis read as that URL. Any other resource, or several, is refused withinvalid_target. Spellings of the same URL (uppercase scheme or host, trailing slash) are accepted. - 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.
- 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_requiredand no code is minted. - The client exchanges the code for tokens:
POST /oauth/tokenContent-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=...&redirect_uri=...&code_verifier=...&client_id=...&resource=https%3A%2F%2Fapp.probara.net%2FmcpSend 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).
Scopes
Section titled “Scopes”| Scope | Grants |
|---|---|
mcp:read | Read-only tool calls (list_*, get_*, etc.) |
mcp:write | Read 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.
Refresh
Section titled “Refresh”POST /oauth/tokenContent-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=...&client_id=...&resource=https%3A%2F%2Fapp.probara.net%2FmcpA 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.
Managing and revoking grants
Section titled “Managing and revoking grants”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.
Session auth (web app only)
Section titled “Session auth (web app only)”| Mechanism | Used by | Org context |
|---|---|---|
Session cookie (tcms_session / __Host-tcms_session) | Browser SPA | X-Organization-Id header required on domain routes |
| Bearer API token | Scripts, API clients | From token; no org header |
| Bearer app token | CI reporting | From 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.
User profile (session cookie)
Section titled “User profile (session cookie)”| Method | Path | Body | Response |
|---|---|---|---|
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/avatar | multipart/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.
Signup contract
Section titled “Signup contract”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).
Audit log (read-only)
Section titled “Audit log (read-only)”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).
Errors
Section titled “Errors”| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing/invalid cookie or Bearer token |
| 403 | forbidden | An app token called something outside its allowed calls, or the organization has no paid plan for CI reporting |
| 403 | email_not_verified | The 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. |
| 403 | organization_two_factor_required | The 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. |
| 429 | too_many_requests | Token creation rate limit (10 per user per hour, for API tokens and app tokens alike) |
Rate limits
Section titled “Rate limits”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.