---
title: "Authentication & API Keys"
description: "API keys, OAuth access tokens, webhook secrets, and how every credential is scoped to a single organization."
canonical: "https://docs.primitive.dev/docs/auth"
last-updated: "2026-05-17T00:00:00.000Z"
---

# Primitive authentication and API keys

Every authenticated request to `https://api.primitive.dev/v1` carries a bearer credential in the `Authorization` header. Primitive issues three kinds of bearer credential, each scoped to a single organization:

CredentialPrefixUse it forLifetimeAPI key`prim_`Server-to-server integrations, CI, headless agentsLong-lived until revokedOAuth access token`prim_oat_`CLI, desktop, local-agent, and third-party app accessShort-lived (1 hour), refreshableAgent connection key`pconn_`External runtime paired to one email addressUntil reconnect or revoke

```
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](#introspection-with-v1whoami).

## 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](#how-to-obtain-credentials).

## Agent connection keys

An owner creates a private invitation through `POST /v1/agent-connections`. The runtime claims it once through `POST /v1/agent-connections/claim` and stores the returned opaque `pconn_` key privately. Use that key as an ordinary HTTP bearer credential, with the [public setup guide](https://api.primitive.dev/v1/agent-connections/setup).

The key permits mail reads for its address, sending as its address, organization address-note reads, writing its own notes, and disconnecting its own address with `DELETE /v1/agent-connections/{address}`. It cannot call account, whoami, threads, raw downloads, or other connection-management operations. Claim returns its bound address and trusted setup owner address. Reconnect preserves the address and revokes its previous key.

## 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_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](/docs/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_id_issued_at": 1735689600,
  "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_verifier` and its `S256` `code_challenge`.

- 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.

- Primitive redirects back to your `redirect_uri` with a one-time `code`. 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](/docs/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](/docs/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. Rotation is dual-signed: deliveries carry signatures from both the previous and the new secret during a short grace window, and the previous secret is only retired once the new one is fully promoted, so a verifier still on the previous secret keeps validating while you roll it over.

## 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](/docs/cli).

- 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](/docs/skills) for the step-by-step flow, and [Which onboarding fits?](/docs/which-onboarding) to choose a path.

- Third-party apps. Apps acting on behalf of a user use the [OAuth flow](#oauth-authorization-code--pkce) above with Dynamic Client Registration.

## See also

- [REST API](/docs/api) — response envelope, pagination, and endpoint inventory.

- [Errors](/docs/errors) — the `unauthorized` / `forbidden` codes and recovery guidance.

- [CLI](/docs/cli) — `primitive login` and agent signup.

- [SDKs](/docs/sdks) — language clients that handle the `Authorization` header for you.

- [Signature verification](/docs/signature-verification) — using the webhook signing secret to verify webhooks.

- [Quickstart](/docs/quickstart) — get a credential and make your first call.

Maintained by the [primitive.dev team](/about) · [Trust & security](/trust)Last updated: May 17, 2026
