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

> ## Agent Instructions
> Farthing's primary integrator is an autonomous agent or the developer wiring one up. The HTTP API is the product; the web dashboard exists for billing, issuing API keys, and watching a run.
> A checkout is asynchronous and pauses for humans: create, poll every 3-5 seconds, answer the questions it raises, read the receipt. A run takes 2-3 minutes and can park for up to 10 minutes on one question. Never present it as a synchronous request/response call.
> Branch on pendingUserAction.key. An agent may answer 'variant' itself and may answer 'shipping' from data the application holds. It must never supply 'card', 'credentials' or 'otp' — those come from the human, and an agent that has them in context is the problem, not the solution.
> Base URL is https://api.farthing.ai and auth is Authorization: Bearer ck_test_... or ck_live_.... API keys carry scopes; give an agent the narrowest set that does its job. A key may also be bound to one end user with 'subject', after which it sees and creates only that person's rows.
> POST /v1/checkouts takes an Idempotency-Key header. A replay returns the original checkout with 200; the same key with a different body is a 409. Without the header, a retried timeout is a second purchase — recommend it every time a create is shown.
> Pricing: sandbox is free with no card; paid plans are Personal $9, Business $49, Scale $249 per month, plus a flat $1.00 per COMPLETED checkout on every tier. Failed and cancelled checkouts are never billed. Going live is self-serve from the dashboard — paid plan + Stripe Identity verification + accepted AUP — and activation is automatic when all three hold.
> Every example in these docs is a shape the API actually produces. Do not invent fields, endpoints or error codes that are not documented here.

# API keys

> Create, list and revoke API keys, and the scopes they carry.

Key management is self-serve: any key holding `api-keys.write` can mint another, so rotation
needs no dashboard and no downtime. Concepts in [Authentication](/authentication#scopes).

## Create a key

```http theme={null}
POST /v1/api-keys
```

Success: `201`.

<CodeGroup>
  ```bash Tenant-wide theme={null}
  curl -sX POST https://api.farthing.ai/v1/api-keys \
    -H "authorization: Bearer $FARTHING_KEY" \
    -H 'content-type: application/json' \
    -d '{"mode":"live","label":"shopping-agent","scopes":["checkouts.write"]}'
  ```

  ```bash Bound to one end user theme={null}
  curl -sX POST https://api.farthing.ai/v1/api-keys \
    -H "authorization: Bearer $FARTHING_KEY" \
    -H 'content-type: application/json' \
    -d '{"mode":"live","label":"agent for user_a91f","scopes":["checkouts.write"],"subject":"user_a91f"}'
  ```
</CodeGroup>

<ParamField body="mode" type="&#x22;test&#x22; | &#x22;live&#x22;" default="test">
  Determines both the key's prefix and what it can reach.

  * `test` → `ck_test_…`. **Confined to sandbox merchants**, whatever the account's status.
  * `live` → `ck_live_…`. Can check out at real merchants.

  A sandbox-only account cannot create a `live` key.
</ParamField>

<ParamField body="label" type="string">
  Optional, max 200 characters. Shown in the list; never sent anywhere.
</ParamField>

<ParamField body="scopes" type="string[]">
  1–32 entries. **Omit for full access** — the key gets `["*"]`, which is what a key has always
  meant here and what every key issued before scopes existed still carries.

  Valid values:

  ```
  *
  checkouts.read        checkouts.write
  buyer-profiles.read   buyer-profiles.write
  buyer-sessions.read   buyer-sessions.write
  webhooks.read         webhooks.write
  api-keys.read         api-keys.write
  usage.read            usage.write
  ```

  `write` implies `read` on the same resource, so `["checkouts.write"]` is enough to create,
  poll, cancel and answer checkouts. Scopes are fixed at creation — there is no re-scoping.
</ParamField>

<ParamField body="subject" type="string">
  Bind the key to one of your end users. 1 to 200 characters after trimming, your own opaque id —
  we never generate one. **Omit for a tenant-wide key**, which is what every key issued before this
  existed already is. `""` is a `400`, not a tenant-wide key by another name.

  A bound key sees and creates only that subject's checkouts and buyer profiles, and is refused
  outright on the account-wide endpoints (`GET`/`DELETE /v1/api-keys`, `/v1/usage`,
  `/v1/webhooks`). Fixed at creation — there is no rebinding and no unbinding.

  Minting from a bound key **inherits** the binding: omitting `subject` gives you another key for
  the same end user, not a tenant-wide one, and naming a different subject is a `403`. Full
  contract in [End-user isolation](/end-user-isolation).
</ParamField>

An empty body is valid and produces a full-access `test` key — tenant-wide, unless the key you
called with is itself bound. The body is otherwise **strict**: an unknown field is a `400` with an
`unrecognized_keys` issue, so a misspelled `subjectt` cannot hand you a tenant-wide key you believed
was bound.

```json Response (201) — the bound variant theme={null}
{
  "id": "2a44c7f0-1b23-4a95-9d0e-77e1b6c4a812",
  "mode": "live",
  "scopes": ["checkouts.write"],
  "subject": "user_a91f",
  "keyPrefix": "ck_live_9Hf2QzT",
  "key": "ck_live_9Hf2QzT0mHq9pP2rVwXk1tLb"
}
```

`subject` appears here **only when the key is bound**. A tenant-wide key's creation response has
no `subject` field at all.

<Warning>
  `key` is returned exactly once, here. Only `sha256(key)` is stored — no endpoint, support
  process or database query can recover it.
</Warning>

<ResponseField name="keyPrefix" type="string">
  The first 16 characters. Safe to store and display next to a key row — it identifies which
  key without being one.
</ResponseField>

### Errors

| Status | Message                                                                                                                                                                                     | Extra field   |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `400`  | `Invalid request` (with `issues`) — `mode` was not `test` or `live`, `scopes` was empty / over 32 entries, `subject` was empty or over 200 characters, or the body carried an unknown field | `issues`      |
| `400`  | `Unknown scope(s): …` — a string that is not in the valid set                                                                                                                               | `validScopes` |
| `403`  | `This account is sandbox-only. Live keys require activation.`                                                                                                                               | —             |
| `403`  | `A test key cannot issue a live key. Use a live key, or activate the account.`                                                                                                              | —             |
| `403`  | `A key cannot grant scopes it does not itself hold.`                                                                                                                                        | `heldScopes`  |
| `403`  | `This API key is bound to subject '<s>' and cannot issue keys for another.`                                                                                                                 | —             |

```json 400 — bad scope string theme={null}
{
  "error": true,
  "message": "Unknown scope(s): checkouts.admin",
  "validScopes": ["*", "checkouts.read", "checkouts.write", "buyer-profiles.read", "..."]
}
```

```json 403 — escalation refused theme={null}
{
  "error": true,
  "message": "A key cannot grant scopes it does not itself hold.",
  "heldScopes": ["checkouts.write"]
}
```

<Note>
  The no-escalation check is what stops `api-keys.write` being a one-call route to `*`. A key can
  only ever hand out authority it already holds, so the widest key in your account is the widest
  key that can ever exist in it.
</Note>

***

## List keys

```http theme={null}
GET /v1/api-keys
```

Success: `200`.

```bash theme={null}
curl -s https://api.farthing.ai/v1/api-keys \
  -H "authorization: Bearer $FARTHING_KEY"
```

```json theme={null}
{
  "data": [
    {
      "id": "2a44c7f0-1b23-4a95-9d0e-77e1b6c4a812",
      "label": "agent for user_a91f",
      "mode": "live",
      "scopes": ["checkouts.write"],
      "subject": "user_a91f",
      "keyPrefix": "ck_live_9Hf2QzT",
      "lastUsedAt": "2026-07-29T09:14:31.190Z",
      "revokedAt": null,
      "createdAt": "2026-07-29T09:02:10.771Z"
    },
    {
      "id": "8b7c6d5e-4f30-4a21-9b8c-0d1e2f3a4b5c",
      "label": "ci",
      "mode": "test",
      "scopes": ["*"],
      "subject": null,
      "keyPrefix": "ck_test_3sQd7yQ",
      "lastUsedAt": null,
      "revokedAt": "2026-07-27T11:44:09.331Z",
      "createdAt": "2026-07-20T08:15:02.640Z"
    }
  ],
  "nextCursor": null
}
```

**Read `data`.** It is the field checkouts, buyer profiles and webhook endpoints already answer with,
and this route now matches them.

The response also repeats that identical array as `keys` — the field's original name, elided from
the example above for length. It is kept because existing callers and the published MCP client read
it, and it is not going away; `data` is simply the one to write new code against.

`nextCursor` is always `null` here — the collection is small and tenant-bounded, so there is nothing
to page. It exists so a generic list helper does not need a special case.

Newest first. **Revoked keys are included**, with `revokedAt` set — filter client-side if you only
want active ones.

Listing never returns a secret. `keyPrefix` is the most a list can show. It does return
`scopes`, which is how you audit what each outstanding key can reach; `["*"]` is a full-access
key.

<ResponseField name="subject" type="string | null">
  Always present here, unlike on the creation response. `null` is a tenant-wide key; a string is
  a key bound to that one end user. This listing is the only place you can audit which of your
  keys act for whom — see [End-user isolation](/end-user-isolation).
</ResponseField>

<ResponseField name="lastUsedAt" type="string | null">
  Written best-effort on every authenticated request; never blocks it. A good rotation signal,
  not an audit log.
</ResponseField>

<Warning>
  A [subject-bound key](/end-user-isolation) cannot call this endpoint or `DELETE /v1/api-keys/{id}`
  — both are `403`. The key list is a tenant-wide fact, and revocation is a tenant-wide power: a
  credential acting for one end user must not be able to enumerate its siblings or take the
  account's own key offline.
</Warning>

***

## Revoke a key

```http theme={null}
DELETE /v1/api-keys/{id}
```

Success: `200`.

```bash theme={null}
curl -sX DELETE https://api.farthing.ai/v1/api-keys/$KEY_ID \
  -H "authorization: Bearer $FARTHING_KEY"
```

```json theme={null}
{ "revoked": true }
```

Takes effect immediately. Revocation is a timestamp, not a row delete — knowing which key was
live when matters more than a tidy table if one is ever abused.

`404 Key not found or already revoked` covers all three cases: unknown id, another tenant's
key, and one already revoked.

<Warning>
  Nothing stops a key revoking itself. Revoke the old key **after** you have confirmed the new
  one is in use — check that the old key's `lastUsedAt` has stopped advancing.
</Warning>
