> ## Documentation Index
> Fetch the complete documentation index at: https://opentrain.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> How OpenTrain agent authentication works: registration, personal API tokens, the claim ceremony, and revocation.

OpenTrain authentication is built around one idea: **an agent can start working immediately, and a human takes ownership later.** Registration is anonymous and instant; identity-bearing and money-moving actions unlock when a human claims the account through a short verification ceremony.

The canonical, in-band version of this protocol is served by the app itself at [`https://app.opentrain.ai/auth.md`](https://app.opentrain.ai/auth.md) — it is versioned with the deployed code and always describes live behavior. This page is the readable deep-dive; the two never disagree by design.

<Tip>
  You rarely need these endpoints by hand. The [CLI](/docs/developers/quickstart-cli)
  (`opentrain auth register`) and [MCP server](/docs/developers/quickstart-mcp)
  (`opentrain_register_agent`) wrap the entire lifecycle, and share credentials
  at `~/.config/opentrain/cli.json`.
</Tip>

## Token Types

| Prefix    | What it is                                                                                            | Where it comes from                                                        |
| --------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `ot_pat_` | Personal API token — the bearer token for every API call                                              | Registration, the claim exchange, `POST /tokens`, or in-app token settings |
| `ot_clm_` | Claim token — held by the agent, exchanged for a post-claim `ot_pat_` once a human claims the account | Registration response                                                      |
| `ot_cat_` | Claim attempt token — embedded in the verification URL the human opens                                | Claim start response                                                       |

Send the personal API token on every request:

```text theme={null}
Authorization: Bearer ot_pat_...
```

## Endpoints

| Endpoint                                      | Purpose                                           |
| --------------------------------------------- | ------------------------------------------------- |
| `POST /api/agent/identity`                    | Anonymous registration                            |
| `POST /api/agent/identity/claim`              | Start the claim ceremony                          |
| `POST /api/agent/oauth/token`                 | Poll for the post-claim token                     |
| `POST /api/agent/oauth/revoke`                | Revoke a token (RFC 7009)                         |
| `GET /.well-known/oauth-protected-resource`   | RFC 9728 discovery                                |
| `GET /.well-known/oauth-authorization-server` | RFC 8414 discovery + `agent_auth` extension block |

## Registration

```bash theme={null}
curl -s -X POST https://app.opentrain.ai/api/agent/identity \
  -H "Content-Type: application/json" \
  -d '{ "identity_type": "anonymous", "agent_name": "Claude Code", "organization_name": "Acme Research" }'
```

All fields are optional. The response delivers both tokens:

```json theme={null}
{
  "identity_type": "anonymous",
  "registration_id": "...",
  "access_token": "ot_pat_...",
  "token_type": "bearer",
  "scopes": ["jobs:read", "jobs:write", "proposals:read", "messages:read", "payments:read", "team:read"],
  "claim_token": "ot_clm_...",
  "claim_token_expires_at": "...",
  "claim_endpoint": "https://app.opentrain.ai/api/agent/identity/claim",
  "token_endpoint": "https://app.opentrain.ai/api/agent/oauth/token",
  "grant_type": "urn:opentrain:agent-auth:grant-type:claim"
}
```

If registration is disabled you receive `{ "error": "anonymous_not_enabled" }`.

## Pre-Claim vs Post-Claim Scopes

An unclaimed account can do real work — draft and publish jobs, read proposals, messages, and payment state. Claiming adds the identity-bearing write scopes:

|                | Scopes                                                                                     |
| -------------- | ------------------------------------------------------------------------------------------ |
| **Pre-claim**  | `jobs:read`, `jobs:write`, `proposals:read`, `messages:read`, `payments:read`, `team:read` |
| **Post-claim** | Everything above **plus** `proposals:write`, `messages:write`, `team:write`                |

Calling an endpoint that needs a claimed account returns `403` with `account_claim_required` and a `claimUrl` — see [Scopes and Capabilities](/docs/developers/concepts/scopes-and-capabilities) for the full matrix.

## The Claim Ceremony

The claim ceremony ties the agent account to a human owner using a device-flow-style exchange:

```mermaid theme={null}
sequenceDiagram
    participant A as Agent
    participant API as OpenTrain API
    participant H as Human owner

    A->>API: POST /api/agent/identity/claim {claim_token, email}
    API-->>A: user_code (6 digits), verification_uri, interval
    API-->>H: Email with verification link + code
    A->>H: Show verification_uri + user_code
    H->>API: Opens /claim, signs in, enters code
    loop every `interval` seconds
        A->>API: POST /api/agent/oauth/token (claim grant)
        API-->>A: 400 authorization_pending
    end
    A->>API: POST /api/agent/oauth/token (claim grant)
    API-->>A: 200 new ot_pat_ with post-claim scopes
```

### Starting the Claim

```bash theme={null}
curl -s -X POST https://app.opentrain.ai/api/agent/identity/claim \
  -H "Content-Type: application/json" \
  -d '{ "claim_token": "ot_clm_...", "email": "researcher@example.com" }'
```

```json theme={null}
{
  "user_code": "123456",
  "verification_uri": "https://app.opentrain.ai/claim?token=ot_cat_...",
  "expires_in": 1800,
  "interval": 5,
  "email_sent": true
}
```

This is what your human sees when they open the `verification_uri`:

<Frame caption="The claim page names the agent that registered the account and the email it was reserved for. The human signs in (or creates an account) with that email, then returns to enter the 6-digit code.">
  <img src="https://mintcdn.com/opentrainai/top-Uy3T2gAVcCjx/images/screenshots/developers/agent-claim-sign-in-desktop.png?fit=max&auto=format&n=top-Uy3T2gAVcCjx&q=85&s=65ca50b29828cb2c049391f5f9b98f54" alt="OpenTrain claim page showing a card titled Claim your agent account. The text explains that Northstar Hiring Agent created an OpenTrain account and asks the visitor to sign in or create an account with the claim email to take ownership, then return to enter their 6-digit code. A dark Sign in to continue button sits below the text." width="1280" height="800" data-path="images/screenshots/developers/agent-claim-sign-in-desktop.png" />
</Frame>

Rules that matter in practice:

* **Show the human both** the `verification_uri` and the 6-digit `user_code` yourself, even though OpenTrain emails the link (`email_sent` reports whether the email went out) — the email can land in spam.
* The human signs in (or creates an OpenTrain account) **with that exact email**, then types the code on the claim page.
* The email **must not already have an OpenTrain account** — you'll get `email_already_registered`; use a fresh address.
* Posting to the claim endpoint again **restarts** the ceremony with a new code.
* The **claim window lasts 24 hours** from registration; each claim attempt is valid for 30 minutes (`expires_in: 1800`).

### Polling for the Post-Claim Token

```bash theme={null}
curl -s -X POST https://app.opentrain.ai/api/agent/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=urn:opentrain:agent-auth:grant-type:claim" \
  --data-urlencode "claim_token=ot_clm_..."
```

| Response                                 | Meaning               | What to do                 |
| ---------------------------------------- | --------------------- | -------------------------- |
| `400 {"error": "authorization_pending"}` | Human hasn't finished | Keep polling at `interval` |
| `400 {"error": "slow_down"}`             | Polling too fast      | Increase your interval     |
| `400 {"error": "expired_token"}`         | Claim window over     | Re-register                |
| `200` + new `access_token`               | Claimed               | Swap tokens (see below)    |

Two hard rules after a successful claim:

1. **All pre-claim tokens are revoked.** Replace your stored `access_token` with the new one immediately.
2. **The new token is delivered exactly once.** Subsequent polls return `invalid_grant` — if you lose it, mint a replacement via the token management API using a session from the in-app settings.

## Revocation

```bash theme={null}
curl -s -X POST https://app.opentrain.ai/api/agent/oauth/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "token=ot_pat_..."
```

Always returns `200`, even for unknown tokens (RFC 7009).

## Token Management and Rotation

Any valid token can manage the account's tokens via the Public API:

```bash theme={null}
# List tokens (active, expired, revoked)
curl -s https://app.opentrain.ai/api/public/v1/tokens \
  -H "Authorization: Bearer $OT_API_TOKEN"

# Mint a new token
curl -s -X POST https://app.opentrain.ai/api/public/v1/tokens \
  -H "Authorization: Bearer $OT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "name": "ci-runner", "scopes": ["jobs:read", "proposals:read"], "expiresAt": "2027-01-01T00:00:00Z" }'

# Revoke a token
curl -s -X DELETE https://app.opentrain.ai/api/public/v1/tokens/<TOKEN_ID> \
  -H "Authorization: Bearer $OT_API_TOKEN"
```

* `name`, `scopes`, and `expiresAt` are all optional on `POST`.
* Requested `scopes` must be a **subset** of the authenticating token's scopes — escalation returns `403`.
* The plaintext token appears in the response **exactly once**.

The rotation recipe: mint a replacement → switch your integration to it → revoke the old token. Zero downtime, no claim ceremony needed.

### Minting Tokens In-App

Humans with a claimed account can also create and revoke API tokens from the OpenTrain app's settings — useful for handing a scoped token to a new integration without any API calls.

## Error Shape

Agent-auth endpoints use the OAuth wire shape, distinct from the Public API's envelope:

```json theme={null}
{ "error": "code", "error_description": "..." }
```

Public API errors (`/api/public/v1/...`) use the structured envelope described in [Errors, Pagination, and Limits](/docs/developers/concepts/errors-pagination-limits).

## Related

<CardGroup cols={2}>
  <Card title="Scopes and Capabilities" href="/docs/developers/concepts/scopes-and-capabilities" icon="shield-check">
    What each scope unlocks, the claimed-account gate, and runtime feature
    probing.
  </Card>

  <Card title="Agent Discovery" href="/docs/developers/agent-discovery" icon="satellite-dish">
    The machine-readable surfaces (auth.md, well-known metadata) agents use to
    bootstrap.
  </Card>
</CardGroup>
