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

# Authentication

> Bearer API keys, the scopes they carry, how they are stored, what key mode actually controls — and the dashboard session, the credential humans hold.

Every call to `/v1/*` except `POST /v1/signup` requires a bearer token:

```bash theme={null}
curl https://api.farthing.ai/v1/checkouts \
  -H "authorization: Bearer ck_test_3sQd7yQ0mHq9pP2rVwXk1tLb"
```

Two credentials arrive on that header: **API keys**, which this page is mostly about, and
**[dashboard sessions](#dashboard-sessions)** — the short-lived token a signed-in human
holds at `app.farthing.ai`. Keys are for agents; sessions are for the person in the
dashboard, and your integration never handles one. There is no third scheme — no
query-string keys, no HMAC request signing, no OAuth. The one exception is the live-view
embed, which is gated by a separate short-lived JWT rather than your API key — see
[Live view](/live-view).

## Key format

```
ck_test_<24 random bytes, base64url>
ck_live_<24 random bytes, base64url>
```

The mode is in the key itself on purpose, and it is not just a label — see
[what key mode actually controls](#what-key-mode-actually-controls). A misrouted key should be
obvious at a glance in a config file or a log line, and it should also refuse to do damage.

## Keys are shown once

The API stores only `sha256(key)`, plus enough non-secret metadata for a dashboard to render
a key row: its 16-character prefix, its mode, its scopes, when it was created, and when it was
last used.

Consequences, stated plainly:

* The plaintext is returned exactly once, in the `key` field of the `POST /v1/api-keys`
  response (or `apiKey` from `POST /v1/signup`). Nothing can recover it afterwards.
* Losing a key is not a support ticket. Create a new one and revoke the old one.
* A key is scoped to one tenant. What it may *do* within that tenant is narrowed by
  [scopes](#scopes); *whose data* it may do it to is narrowed by
  [`subject`](#binding-a-key-to-one-end-user). Both are optional and both are fixed at
  creation.

## Scopes

A scope is `<resource>.read` or `<resource>.write`, plus the wildcard `*`.

```
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
billing.read          billing.write
*
```

The resources are exactly the route groups mounted under `/v1`, so the boundary you reason
about — "this key may read checkouts and nothing else" — is the same boundary the code
enforces. There is no mapping table in between to fall out of date.

The `billing.*` scopes exist because the route group does, but they buy a key nothing:
`/v1/billing` refuses API keys outright, whatever their scopes — see
[dashboard sessions](#dashboard-sessions).

### The three rules

<Steps>
  <Step title="write implies read on the same resource">
    A key holding `buyer-profiles.write` can `GET` a profile without also being granted
    `buyer-profiles.read`. Requiring both would be noise, not safety.

    It does **not** work in the other direction, and it does not cross resources.
  </Step>

  <Step title="A key issued without scopes has full access">
    Omitting `scopes` at creation stores `["*"]`, which is what a key has always meant here —
    and what every key issued before scopes existed still carries. Narrowing is opt-in, per
    key, at creation. There is no way to widen or re-scope a key afterwards; create a new one.
  </Step>

  <Step title="No escalation">
    A key cannot mint a key with scopes it does not itself hold. Without that rule
    `api-keys.write` would be equivalent to `*` in one extra call — mint a wildcard key, then
    use it.

    ```json 403 theme={null}
    {
      "error": true,
      "message": "A key cannot grant scopes it does not itself hold.",
      "heldScopes": ["checkouts.write"]
    }
    ```
  </Step>
</Steps>

### Out-of-scope requests

Enforcement happens once, on the whole `/v1` surface, before any route sees the request. The
resource comes from the first path segment and read/write from the HTTP method — so `DELETE`
counts as a write, and a route added later is covered by construction rather than by someone
remembering.

```json 403 theme={null}
{
  "error": true,
  "message": "This API key is missing the 'buyer-profiles.read' scope.",
  "requiredScope": "buyer-profiles.read"
}
```

`requiredScope` is there so you never have to parse the message.

<Note>
  An unrecognised resource is denied, not allowed. A path under `/v1` that is not one of the
  seven resources returns `404 Unknown resource` — a new route group is unreachable until it is
  named in the scope list, which is the failure direction you want.
</Note>

### Why this matters more for agents

The holder of a key is often an autonomous process, and the useful question is not "do I trust
this tenant" but *what is the worst this particular agent can do if its prompt is turned
against it*.

```bash theme={null}
# An agent that runs checkouts and picks from stored addresses. Nothing else.
curl -sX POST https://api.farthing.ai/v1/api-keys \
  -H "authorization: Bearer $FARTHING_ADMIN_KEY" \
  -H 'content-type: application/json' \
  -d '{"mode":"live","label":"shopping-agent","scopes":["checkouts.write","buyer-profiles.read"]}'
```

That key cannot exfiltrate your usage figures, cannot repoint your webhooks at someone else's
server, and cannot mint itself a better key. Narrow further by dropping
`buyer-profiles.read` and handing the agent a `buyerProfileId` directly — attaching a profile
to a checkout does not require the scope to read it, so an agent holding only
`["checkouts.write"]` can still ship to an address it can never enumerate.

## Binding a key to one end user

Scopes narrow what a key may do. `subject` narrows whose data it may do it to — the second axis,
and the one that matters if you run one agent on behalf of many people.

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

A `subject` is your own opaque id for one of your end users; we never generate one. A key
carrying one sees and creates only that person's checkouts and buyer profiles, cannot list or
revoke account keys, cannot read `/v1/usage`, cannot manage webhooks, and cannot mint a key for
anybody else. A key without one is tenant-wide — which is what every key issued before this
existed already is, so the mechanism is inert until you use it.

Send a real id or omit the field: `""` is a `400`, and a misspelled `subjectt` is a `400` too, so
there is no body that returns `201` with a key you believe is bound and is not.

Full contract, including what it deliberately does not cover:
[End-user isolation](/end-user-isolation).

## Managing keys

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

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

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

```json Create → 201 theme={null}
{
  "id": "2a44c7f0-1b23-4a95-9d0e-77e1b6c4a812",
  "mode": "test",
  "scopes": ["checkouts.write"],
  "keyPrefix": "ck_test_3sQd7yQ",
  "key": "ck_test_3sQd7yQ0mHq9pP2rVwXk1tLb"
}
```

```json List → 200 theme={null}
{
  "data": [
    {
      "id": "2a44c7f0-1b23-4a95-9d0e-77e1b6c4a812",
      "label": "ci",
      "mode": "test",
      "scopes": ["checkouts.write"],
      "subject": null,
      "keyPrefix": "ck_test_3sQd7yQ",
      "lastUsedAt": "2026-07-29T09:14:31.190Z",
      "revokedAt": null,
      "createdAt": "2026-07-29T09:02:10.771Z"
    }
  ],
  "nextCursor": null
}
```

Read the array from `data`, the field the checkout and buyer-profile lists also use. The same array
comes back as `keys` too — the original field name, kept for existing callers — and `nextCursor` is
always `null` because this collection is not paged.

Listing never returns a secret — the `key` field exists only on the creation response. It does
return `scopes` and `subject`, which is how you audit what each outstanding key can reach and
whose data it can reach it on. `subject: null` is a tenant-wide key.

### Rotation

Any key holding `api-keys.write` can mint another, so rotation needs no downtime and no
dashboard:

1. `POST /v1/api-keys` — deploy the new key. Copy the old key's `scopes` unless you are
   deliberately narrowing it.
2. Confirm the old key's `lastUsedAt` has stopped advancing.
3. `DELETE /v1/api-keys/{id}` on the old one.

<Warning>
  A key scoped `["checkouts.write"]` cannot rotate itself — it holds no `api-keys.write`. That is
  the intended trade: keep one broad key wherever you keep your other deployment secrets, and use
  it to issue the narrow ones your agents run on.
</Warning>

Revocation is a timestamp, not a row delete. A revoked key stays in the list with
`revokedAt` set and stops authenticating immediately. Keeping the record matters more than a
tidy table if a key is ever abused — you want to be able to say which key was live when.

## What key mode actually controls

Mode is enforced, not decorative. **A `ck_test_` key can only check out at sandbox
merchants** — merchants we control — no matter how your account is configured.

`POST /v1/checkouts` confines a request to sandbox merchants if **either** of these is true:

1. The account is sandbox-only, or
2. the request arrived on a `ck_test_` key.

So a fully activated, live account holding a test key still cannot spend real money. The two
triggers are independent on purpose: account status is a property of who you are, key mode is
a property of the credential in front of you, and each should be able to stop a real purchase
on its own.

```bash theme={null}
# Same account, same body. Different key, different outcome.
curl -sX POST https://api.farthing.ai/v1/checkouts \
  -H "authorization: Bearer ck_test_…" -H 'content-type: application/json' \
  -d '{"target":{"url":"https://example-store.com/products/widget"}}'
# → 403  Cannot check out at example-store.com: This request used a test key. …

curl -sX POST https://api.farthing.ai/v1/checkouts \
  -H "authorization: Bearer ck_live_…" -H 'content-type: application/json' \
  -d '{"target":{"url":"https://example-store.com/products/widget"}}'
# → 201
```

The full refusal, including which of the two reasons applied, is documented in
[Sandbox](/sandbox#what-you-cannot-do).

### Minting a live key

Two independent things have to be true, and each refusal is a `403`.

**The account must be activated.** A sandbox-only account cannot create a live key at all:

```json theme={null}
{
  "error": true,
  "message": "This account is sandbox-only. Live keys require activation."
}
```

**The calling key must itself be live.** A `ck_test_` key cannot issue a `ck_live_` one, however
activated the account is and however broad the test key's scopes are:

```json theme={null}
{
  "error": true,
  "message": "A test key cannot issue a live key. Use a live key, or activate the account."
}
```

That second rule is the same one that governs scopes and `subject`: authority passes down, never
sideways. Without it, the sandbox confinement a test key exists to provide would be one `POST` deep
— a tenant that deliberately handed its agent a test key could be talked into minting a live one,
and every guarantee on this page would evaporate. A live key minting a test key is fine; that is
narrowing.

With an activated account and a live key in hand, `{"mode":"live"}` works, and only then can any of
its keys reach a real merchant.

Where the first live key comes from: the dashboard. A [dashboard session](#dashboard-sessions)
on an activated workspace counts as a live-mode credential, so the workspace owner mints the
first `ck_live_` key there — see [Going live](/going-live#your-first-live-key).

<Tip>
  This gives you a genuinely spend-proof credential for CI, staging, and anything running
  unattended: issue a `test` key. It exercises the entire product — real browser, real
  storefront, real card entry, a genuinely placed order — against a sandbox merchant, and it
  cannot be pointed anywhere else even by accident.
</Tip>

<Note>
  Mode, scopes and `subject` are all fixed at creation. There is no way to promote a `test` key to
  `live`, no way to re-scope a key, and no way to rebind or unbind one — create a new one and
  revoke the old one.
</Note>

## Dashboard sessions

The second credential, stated briefly because an integration never touches it.

When you are signed in at `app.farthing.ai`, your browser holds a short-lived session token
(a Clerk JWT, expiring in about a minute), and the dashboard calls this same API with it —
same `Authorization: Bearer` header, dispatched by shape: a session token is a JWT, a key
starts with `ck_`, and neither can be mistaken for the other.

What a session gets:

* **Your workspace, from membership** — never from anything the request claims. It acts as
  the owner: full scopes, no `subject` binding, and key mode follows the workspace's state —
  a sandbox workspace's session behaves like a test key, an activated one like a live key.
* **The session-only surface.** `/v1/workspaces` (provisioning) and `/v1/billing` (plan,
  identity verification, AUP) refuse API keys with `401`. Money and membership are decisions
  the accountable human makes in a browser — and refusing keys there means a leaked agent
  key cannot re-plan, de-verify, or re-terms a workspace.

What a session cannot do: exist outside Clerk. There is no endpoint that mints one and no
way to derive one from a key, so the session-only surface is unreachable to any credential
you could leak into an agent.

One session-specific refusal to know: a valid session whose account has no workspace yet
gets `403` on `/v1/*` with a machine-readable marker —

```json 403 theme={null}
{ "error": true, "message": "No workspace for this account", "code": "no_workspace" }
```

— and the same `code` with `404` on `GET /v1/workspaces/me`. `POST /v1/workspaces` is the
fix. The dashboard keys its onboarding off that code; nothing agent-facing ever produces it.

## Auth failures

| Status | Body `message`                                   | Cause                                                                                                                         |
| ------ | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `Missing bearer token`                           | No `Authorization` header, or it does not match `Bearer <token>`.                                                             |
| `401`  | `Invalid or revoked API key`                     | The token's hash matches no active key row.                                                                                   |
| `401`  | `Invalid session token: …`                       | A [dashboard session](#dashboard-sessions) that failed verification — expired, wrong issuer, minted for another application.  |
| `401`  | `… requires a dashboard session, not an API key` | An API key on the session-only surface: `/v1/workspaces` or `/v1/billing`.                                                    |
| `403`  | `Tenant suspended`                               | The credential is valid but the tenant has `suspended_at` set. Contact us.                                                    |
| `403`  | `This API key is missing the '<scope>' scope.`   | The key authenticated but is not scoped for this route. Carries `requiredScope`.                                              |
| `403`  | `A subject-bound key cannot …`                   | The key is [bound to one end user](/end-user-isolation) and asked for an account-wide fact: the key list, usage, or webhooks. |
| `403`  | `No workspace for this account`                  | A valid session with no workspace yet. Carries `code: "no_workspace"`.                                                        |
| `404`  | `Unknown resource`                               | A path under `/v1` that is not one of the seven scoped resources.                                                             |

They all come back as `{ "error": true, "message": "..." }`. There is no distinction in the
401 between "never existed" and "revoked" — deliberately, since that difference is only
useful to someone probing.

Note the ordering: authentication runs first, then scope, then any route-specific check. A
revoked key gets `401` whatever it was asking for; a valid but narrow key gets `403` with the
scope named. One consequence on `/v1/billing`: a key without `billing.*` scopes gets the
scope `403` first, while a full-access key reaches the session-only check and gets its `401`
— either way, no API key gets in.

<Note>
  `lastUsedAt` is written best-effort on every authenticated request and never blocks it. Treat
  it as a good signal for rotation, not as an audit log.
</Note>
