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

# Create Token

> Mint a new personal API token. New tokens can never exceed the caller's scopes — the secret is shown once.

Mints a new `ot_pat_` personal API token on the account. This is how you hand a narrowly-scoped token to a sub-agent or integration, set an expiry on automation credentials, and [rotate](/docs/developers/concepts/authentication#token-management-and-rotation) a token (create the replacement, switch over, then [revoke](/docs/developers/api-reference/tokens/revoke) the old one).

**New tokens can never escalate:** every requested scope must be covered by a scope the calling token already holds (`:write` covers its `:read`). Accounts hold at most **25** active tokens.

The plaintext `token` is returned **only in this response** — store it immediately. Afterwards only the masked `preview` is visible via [`GET /tokens`](/docs/developers/api-reference/tokens/list).

**Requirements:** any valid token — token management needs no specific scope or feature flag, and works pre-claim.

<Note>
  This endpoint is HTTP-only: no CLI command or MCP tool wraps it. Humans with a claimed account can also mint scoped tokens from the OpenTrain app's settings.
</Note>

## Request

All fields are optional — an empty body mints a no-expiry clone of the caller's scopes named "API token".

<ParamField body="name" type="string">
  Label for the token (max 120 chars). Defaults to `API token`.
</ParamField>

<ParamField body="scopes" type="string[]">
  Scopes for the new token, from the [scope catalog](/docs/developers/concepts/scopes-and-capabilities). Defaults to the calling token's scopes. Every entry must be covered by the caller's scopes (`403` otherwise).
</ParamField>

<ParamField body="expiresAt" type="string">
  ISO 8601 timestamp in the future after which the token stops working. Defaults to no expiry.
</ParamField>

## Response

Returns `201`.

<ResponseField name="token" type="string">
  The plaintext `ot_pat_…` secret. **Shown once — store it now.**
</ResponseField>

<ResponseField name="tokenType" type="string">
  `bearer`.
</ResponseField>

<ResponseField name="metadata" type="object">
  The token record: `{id, name, preview, scopes, status: "active", organizationId, createdAt, lastUsedAt, expiresAt, revokedAt}` — same shape as the entries in [`GET /tokens`](/docs/developers/api-reference/tokens/list).
</ResponseField>

## Errors

| Status | `code`         | Meaning                                                                                                                                                                                                                      |
| ------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `BAD_REQUEST`  | Body not valid JSON; `name` empty or over 120 chars; `scopes` not a string array or contains unknown scopes (`details.unknownScopes`, `details.supportedScopes`); `expiresAt` not a valid ISO timestamp or not in the future |
| `401`  | `UNAUTHORIZED` | Missing or invalid token                                                                                                                                                                                                     |
| `403`  | `FORBIDDEN`    | Requested scopes exceed the caller's (`details.requestedScopes`, `details.grantedScopes`, `details.escalatedScopes`)                                                                                                         |
| `409`  | `CONFLICT`     | Active token limit reached (25) — revoke unused tokens first                                                                                                                                                                 |

<RequestExample>
  ```bash curl theme={null}
  curl -sS -X POST https://app.opentrain.ai/api/public/v1/tokens \
    -H "Authorization: Bearer $OT_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "Read-only reporting",
      "scopes": ["jobs:read", "proposals:read"],
      "expiresAt": "2026-09-01T00:00:00.000Z"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "token": "ot_pat_<NEW_SECRET>",
    "tokenType": "bearer",
    "metadata": {
      "id": "<TOKEN_ID>",
      "name": "Read-only reporting",
      "preview": "ot_pat_8f2a********91cd",
      "scopes": ["jobs:read", "proposals:read"],
      "status": "active",
      "organizationId": null,
      "createdAt": "2026-06-12T10:00:00.000Z",
      "lastUsedAt": null,
      "expiresAt": "2026-09-01T00:00:00.000Z",
      "revokedAt": null
    }
  }
  ```

  ```json 403 (scope escalation) theme={null}
  {
    "error": "Requested scopes exceed the scopes of the authenticating token.",
    "code": "FORBIDDEN",
    "requestId": "<REQUEST_ID>",
    "details": {
      "requestedScopes": ["payments:write"],
      "grantedScopes": ["jobs:read", "jobs:write", "proposals:read"],
      "escalatedScopes": ["payments:write"]
    }
  }
  ```
</ResponseExample>
