Authentication & API Keys
Every authenticated request to https://api.primitive.dev/v1 carries a bearer credential in the Authorization header. Primitive issues two kinds of bearer credential, both scoped to a single organization:
| Credential | Prefix | Use it for | Lifetime |
|---|---|---|---|
| API key | prim_ | Server-to-server integrations, CI, headless agents | Long-lived until revoked |
| OAuth access token | prim_oat_ | CLI, desktop, local-agent, and third-party app access | Short-lived (1 hour), refreshable |
curl https://api.primitive.dev/v1/whoami \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN"
Set PRIMITIVE_AUTH_TOKEN to either an API key (prim_...) or an OAuth access token (prim_oat_...). The wire format is identical for both:
Authorization: Bearer prim_... Authorization: Bearer prim_oat_...
Never put API keys, OAuth access tokens, or OAuth refresh tokens in client-side code, browser storage, screenshots, or logs.
Organization scoping
Every credential authorizes exactly one organization. An API key or OAuth token resolves to one org_id, and all reads and writes are isolated to that org's data — you cannot reach another org's resources by changing an id in the URL. To act on a different organization, use a credential issued for that organization. Confirm what a credential resolves to with GET /v1/whoami.
API keys
API keys (prefix prim_) are the simplest credential for backend services. Send the key as a bearer token on every request. Keys do not expire on a timer; they remain valid until revoked. Treat a key as a secret: rotate it if it leaks, and prefer one key per integration so you can revoke narrowly.
Headless agents can obtain a key with no human in the loop — see How to obtain credentials.
OAuth (Authorization Code + PKCE)
Primitive supports OAuth 2.0 Authorization Code with PKCE for public clients (no client secret). This is the right flow for CLIs, desktop apps, local agents, and third-party apps that act on behalf of a human.
Endpoints:
GET /.well-known/oauth-authorization-server GET /.well-known/oauth-protected-resource POST /oauth/register GET /oauth/authorize POST /oauth/token POST /oauth/revoke
Supported parameters:
| Field | Value |
|---|---|
response_types | code |
grant_types | authorization_code, refresh_token |
code_challenge_methods | S256 |
token_endpoint_auth_method | none (public clients) |
scope | primitive:api |
A grant currently authorizes full Primitive API access for the selected organization. Fine-grained OAuth scopes are not exposed yet.
The /oauth/* endpoints follow the OAuth 2.0 spec, not the v1 REST envelope: errors come back as { "error": "...", "error_description": "..." } (for example invalid_grant, invalid_request, unsupported_grant_type), and a rate-limited token request returns the OAuth slow_down error. Redirect URIs are capped at 20 per client. Don't apply the Errors success/error envelope handling to these endpoints.
Dynamic Client Registration
Clients register themselves with no manual setup. POST /oauth/register returns a client_id:
curl -X POST https://api.primitive.dev/oauth/register \ -H "Content-Type: application/json" \ -d '{ "client_name": "my-cli", "redirect_uris": ["http://127.0.0.1:8976/callback"] }'
{ "client_id": "...", "client_name": "my-cli", "redirect_uris": ["http://127.0.0.1:8976/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" }
Redirect URIs must be HTTPS, or http:// on a loopback host (localhost, 127.0.0.1, ::1) for local tools. Fragments are not allowed.
Authorization Code lifecycle
- Generate a PKCE
code_verifierand itsS256code_challenge. - Send the user to
GET /oauth/authorizewithresponse_type=code, yourclient_id,redirect_uri,code_challenge, andcode_challenge_method=S256. The user signs in and picks the organization to authorize. - Primitive redirects back to your
redirect_uriwith a one-timecode. Authorization codes expire after 10 minutes and can be exchanged only once. - Exchange the code at
POST /oauth/token:
curl -X POST https://api.primitive.dev/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=authorization_code \ -d client_id=$CLIENT_ID \ -d code=$CODE \ -d redirect_uri=http://127.0.0.1:8976/callback \ -d code_verifier=$CODE_VERIFIER
{ "access_token": "prim_oat_...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "...", "scope": "primitive:api" }
The access token is valid for 1 hour. Use it as a bearer token exactly like an API key.
Refresh-token rotation
Refresh tokens are valid for 90 days and are single-use. Exchanging one issues a new access token and a new refresh token, and immediately revokes the one you presented:
curl -X POST https://api.primitive.dev/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=refresh_token \ -d client_id=$CLIENT_ID \ -d refresh_token=$REFRESH_TOKEN
Always store the new refresh_token from the response and discard the old one. Presenting a refresh token that has already been rotated or revoked is treated as token reuse: the entire grant is revoked, and every access and refresh token under it stops working. Re-run the authorization flow to recover.
Revocation
Revoke a token (access or refresh) with POST /oauth/revoke. Revoking any token revokes the whole grant — both its access and refresh tokens:
curl -X POST https://api.primitive.dev/oauth/revoke \ -H "Content-Type: application/x-www-form-urlencoded" \ -d token=$TOKEN
Connected apps can also be revoked from Settings -> Connected Apps.
Discovery metadata
Two metadata documents let clients (and agents) configure auth without hard-coding endpoints:
GET /.well-known/oauth-authorization-server GET /.well-known/oauth-protected-resource
The authorization-server document (RFC 8414) advertises the authorization_endpoint, token_endpoint, revocation_endpoint, registration_endpoint, supported response/grant types, code_challenge_methods_supported, and token lifetimes. The protected-resource document (RFC 9728) describes this API as an OAuth-protected resource and points back at the authorization server.
The 401 challenge
A request with a missing or invalid bearer token returns 401 unauthorized with a spec-shaped WWW-Authenticate header pointing at the protected-resource metadata:
WWW-Authenticate: Bearer realm="Primitive API", resource_metadata="https://www.primitive.dev/.well-known/oauth-protected-resource"A cold-starting client should fetch that metadata document, follow it to the authorization server, and obtain a token — rather than guessing endpoints or looping on the 401. See the Errors reference for the full envelope.
Introspection with /v1/whoami
GET /v1/whoami resolves the presented credential and reports what it authorizes. Use it to confirm the org, user, and credential type before acting:
curl https://api.primitive.dev/v1/whoami \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN"
{ "success": true, "data": { "org_id": "...", "user_id": "...", "role": "owner", "request_id": "req_...", "auth_method": "api_key", "key_id": "..." } }
auth_method distinguishes an API key from an OAuth token; key_id identifies the specific credential (useful for audit and revocation).
Webhook signing secrets
Webhook deliveries are signed with a per-organization secret. Verifiers need this secret to validate that a delivery came from Primitive — see Signature verification.
Retrieve the current secret (it is generated on first read if the org does not have one yet):
curl https://api.primitive.dev/v1/account/webhook-secret \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN"
{ "secret": "<opaque-base64-secret>" }Rotate the secret when it may have leaked:
curl -X POST https://api.primitive.dev/v1/account/webhook-secret/rotate \ -H "Authorization: Bearer $PRIMITIVE_AUTH_TOKEN"
{ "secret": "<opaque-base64-secret>" }Rotation is rate-limited to once per 60 minutes; a rotation inside that window returns rate_limit_exceeded with a Retry-After header. Rotating invalidates the previous secret immediately, so update your verifier before rotating in production.
How to obtain credentials
You do not start with a credential — you acquire one through one of these flows, then use it as a bearer token everywhere above.
- CLI device login. Run
primitive loginto authorize the CLI on your machine. It registers a client, opens the browser sign-in, and stores the resulting OAuth tokens locally, refreshing them automatically. See the CLI reference. - Agent signup (headless). An autonomous agent can self-provision a managed inbox with no API key and only one email-verification code. It can later be upgraded to a full account. See Skills & Agent Signup for the step-by-step flow, and Which onboarding fits? to choose a path.
- Third-party apps. Apps acting on behalf of a user use the OAuth flow above with Dynamic Client Registration.
See also
- REST API — response envelope, pagination, and endpoint inventory.
- Errors — the
unauthorized/forbiddencodes and recovery guidance. - CLI —
primitive loginand agent signup. - SDKs — language clients that handle the
Authorizationheader for you. - Signature verification — using the webhook signing secret to verify webhooks.
- Quickstart — get a credential and make your first call.