API authentication
Integrations authenticate with an organization-scoped API token sent as a Bearer credential. The Probara web app continues to use session cookies plus X-Organization-Id for org switching.
API tokens (available)
Section titled “API tokens (available)”- Sign in to Probara and open Workspace → API tokens (
/workspace/api-tokens). - In the New token section, enter a name, pick the organization (shared dropdown, limited to your memberships), and click Create token.
- Copy the secret from the amber 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://probara.net/api/v1/projects"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.
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. 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).
Authorize + consent
Section titled “Authorize + consent”- The client redirects the user to
GET /oauth/authorizewith a PKCEcode_challenge(S256), the exact registeredredirect_uri, and an optionalscope— see Scopes below for what happens when it is omitted or unrecognized. - 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.
- 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=...Once client_id and redirect_uri are validated, any further authorize error (unsupported response_type, invalid PKCE) 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. 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.) |
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.
Refresh
Section titled “Refresh”POST /oauth/tokenContent-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=...&client_id=...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 | CI, scripts, API clients | 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, and all of /api/v1/me/api-tokens.
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 owner or admin on that organization.
Errors
Section titled “Errors”| Status | Code | Meaning |
|---|---|---|
| 401 | unauthorized | Missing/invalid cookie or Bearer token |
| 403 | email_not_verified | Residual: returned for legacy users whose email_verified_at is NULL (backups, manual DB edits). New signups are auto-verified and never trigger this. |
| 429 | too_many_requests | Token creation rate limit (10 per user per hour) |
| 500 | email_send_failed | Returned by request-password-reset (and the COLLISION branch of signup) when the upstream email provider rejects the send. The user can retry. |
Rate limits
Section titled “Rate limits”Creating API tokens is limited to 10 requests per user per hour (configurable in the Worker). 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.