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:

CredentialPrefixUse it forLifetime
API keyprim_Server-to-server integrations, CI, headless agentsLong-lived until revoked
OAuth access tokenprim_oat_CLI, desktop, local-agent, and third-party app accessShort-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:

FieldValue
response_typescode
grant_typesauthorization_code, refresh_token
code_challenge_methodsS256
token_endpoint_auth_methodnone (public clients)
scopeprimitive: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

  1. Generate a PKCE code_verifier and its S256 code_challenge.
  2. Send the user to GET /oauth/authorize with response_type=code, your client_id, redirect_uri, code_challenge, and code_challenge_method=S256. The user signs in and picks the organization to authorize.
  3. Primitive redirects back to your redirect_uri with a one-time code. Authorization codes expire after 10 minutes and can be exchanged only once.
  4. 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 login to 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 / forbidden codes and recovery guidance.
  • CLIprimitive login and agent signup.
  • SDKs — language clients that handle the Authorization header for you.
  • Signature verification — using the webhook signing secret to verify webhooks.
  • Quickstart — get a credential and make your first call.