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

# Checkouts

> Create, list, get, cancel, and mint a live-view token.

## Create a checkout

```http theme={null}
POST /v1/checkouts
```

Success: `201`.

```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://example-store.com/products/widget",
      "request": "buy this in medium, cheapest delivery",
      "merchantContext": "The size selector is under the price, not in the sidebar.",
      "allowedDomains": ["auth.example-store.com"],
      "countryCode": "US"
    },
    "buyerProfileId": "b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30",
    "subject": "user_a91f",
    "constraints": { "maxCost": { "amount": "60.00", "currency": "USD" } },
    "metadata": { "orderRef": "acme-4412" }
  }'
```

### Headers

<ParamField header="Idempotency-Key" type="string">
  Makes this create safe to retry. A second `POST` with the same key returns the **original**
  checkout with `200` instead of starting a second run; the same key with a different body is a
  `409`.

  Strongly recommended on every create. Without it, a `POST` that times out after we accepted it
  is indistinguishable from one that never landed, and the retry buys the item twice. Empty or
  whitespace-only is treated as absent.

  A key lives **24 hours** from the creation of the checkout that claimed it. After that it is
  released and the same string starts a new purchase.

  See [Idempotent creates](#idempotent-creates) below and
  [Idempotency](/errors-and-limits#idempotency).
</ParamField>

### Body

The top level is **strict**: an unknown field is a `400`, not a field we drop for you. A misspelled
`subjectt` would otherwise have returned `201` with an untagged checkout the whole account can read.
`target`, `constraints` and `metadata` stay permissive inside — they are extension points.

```json 400 — a misspelled field theme={null}
{
  "error": true,
  "message": "Invalid request",
  "issues": [
    { "code": "unrecognized_keys", "keys": ["subjectt"], "path": [], "message": "Unrecognized key(s) in object: 'subjectt'" }
  ]
}
```

<ParamField body="target" type="object" required>
  <Expandable title="target">
    <ParamField body="url" type="string" required>
      The page to start from. Must be a valid absolute URL. Typically a product page; on
      tier 2 a category or home page also works if the `request` says what to find.
    </ParamField>

    <ParamField body="kind" type="string" default="direct_url">
      Only `"direct_url"` exists. Omit it.
    </ParamField>

    <ParamField body="request" type="string">
      Plain English. What to buy, which variant, which shipping. `"buy this in medium,
                  cheapest delivery"`. Omitted, the agent's goal is simply "complete the purchase" —
      which usually means it has to ask you which variant.

      On major platforms this is also how the variant prompt is avoided: option values are
      matched as whole words, and one unambiguous winner skips the question.
    </ParamField>

    <ParamField body="merchantContext" type="string">
      Up to 4000 characters of hints about this specific merchant, passed to the tier-2
      agent as guidance. Use it for a store whose checkout you already know is unusual.
    </ParamField>

    <ParamField body="allowedDomains" type="string[]">
      Extra hostnames the run may navigate to, beyond the target's own registrable domain
      and its subdomains. Max 10, hostnames only — no scheme, no path.

      This exists for merchants whose login lives on a separate identity origin. It is
      declared here, at creation time, before any untrusted page content exists — page text
      can never grow it at runtime.
    </ParamField>

    <ParamField body="countryCode" type="string">
      ISO 3166-1 alpha-2. Selects the network exit so the merchant sees a local visitor.
      Inferred from the URL's ccTLD when omitted.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="buyerProfileId" type="string">
  UUID of a [buyer profile](/api-reference/buyer-profiles) you own. Must exist and belong to your
  tenant, or you get `400 buyerProfileId not found`.

  It is checked against **this checkout's** `subject`, not the key's. A
  [subject-bound key](/end-user-isolation) reaching for another end user's profile gets that same
  `400` — from where it stands the profile does not exist. A tenant-wide key naming one end user's
  profile alongside another's `subject` gets a `403` that names both. An untagged profile on a
  tagged checkout is allowed, and so is the reverse.

  Without one the agent has no address or email, and most checkouts cannot be completed.
</ParamField>

<ParamField body="subject" type="string">
  Your own id for the end user this purchase is for. 1 to 200 characters after trimming, opaque to
  us, never sent to the merchant. Comes back on the checkout when set, and omitted when not.

  Empty or whitespace-only is a `400` — a subject that means nothing is worse than none, because it
  reads as isolation you do not have.

  On a [subject-bound key](/end-user-isolation) this is filled in for you: omit it and the
  checkout is stamped with the key's subject; name a different one and the request is a `403`.
  On a tenant-wide key it is how a row gets tagged in the first place, and how you can later filter
  the list with `?subject=`.

  It also namespaces the `Idempotency-Key` — two end users may use the same key string without
  colliding.
</ParamField>

<ParamField body="constraints" type="object" default="{}">
  <Expandable title="constraints">
    <ParamField body="maxCost" type="object">
      `{ "amount": "60.00", "currency": "USD" }`. `amount` is a decimal string; `currency`
      defaults to `"USD"`. See [maxCost enforcement](/errors-and-limits#maxcost-enforcement).
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="metadata" type="object" default="{}">
  Arbitrary JSON, stored and echoed back on every read. Never sent to the merchant, never
  interpreted by us, not searchable. Use it to carry your own order reference.
</ParamField>

### Response

`201` with the full checkout object, in status `queued`, already carrying the
`intent_started` progress item.

```json theme={null}
{
  "id": "1f2e3d4c-5b6a-4790-8123-abcdef012345",
  "status": "queued",
  "subject": "user_a91f",
  "target": {
    "kind": "direct_url",
    "url": "https://example-store.com/products/widget",
    "request": "buy this in medium, cheapest delivery",
    "countryCode": "US"
  },
  "constraints": { "maxCost": { "amount": "60.00", "currency": "USD" } },
  "buyerProfileId": "b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30",
  "metadata": { "orderRef": "acme-4412" },
  "progressItems": [
    { "kind": "intent_started", "label": "Started checkout", "data": {}, "at": "2026-07-29T09:14:31.203Z", "sequence": 1 }
  ],
  "createdAt": "2026-07-29T09:14:31.190Z",
  "updatedAt": "2026-07-29T09:14:31.190Z"
}
```

### Idempotent creates

With an `Idempotency-Key` header, this endpoint has a second success code:

| Response | When                                                                                                                                                        |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `201`    | First use of the key — or its first use since the 24-hour window lapsed. A new checkout, a new run.                                                         |
| `200`    | The key has been used in the last 24 hours with the same body. The **original** checkout, in whatever state it is in now — poll it, do not assume `queued`. |
| `409`    | The key has been used in the last 24 hours with a **different** body. Nothing is created.                                                                   |

The body is hashed with its keys sorted and its defaults applied, so key order does not matter
and `"constraints": {}` matches an omitted `constraints`. Keys live in a `(tenant, subject)`
namespace and expire 24 hours after the checkout that claimed them was created — released by an
hourly sweep, so the boundary is approximate to within an hour.

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

Two concurrent retries of the same request do not race into two purchases: the database picks a
winner and the loser returns the winner's checkout with `200`.

A replay is checked before the quota, so it does not consume one of your active-checkout slots,
and does not launch a second browser. It is checked *after* URL, sandbox, `subject` and
buyer-profile validation, so a replay whose target has since become unreachable — or whose buyer
profile has since been deleted — can still return `400` rather than the checkout you expected.

<Note>
  This is idempotency of the **call**, not of the run. Once a checkout is `failed`, its key returns
  that failure until the key expires. Retrying for real means a new checkout and a new key.
</Note>

### Errors

| Status | Message                                                                                                                                            |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `Invalid request` (with `issues`) — including an unknown top-level field and an empty `subject` — or `buyerProfileId not found`                    |
| `403`  | Sandbox confinement — the account is sandbox-only, or the request used a `ck_test_` key. Body carries `sandboxMerchants`. See [Sandbox](/sandbox). |
| `403`  | `This API key is bound to subject '<s>' and cannot act for another.` — a [bound key](/end-user-isolation) named a different `subject`.             |
| `403`  | `buyerProfileId belongs to subject '<a>', but this checkout is for '<b>'.` — a tenant-wide key mixed two end users.                                |
| `409`  | `This Idempotency-Key was already used with a different request body.` Carries `checkoutId`.                                                       |
| `429`  | `Active checkout limit reached (5). …`, `Daily checkout limit reached (200 per 24h).`                                                              |

```json 403 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"]
}
```

Confinement is checked against `target.allowedDomains` as well as `target.url` — the
offending host named in the message may be either.

***

## Get a checkout

```http theme={null}
GET /v1/checkouts/{id}
```

Success: `200`.

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

This is the object you poll, and it is the same shape returned by create and by submitting an
action.

<ResponseField name="id" type="string" required>UUID.</ResponseField>

<ResponseField name="status" type="string" required>
  `queued` · `running` · `awaiting_user_action` · `succeeded` · `failed` · `cancelled`.
</ResponseField>

<ResponseField name="subject" type="string">
  **Omitted** when the checkout is not tagged to an end user. Your own id when it is — set
  explicitly at creation, or stamped from the key's binding. See
  [End-user isolation](/end-user-isolation).
</ResponseField>

<ResponseField name="target" type="object" required>
  Exactly what you sent, plus `kind: "direct_url"`.
</ResponseField>

<ResponseField name="constraints" type="object" required>
  `{}` if you sent none.
</ResponseField>

<ResponseField name="buyerProfileId" type="string">
  **Omitted** when the checkout has no buyer profile.
</ResponseField>

<ResponseField name="metadata" type="object" required>
  `{}` if you sent none.
</ResponseField>

<ResponseField name="browser" type="object">
  **Omitted** until a browser session exists.

  ```json theme={null}
  { "embedTokenUrl": "/v1/checkouts/{id}/embed-token", "permissions": ["read"] }
  ```

  <Note>
    Its presence means "a browser exists and there is something to watch". `embedTokenUrl`
    points at the endpoint that mints a viewable URL; it is not viewable itself.
  </Note>
</ResponseField>

<ResponseField name="progressItems" type="array" required>
  Every event so far, ordered by `sequence` ascending. Each item is
  `{ kind, label, data, at, sequence }`. See
  [the timeline](/checkout-lifecycle#the-progressitems-timeline).
</ResponseField>

<ResponseField name="pendingUserAction" type="object">
  **Omitted** unless an action is pending right now.

  ```json theme={null}
  {
    "id": "7c1de0a2-9b44-4f0a-b3c1-2a5e8d7f4b60",
    "message": "Enter a single-use / agent card to complete this purchase.",
    "key": "card",
    "detectedTotal": { "amount": "61.40", "currency": "GBP" },
    "responseSchema": { "type": "object", "additionalProperties": false, "required": ["card_number", "card_expiry", "card_cvc"], "properties": { "card_number": { "type": "string", "title": "Card number", "minLength": 1, "description": "Single-use / agent card number for this purchase." }, "card_expiry": { "type": "string", "title": "Expiration date (MM / YY)", "minLength": 1 }, "card_cvc": { "type": "string", "title": "Security code", "minLength": 1 } } },
    "expiresAt": "2026-07-29T09:24:45.612Z"
  }
  ```

  Always absent on a terminal checkout — cancelling or failing a run expires any question
  still open, so `succeeded`, `failed` and `cancelled` never advertise a pending action.

  `detectedTotal` is what the merchant's own checkout says this order costs — **shipping and tax
  included** — read off the live page at the moment the run stopped to ask. Present on `card`
  actions. **Show it to your user before you answer**: after that the money has moved, and
  `receipt.total` only exists once the checkout has succeeded, which is too late to be a
  confirmation step. It is best-effort, so treat it as optional — omitted entirely when no total
  could be parsed, and `currency` can be absent when the page shows a bare number. A confident
  wrong price would be worse than no price, so nothing is guessed.

  See [User actions](/user-actions).
</ResponseField>

<ResponseField name="failure" type="object">
  **Omitted** unless the run failed. `{ "reason": "automation_failed" }` — one of
  `max_cost_exceeded`, `user_cancelled`, `user_action_expired`, `automation_failed`. The
  detail message is on the final `terminal` progress item, not here.
</ResponseField>

<ResponseField name="receipt" type="object">
  **Omitted** unless `status` is `succeeded`.

  ```json theme={null}
  {
    "total": { "amount": "42.10", "currency": "USD" },
    "merchantOrderId": "1042",
    "evidence": { "url": "https://example-store.com/checkouts/c/abc123/thank_you" }
  }
  ```

  `total` and `merchantOrderId` may be `null`. See
  [the receipt](/checkout-lifecycle#the-receipt).
</ResponseField>

<ResponseField name="createdAt" type="string" required>ISO 8601.</ResponseField>
<ResponseField name="updatedAt" type="string" required>ISO 8601.</ResponseField>

### Errors

`404 Checkout not found` — including when it belongs to another tenant, or to another end user
on a [subject-bound key](/end-user-isolation).

***

## List checkouts

```http theme={null}
GET /v1/checkouts
```

Success: `200`.

```bash theme={null}
curl -s "https://api.farthing.ai/v1/checkouts?status=awaiting_user_action&limit=20" \
  -H "authorization: Bearer $FARTHING_KEY"

# One end user's runs, from a tenant-wide key.
curl -s "https://api.farthing.ai/v1/checkouts?subject=user_a91f&limit=20" \
  -H "authorization: Bearer $FARTHING_KEY"
```

### Query

<ParamField query="limit" type="integer" default="20">
  An integer from 1 to 100. Anything else is a `400`.
</ParamField>

<ParamField query="cursor" type="string">
  The `nextCursor` from the previous page — an ISO timestamp. Returns checkouts created
  strictly before it. A value that is not a timestamp is a `400`.
</ParamField>

<ParamField query="status" type="string">
  Filter to one status. An unrecognised value returns `400 Unknown status 'x'`.
</ParamField>

<ParamField query="subject" type="string">
  Filter to one [end user](/end-user-isolation). Composes with `status`, `limit` and `cursor`; a
  subject with no checkouts is an empty page, not an error.

  On a [subject-bound key](/end-user-isolation) the page is already narrowed to its own end user, so
  the parameter is redundant. Passing its own subject is fine; naming a different one is a `403` —
  refused rather than quietly ignored.
</ParamField>

### Response

Deliberately a **summary**, not the full object. The full shape carries the entire event
timeline, which is exactly what a list view must not fetch.

```json theme={null}
{
  "data": [
    {
      "id": "1f2e3d4c-5b6a-4790-8123-abcdef012345",
      "status": "awaiting_user_action",
      "subject": "user_a91f",
      "url": "https://example-store.com/products/widget",
      "request": "buy this in medium, cheapest delivery",
      "pendingUserActionId": "7c1de0a2-9b44-4f0a-b3c1-2a5e8d7f4b60",
      "createdAt": "2026-07-29T09:14:31.190Z",
      "updatedAt": "2026-07-29T09:14:45.622Z"
    },
    {
      "id": "0c9d8e7f-6a5b-4c3d-9e2f-112233445566",
      "status": "succeeded",
      "subject": "user_a91f",
      "url": "https://example-store.com/products/gadget",
      "request": "buy one",
      "receipt": { "total": { "amount": "18.00", "currency": "USD" }, "merchantOrderId": "1041" },
      "createdAt": "2026-07-28T16:02:11.004Z",
      "updatedAt": "2026-07-28T16:09:44.881Z"
    },
    {
      "id": "aa11bb22-cc33-4d44-8e55-66f7788990aa",
      "status": "failed",
      "url": "https://example-store.com/products/thing",
      "request": null,
      "failure": { "reason": "user_action_expired" },
      "createdAt": "2026-07-28T11:20:00.117Z",
      "updatedAt": "2026-07-28T11:31:06.402Z"
    }
  ],
  "nextCursor": "2026-07-28T11:20:00.117Z"
}
```

<ResponseField name="subject" type="string">
  **Omitted** when the checkout is not tagged to an end user — the same convention as the full
  object. The third row above is an untagged checkout.
</ResponseField>

<ResponseField name="url" type="string | null">Lifted from `target.url`.</ResponseField>
<ResponseField name="request" type="string | null">Lifted from `target.request`.</ResponseField>
<ResponseField name="failure" type="object">Omitted unless failed.</ResponseField>

<ResponseField name="receipt" type="object">
  Omitted unless a receipt exists. Note this is the **short** form — `{ total,
      merchantOrderId }` with no `evidence`.
</ResponseField>

<ResponseField name="pendingUserActionId" type="string">
  Omitted unless an action is pending. The id only — fetch the checkout for the message and
  schema.
</ResponseField>

<ResponseField name="nextCursor" type="string | null">
  Pass back as `?cursor=`. `null` on the last page.
</ResponseField>

<Note>
  A [subject-bound key](/end-user-isolation) sees only its own end user's checkouts here — other
  people's runs are absent from the page rather than redacted out of it. A tenant-wide key sees
  everybody's, tells them apart by the `subject` on each row, and narrows to one with `?subject=`.
</Note>

***

## Cancel a checkout

```http theme={null}
DELETE /v1/checkouts/{id}
```

Success: `202`.

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

Returns `202` with an **empty body**.

Cancellation is asynchronous. It stops the run even while it is parked on a user action,
flips the row to `cancelled`, and reclaims the browser. Poll, or wait for the
`checkout.status_changed` webhook, to observe the terminal state.

A `DELETE` on an already-terminal checkout also returns `202` and does nothing. The `202`
acknowledges the request, it does not assert an outcome.

`404 Checkout not found` if it does not exist or is not yours.

<Warning>
  Not an undo. If the pay click has already landed, cancelling stops our automation but does
  not unwind an order the merchant has accepted.
</Warning>

***

## Mint a live-view token

```http theme={null}
POST /v1/checkouts/{id}/embed-token
```

Success: `200`.

```bash theme={null}
curl -sX POST https://api.farthing.ai/v1/checkouts/$FARTHING_ID/embed-token \
  -H "authorization: Bearer $FARTHING_KEY"
```

```json theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiJ9.eyJjaGVja291dElkIjoiMWYyZTNkNGMtNWI2YS00NzkwLTgxMjMtYWJjZGVmMDEyMzQ1IiwiZXhwIjoxNzg1MzE4Mjg1fQ.Qy8xVnZ0S2c",
  "url": "https://api.farthing.ai/embed/1f2e3d4c-5b6a-4790-8123-abcdef012345?token=eyJhbGciOiJIUzI1NiJ9...",
  "expiresAt": "2026-07-29T09:24:45.000Z"
}
```

<ResponseField name="token" type="string">
  HS256 JWT pinned to this checkout id, valid 10 minutes.
</ResponseField>

<ResponseField name="url" type="string">
  Ready to drop into an `<iframe src>`. Contains the token.
</ResponseField>

<ResponseField name="expiresAt" type="string">
  ISO 8601. No refresh — mint a new token when it lapses.
</ResponseField>

Body is ignored; no parameters. `404 Checkout not found` if it is not yours. See
[Live view](/live-view).
