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

# Errors and limits

> Every status code the API returns, the quotas and their real numbers, how maxCost is enforced, and what is honestly missing.

## Error shape

Every error is the same object:

```json theme={null}
{ "error": true, "message": "Checkout not found" }
```

Validation failures add the raw Zod issues, which are precise enough to point at the offending
field:

```json theme={null}
{
  "error": true,
  "message": "Invalid request",
  "issues": [
    {
      "code": "invalid_string",
      "validation": "url",
      "path": ["target", "url"],
      "message": "Invalid url"
    }
  ]
}
```

There are no error codes on the agent-facing surface. Match on the HTTP status; treat
`message` as text for a human, and `issues[].path` as the machine-readable part when it is
present. (The session-only workspace routes are the exception: they add a `code` field —
`no_workspace`, `workspace_exists` — for the dashboard to key its onboarding off. See
[dashboard sessions](/authentication#dashboard-sessions).)

One error carries an extra machine-readable field. A sandbox confinement `403` includes the
hosts you *can* reach, so you never have to parse the message:

```json theme={null}
{
  "error": true,
  "message": "Cannot check out at example-store.com: This request used a test key. Use a live key to check out at real merchants. Sandbox merchants: your-sandbox-store.myshopify.com.",
  "sandboxMerchants": ["your-sandbox-store.myshopify.com"]
}
```

The two reasons a request is confined — a sandbox-only account, or a `ck_test_` key — are
distinguished in the message. See [Sandbox](/sandbox).

Three others carry a machine-readable field, all to do with
[scopes](/authentication#scopes):

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

```json 400 — unknown scope at key creation theme={null}
{
  "error": true,
  "message": "Unknown scope(s): checkouts.admin",
  "validScopes": ["*", "checkouts.read", "checkouts.write", "..."]
}
```

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

One more on `POST /v1/checkouts`, when your `maxCost` asks for more than your plan allows —
`planCeilingUsd` carries the ceiling, so you can cap and retry without parsing the message:

```json 400 — maxCost above the plan's per-checkout ceiling theme={null}
{
  "error": true,
  "message": "maxCost 250.00 exceeds this plan's per-checkout ceiling of $200.00.",
  "planCeilingUsd": 200
}
```

See [the plan ceiling](#the-plan-ceiling) for the semantics — refused, never clamped.

And one more, on an [idempotency](#idempotency) conflict — `checkoutId` names the checkout the
key already refers to:

```json 409 — Idempotency-Key reused with a different body theme={null}
{
  "error": true,
  "message": "This Idempotency-Key was already used with a different request body.",
  "checkoutId": "1f2e3d4c-5b6a-4790-8123-abcdef012345"
}
```

<Note>
  Two endpoints break the JSON pattern: `DELETE /v1/checkouts/{id}` returns `202` with an empty
  body, and the `/embed` routes return plain text (`Invalid or expired embed token`) because
  they are consumed by a browser, not an API client.
</Note>

## Status codes

| Code  | When                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200` | Successful read, successful action submit, successful revoke/delete. Also an [idempotent replay](#idempotency) of `POST /v1/checkouts`.                                                                                                                                                                                                                                                                                |
| `201` | Created: checkout, buyer profile, API key, webhook endpoint, signup.                                                                                                                                                                                                                                                                                                                                                   |
| `202` | `DELETE /v1/checkouts/{id}`. Cancellation accepted, not yet applied. Empty body.                                                                                                                                                                                                                                                                                                                                       |
| `400` | Malformed or invalid body; an unknown top-level field on a create body; unknown `status` filter; unparseable `from`/`to`; `buyerProfileId` that does not exist or, to a [subject-bound key](/end-user-isolation), belongs to another end user; a `maxCost` above [your plan's per-checkout ceiling](#the-plan-ceiling).                                                                                                |
| `401` | Missing, malformed, invalid or revoked bearer token. Expired or mismatched embed token. Also an API key on the session-only surface (`/v1/workspaces`, `/v1/billing`) — see [dashboard sessions](/authentication#dashboard-sessions).                                                                                                                                                                                  |
| `403` | Account suspended; missing scope; sandbox confinement; live-key request from a sandbox-only account or on a test key; a key trying to mint a broader key; a [subject-bound key](/end-user-isolation) reaching for an account-wide fact or another end user; a `buyerProfileId` belonging to a different end user than the checkout; signup closed; a dashboard session with no workspace yet (`code: "no_workspace"`). |
| `404` | Not found, or owned by another tenant — or by another end user. None of the three are distinguished. Also `Unknown resource` for a path under `/v1` that is not a scoped resource.                                                                                                                                                                                                                                     |
| `409` | The action you are answering is already `submitted`, `declined` or `expired`; an `Idempotency-Key` was reused with a different body; or a second workspace for an account that has one (`code: "workspace_exists"`).                                                                                                                                                                                                   |
| `429` | A quota was hit. See below.                                                                                                                                                                                                                                                                                                                                                                                            |

### The 404s

| Message                            | Endpoint                                                                           |
| ---------------------------------- | ---------------------------------------------------------------------------------- |
| `Checkout not found`               | `GET`/`DELETE /v1/checkouts/{id}`, `POST .../embed-token`, `POST .../actions/{id}` |
| `Action not found`                 | `POST /v1/checkouts/{id}/actions/{actionId}` — no such action **on this checkout** |
| `Profile not found`                | `GET`/`PATCH`/`DELETE /v1/buyer-profiles/{id}`                                     |
| `Session not found`                | `DELETE /v1/buyer-sessions/{id}`                                                   |
| `Endpoint not found`               | `DELETE /v1/webhooks/{id}`                                                         |
| `Key not found or already revoked` | `DELETE /v1/api-keys/{id}`                                                         |

Every lookup is scoped to the authenticated tenant. Another tenant's checkout is a `404`, not
a `403` — you learn nothing about whether it exists.

### The 409s

```json theme={null}
{ "error": true, "message": "Action already submitted" }
```

On `POST /v1/checkouts/{id}/actions/{actionId}`, this is also your idempotency signal. If your
own submit retries and comes back `Action already submitted`, the first attempt landed — poll
the checkout rather than treating it as a failure.

On `POST /v1/checkouts`, a `409` means something different: an `Idempotency-Key` you have used
before, with a different body. See below.

## Idempotency

`POST /v1/checkouts` accepts an `Idempotency-Key` request header. Send the same key twice and
the second call returns the **original** checkout with `200` instead of creating a second one.

```bash theme={null}
curl -sX POST https://api.farthing.ai/v1/checkouts \
  -H "authorization: Bearer $FARTHING_KEY" \
  -H 'content-type: application/json' \
  -H "idempotency-key: acme-4412" \
  -d '{"target":{"url":"https://your-sandbox-store.myshopify.com/products/widget","request":"buy this in medium"}}'
```

<Warning>
  **It is opt-in, and the failure mode is a duplicate purchase.** With no header, two identical
  `POST`s are two checkouts, two browsers and two orders. A `POST` that times out after we
  accepted it is indistinguishable, from your side, from one that never landed — and every HTTP
  client, job runner and agent framework retries a timeout by default. Set the header on every
  create you might retry, which is all of them.
</Warning>

| Situation                                 | Result                                                   |
| ----------------------------------------- | -------------------------------------------------------- |
| New key                                   | `201` and a new checkout, exactly as without the header. |
| Same key, same body, inside 24 hours      | `200` and the original checkout.                         |
| Same key, different body, inside 24 hours | `409`, and nothing is created.                           |
| Same key, two concurrent requests         | Both get the same checkout — one `201`, one `200`.       |
| Same key, more than 24 hours later        | `201` and a **new** checkout. The key was released.      |
| No header, same body twice                | Two checkouts. Two purchases.                            |

### What "same body" means

The body is hashed after parsing, and object keys are sorted first, so two clients serialising
the same request are under no obligation to agree on key order. Defaults are applied before
hashing too: omitting `constraints` and sending `"constraints": {}` are the same request.

Reusing a key with a genuinely different body is a caller bug, so it is refused rather than
quietly answered with a checkout for something else. Anything other than a fingerprint match is a
`409` — including the pathological case of a checkout that holds a key but no fingerprint at all,
which replays nothing rather than replaying for any body you send:

```json 409 theme={null}
{
  "error": true,
  "message": "This Idempotency-Key was already used with a different request body.",
  "checkoutId": "1f2e3d4c-5b6a-4790-8123-abcdef012345"
}
```

`checkoutId` points at what the key already refers to, so you can inspect it and decide whether
your retry was the mistake or the key was.

### Scope and lifetime

* Keys are scoped per **(tenant, `subject`)**, not globally. Two of your
  [end users](/end-user-isolation) may pick the same string — `"order-1"`, a timestamp, a retry
  counter — and neither can replay the other's checkout. A tenant-wide key shares one namespace
  across everything it creates untagged.
* **A key lives 24 hours**, measured from the creation of the checkout that claimed it. Inside the
  window a retry is a retry. Outside it the key is released and the same string starts a new
  purchase, which is the same 24 hours Stripe gives you and the same assumption most HTTP clients
  already make.
* Release is a background sweep, not a clock read on the request, and it runs hourly — so a key can
  outlive its window by up to an hour before it stops replaying. Do not build a flow that depends
  on the boundary landing at a particular second.
* Expiry frees the key, it does not touch the checkout. The row, its timeline and its receipt stay
  exactly where they were; only the key and the body fingerprint are cleared off it.
* The namespace belongs to the account and the end user, never to the credential. Rotate your
  API key and a replay of an hour-old key still works; two keys bound to *different* subjects
  never collide.
* An empty or whitespace-only header is treated as absent.

<Warning>
  **24 hours is a retry window, not a duplicate-purchase guard.** A key you reuse a week later
  buys the item again, with a `201` that looks like the first one. Make yours unique per *purchase* —
  your own order reference, or a UUID minted when the user confirmed — reuse it across the retries of
  that one purchase, and keep your own record of what you have already bought if your flow can come
  back to the same cart tomorrow.
</Warning>

### What a replay actually returns

The checkout **as it is now**, not a recording of the original response. A retry ninety seconds
later returns the same id with `status: "running"` and a timeline that has grown — possibly with
a `pendingUserAction` waiting on you. Treat the `200` as "this already exists, here is its
current state", and branch on `status` rather than assuming `queued`.

A replay also does not consume quota, does not launch a second browser, and does not re-enqueue
the run.

<Note>
  Idempotency covers **creation only**. It is not a retry of a failed run: while the key is still
  live, reusing it on a checkout that has already failed hands you back that same failed checkout
  with a `200`. To try again inside the window, create a new checkout with a new key.

  This is also why the window exists. Without it, a failed checkout replayed its own failure for as
  long as the row lived, and a caller that had sensibly derived its key from the purchase had no way
  to retry at all short of inventing a key it had no reason to think it needed.

  No other endpoint reads the header. Answering a user action is made safe by its own `409`
  instead — see [The 409s](#the-409s).
</Note>

## Quotas

Two independent per-tenant limits, both enforced at `POST /v1/checkouts`, both returning
`429`. Per **tenant**, not per end user: `subject` partitions who can see what, never who gets a
slot, so one end user's parked runs can `429` another's next create. See
[what isolation does not cover](/end-user-isolation#what-isolation-does-not-cover).

<ResponseField name="Active checkouts" type="set by your plan">
  ```json theme={null}
  { "error": true, "message": "Active checkout limit reached (5). Wait for runs to finish or cancel them." }
  ```

  The number in the message is your limit. Sandbox workspaces default to **5** concurrent;
  activation applies the plan's number — **Personal 1, Business 5, Scale 15**. See
  [Pricing](/pricing#what-the-tiers-change).

  "Active" means any checkout not in `succeeded`, `failed` or `cancelled` — including
  `queued` and including one parked on a user action.

  This is a resource cap as much as an abuse cap: every non-terminal checkout is holding a
  real browser. The practical consequence is that an unanswered action ties up a slot for its
  full 10-minute TTL. `DELETE` checkouts you have given up on rather than waiting them out.
</ResponseField>

<ResponseField name="Daily volume" type="200 per rolling 24h (default)">
  ```json theme={null}
  { "error": true, "message": "Daily checkout limit reached (200 per 24h)." }
  ```

  A rolling 24-hour window over creation time, not a calendar day. It is a velocity brake: an
  agentic checkout platform without one is a card-testing tool waiting to be discovered. The
  default is the same on every plan.
</ResponseField>

Both are raisable per tenant — ask, with a sense of your volume. Neither limit sends a
`Retry-After` header; for the concurrency limit, retry when one of your own runs finishes.

### Signup limits

`POST /v1/signup` is unauthenticated, so it carries its own limits: **3 per hour per source
IP** and **200 per 24 hours platform-wide**. See [Sandbox](/sandbox#signup-limits).

### What is not rate-limited

Reads. There is no limit on `GET /v1/checkouts/{id}`, so polling is free. Be sensible — a
3–5 second interval is right for a process that takes minutes — but you will not be throttled
for it.

## maxCost enforcement

`constraints.maxCost` is your spending ceiling for a single checkout:

```json theme={null}
{ "constraints": { "maxCost": { "amount": "60.00", "currency": "USD" } } }
```

`amount` is a decimal string. `currency` defaults to `"USD"` if you omit it. Both checks run
**before** a card is ever requested, so a breach costs you nothing.

<Steps>
  <Step title="Item price — major platforms only">
    When the variant resolves, its price is compared against `maxCost`. This is the item
    price alone: no shipping, no tax. It also ignores currency, because the storefront
    product JSON does not state one.

    Fails with `max_cost_exceeded` and the message
    `Item price 65.00 exceeds maxCost 60.00`.
  </Step>

  <Step title="Order total — both tiers">
    At the moment the engine wants to ask for a card, it reports the total displayed on the
    page — which does include shipping and tax. If that exceeds `maxCost`, the run fails
    instead of raising the card action.

    The comparison runs when both values parse and the currencies do not contradict each
    other. If the page's currency cannot be determined, the check still enforces against the
    number: a false stop is far cheaper than an uncapped purchase.

    Fails with `max_cost_exceeded` and the message
    `Order total 61.40 USD exceeds maxCost 60.00 USD`.
  </Step>
</Steps>

<Warning>
  **`maxCost` is a strong guardrail, not a hard ceiling.** The order-total check depends on
  reading a total off the merchant's page. If no total can be parsed there is nothing to
  compare, and the run proceeds to ask you for a card.

  Two things follow. First, the real ceiling should be the card: use single-use instruments
  with their own limit. Second, you are the last check — you can always inspect the checkout
  before answering a `card` action, and `decline` if the run's timeline does not look like what
  you asked for.
</Warning>

Omitting `maxCost` means no cost enforcement at all — unless your plan has a ceiling, in
which case the ceiling steps in. For anything touching a real merchant, set it.

### The plan ceiling

On plans with a per-checkout spend ceiling — [Personal, at \$200](/pricing#what-the-tiers-change)
— the ceiling is enforced at `POST /v1/checkouts`, before anything runs:

* **Omit `constraints.maxCost`** and the ceiling is applied *as* your `maxCost`
  (`{"amount": "200.00", "currency": "USD"}`), enforced by the same two checks above.
* **Ask for more than the ceiling** and the create is refused with a `400` carrying
  `planCeilingUsd`:

  ```json 400 theme={null}
  {
    "error": true,
    "message": "maxCost 250.00 exceeds this plan's per-checkout ceiling of $200.00.",
    "planCeilingUsd": 200
  }
  ```

Refused, **not clamped**: silently weakening an instruction about money is worse than
rejecting it. If the API quietly turned your `250.00` into `200.00`, a purchase could
succeed under a constraint you never agreed to; a `400` at create time costs nothing and
leaves the decision with you.

The ceiling is a dollar figure, and the refusal compares USD amounts — a `maxCost` in
another currency passes this gate and is enforced only by the ordinary `maxCost` checks
above. Business and Scale have no ceiling; `maxCost` on those plans is entirely yours.

## Other input limits

| Field                                           | Limit                                                                        |
| ----------------------------------------------- | ---------------------------------------------------------------------------- |
| `target.url`                                    | Must be a valid absolute URL.                                                |
| `target.merchantContext`                        | 4000 characters.                                                             |
| `target.allowedDomains`                         | Max 10 entries, hostnames only — no scheme, no path.                         |
| `target.countryCode`                            | Two letters, ISO 3166-1 alpha-2. Inferred from the URL's ccTLD when omitted. |
| `subject` (checkouts, buyer profiles, API keys) | 1 to 200 characters after trimming. Empty or whitespace-only is a `400`.     |
| Unknown top-level fields on a create body       | Refused. `target`, `constraints` and `metadata` stay permissive inside.      |
| `label` (API keys, webhooks)                    | 200 characters.                                                              |
| `name` (signup)                                 | 120 characters. `email`, 200.                                                |
| `limit` (list endpoints)                        | Defaults to 20. Must be an integer from 1 to 100; anything else is a `400`.  |
| Webhook `url`                                   | Must be `https://`, except `http://localhost` and `http://127.0.0.1`.        |

## Known gaps

Things a reasonable integrator expects that do not exist yet. Listed so you design around
them rather than discover them.

<AccordionGroup>
  <Accordion title="No shipped-to address on a past checkout">
    A checkout stores `buyerProfileId`, never a copy of the address it used. `PATCH` or
    `DELETE` the profile and there is no longer any way to recover where a past order actually
    went. Record it yourself at creation time if refunds or disputes depend on it.
  </Accordion>

  <Accordion title="No way to list an action's history">
    Only the currently pending action is exposed, on the checkout object. Once resolved it
    disappears from the API. The timeline retains the fact that it happened — kind
    `user_action`, with `data.key` — but not what was submitted.
  </Accordion>

  <Accordion title="Webhooks have no event id and no ordering guarantee">
    Dedupe on `(checkoutId, status)` or, better, on the state you fetch. See
    [Webhooks](/webhooks#caveats-you-must-design-around).
  </Accordion>

  <Accordion title="No retry, replay or clone of a checkout">
    A failed checkout is final. Create a new one with the same body — and a new
    [`Idempotency-Key`](#idempotency), since reusing the old one hands you back the failure for as
    long as that key is still live. `Idempotency-Key` deduplicates the *call*, not the run.
  </Accordion>

  <Accordion title="No search across checkouts">
    List supports `subject`, `status`, `limit` and `cursor`. You cannot filter by `metadata`, by
    merchant, or by date range — keep your own index keyed on the checkout id you got at
    creation.
  </Accordion>
</AccordionGroup>
