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

# End-user isolation

> Bind an API key to one of your end users with `subject`, so the worst a confused or manipulated agent can reach is one person's data.

A `subject` is **your** id for one of your end users. You already have one — a row id, a UUID,
a user id from your auth provider — so we never generate a second. Rows carry it, keys can be
bound to it, and a bound key can only see and create rows for that one person.

```bash theme={null}
# A key that acts for exactly one of your users, and can never act for another.
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","buyer-profiles.write"],"subject":"user_a91f"}'
```

<Note>
  **The rule, in one line.** A key with a `subject` sees only that subject's rows. A key without
  one sees the whole tenant, exactly as it always has.
</Note>

## Why this exists

[Scopes](/authentication#scopes) narrow *what* a key may do. `subject` narrows *whose data* it
may do it to. You need the second axis for the same reason you need the first, only more so.

The holder of a Farthing key is frequently an autonomous agent, and that agent spends its
run reading merchant pages — untrusted text, written by someone else, quoted back to the model
in option names, form labels and progress messages. "List the buyer profiles" is a completely
plausible call for a confused or prompt-injected agent to make. It is also a call that, on a
tenant-wide key, returns every name, email and shipping address you have ever stored.

The blast radius of that call should be one person. That is the whole feature.

Concretely, on a tenant serving thousands of end users through one agent, a leaked or
manipulated tenant-wide key exposes all of them at once. A bound key exposes the one buyer it
was minted for, and cannot mint itself anything wider.

### Nothing changes until you opt in

Every key issued before this existed carries no subject, which means tenant-wide, which is
exactly what those keys have always been. There is no migration, no behaviour change, and no
new failure mode on an integration that ignores this page. Isolation is something you turn on
per key, at creation.

## Bind a key to an end user

`subject` on `POST /v1/api-keys` is the only place a binding is created. It is fixed for the
life of the key, like `mode` and `scopes`. The call at the top of this page answers with:

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

`subject` is present on the creation response only when the key is bound. On
`GET /v1/api-keys` it is always present and `null` for a tenant-wide key — which is how you
audit, from your admin key, which of your outstanding keys act for whom.

<ParamField body="subject" type="string">
  1 to 200 characters, trimmed. Opaque to us: never parsed, never validated against anything,
  never sent to a merchant, and never typed into a checkout form. There is no registry of
  subjects and no endpoint that lists them — a subject exists because a row or a key mentions it.

  Prefer your own internal id over an email address or a name. It is stored with us and echoed
  back on reads, and an opaque id is the version of that data you would rather have in both
  places.
</ParamField>

<Warning>
  **`subject: ""` is refused.** An empty or whitespace-only string is a `400` on every body that
  takes the field, not a subject that quietly means nothing.

  `subject: user.id ?? ""` is the shape this protects you from. An empty string is falsy, so it
  used to mint a key that looked bound and acted tenant-wide, and tag rows that no bound key could
  ever see — an opt-in that silently no-ops is worse than no opt-in. It now fails at the call site,
  where you can still fix it.
</Warning>

```json 400 theme={null}
{
  "error": true,
  "message": "Invalid request",
  "issues": [
    {
      "code": "too_small",
      "minimum": 1,
      "type": "string",
      "inclusive": true,
      "exact": false,
      "message": "String must contain at least 1 character(s)",
      "path": ["subject"]
    }
  ]
}
```

`message` is `Invalid profile` on `POST /v1/buyer-profiles` and `Invalid request` on the other
two; the `issues` are the same. Surrounding whitespace is stripped before the value is stored, so
`" user_a91f "` and `"user_a91f"` are the same subject rather than two.

## What a bound key can reach

Every lookup a bound key makes is filtered on `(tenant, subject)`, and the endpoints that answer
account-level questions refuse it outright.

| Call                                           | On a bound key                                                                                                                                    |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /v1/checkouts`                           | Allowed. The checkout is stamped with the key's subject.                                                                                          |
| `GET /v1/checkouts`                            | Only that subject's checkouts. Another end user's runs are not in the page — not redacted, absent. `?subject=` naming anyone else is a **`403`**. |
| `GET`/`DELETE /v1/checkouts/{id}`              | `404 Checkout not found` for another subject's checkout.                                                                                          |
| `POST /v1/checkouts/{id}/actions/{actionId}`   | `404 Checkout not found` — you cannot answer a question raised on someone else's run.                                                             |
| `POST /v1/checkouts/{id}/embed-token`          | `404 Checkout not found` — you cannot watch someone else's browser.                                                                               |
| `POST /v1/buyer-profiles`                      | Allowed. Stamped with the key's subject.                                                                                                          |
| `GET /v1/buyer-profiles`                       | Only that subject's profiles.                                                                                                                     |
| `GET`/`PATCH`/`DELETE /v1/buyer-profiles/{id}` | `404 Profile not found` for another subject's profile.                                                                                            |
| `GET /v1/buyer-sessions`                       | Only sessions belonging to that subject's profiles.                                                                                               |
| `DELETE /v1/buyer-sessions/{id}`               | `404 Session not found` for another subject's session.                                                                                            |
| `POST /v1/api-keys`                            | Allowed, and the new key inherits the binding.                                                                                                    |
| `GET /v1/api-keys`                             | **`403`** — the key list is a tenant-wide fact.                                                                                                   |
| `DELETE /v1/api-keys/{id}`                     | **`403`** — revocation is a tenant-wide power.                                                                                                    |
| `GET /v1/usage`                                | **`403`** — billing is between us and you.                                                                                                        |
| `POST`/`GET`/`DELETE /v1/webhooks`             | **`403`** — endpoints receive every end user's transitions.                                                                                       |

The four account-level `403`s — the key list, revocation, usage, webhooks — are all the same
judgement: those calls answer questions about the *account*, not about a buyer. An end user's key
enumerating its siblings, revoking the account's own key,
reading the account's total spend, or subscribing to everyone's checkout activity would each be
a leak between your own customers.

```json 403 — a bound key reading account usage theme={null}
{ "error": true, "message": "A subject-bound key cannot read account usage." }
```

### Authority passes down, never sideways

A bound key can mint keys, which is useful — a per-session credential for a single buyer,
narrower in scope than the one that issued it. What it cannot do is widen:

* Omitting `subject` **inherits the binding**. It does not mean tenant-wide. There is no body
  that mints a tenant-wide key from a bound one.
* Naming a different `subject` is a `403`.

```json 403 theme={null}
{ "error": true, "message": "This API key is bound to subject 'user_a91f' and cannot issue keys for another." }
```

This is the same rule that already governs [scopes](/authentication#scopes) and
`mode`: a key can only ever hand out authority it already holds. Without it, one `POST` turns a
single buyer's credential into the account's.

## Which buyer profile a checkout may use

`buyerProfileId` on `POST /v1/checkouts` is validated against **the checkout's** subject, not the
authenticating key's. That distinction is the whole point: a tenant-wide admin key passes any
key-level fence trivially, so a check written against the key would have let one `POST` create a
checkout stamped `subject: "user_bob"` while spending Alice's stored address — handing her address
to a merchant, her order to a stranger, and writing any merchant login that run captured back onto
her profile.

| The profile is tagged | The checkout is tagged       | Result                         |
| --------------------- | ---------------------------- | ------------------------------ |
| another end user      | anything, on a **bound** key | `400 buyerProfileId not found` |
| `user_alice`          | `user_bob`                   | `403`, naming both subjects    |
| not at all (shared)   | `user_bob`                   | Allowed                        |
| `user_alice`          | not at all                   | Allowed                        |

```json 400 — a bound key reaching for another end user's profile theme={null}
{ "error": true, "message": "buyerProfileId not found" }
```

```json 403 — a tenant-wide key mixing two end users theme={null}
{
  "error": true,
  "message": "buyerProfileId belongs to subject 'user_alice', but this checkout is for 'user_bob'."
}
```

The first is a `400` rather than a `403` because from a bound key's position the profile genuinely
does not exist — the ownership check and the existence check are the same query, and a `403` would
confirm that some other end user has a profile by that id.

<Note>
  **An untagged profile with a tagged checkout stays legal**, and so does the reverse. Only a
  genuine mismatch between two different end users is refused.

  That is deliberate: adoption is incremental. Profiles you created before you started tagging have
  no subject, a shared operations address is a reasonable thing to keep untagged, and refusing those
  combinations would mean no tenant could turn `subject` on without a backfill it has no endpoint
  for.
</Note>

## Tagging rows from a tenant-wide key

`subject` is also accepted on the create bodies, which is how rows get tagged in the first place
if you would rather keep one admin key than mint one per user:

<CodeGroup>
  ```bash Checkout theme={null}
  curl -sX POST https://api.farthing.ai/v1/checkouts \
    -H "authorization: Bearer $FARTHING_ADMIN_KEY" \
    -H 'content-type: application/json' \
    -d '{
      "target": { "url": "https://your-sandbox-store.myshopify.com/products/widget", "request": "buy this in medium" },
      "buyerProfileId": "b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30",
      "subject": "user_a91f"
    }'
  ```

  ```bash Buyer profile theme={null}
  curl -sX POST https://api.farthing.ai/v1/buyer-profiles \
    -H "authorization: Bearer $FARTHING_ADMIN_KEY" \
    -H 'content-type: application/json' \
    -d '{
      "label": "home",
      "subject": "user_a91f",
      "contact": { "email": "ada@example.com" }
    }'
  ```
</CodeGroup>

On a bound key the field is redundant but permitted: sending the key's own subject is fine,
sending any other value is a `403`.

```json 403 theme={null}
{ "error": true, "message": "This API key is bound to subject 'user_a91f' and cannot act for another." }
```

<Tip>
  Tagging from an admin key gets you the audit trail — you can answer "what has this user bought"
  — without getting you the containment. The agent still holds a key that can read everyone. If
  the point is to survive a prompt injection, the agent needs a bound key.
</Tip>

### A typo cannot produce an untagged row

All three bodies that accept `subject` — `POST /v1/api-keys`, `POST /v1/checkouts` and
`POST /v1/buyer-profiles` — reject unknown top-level fields. A misspelled `subjectt` is a `400`:

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

This is the difference between a typo that fails loudly and a `201` carrying an untagged row that
every key on the account can read. A permissive schema drops what it does not recognise, and the
field most worth misspelling here is the one that decides who can see the row.

Nested objects stay permissive on purpose — `target`, `constraints` and `metadata` are extension
points, and unknown keys inside them are ignored rather than refused. The strictness is at the top
level, where `subject` lives.

### How `subject` comes back

Three shapes, and they differ. Worth reading once if you are writing a client:

| Read                                             | Convention                                        |
| ------------------------------------------------ | ------------------------------------------------- |
| A checkout, from create / get / an action submit | Present when set, **field absent** when not.      |
| A checkout **list** row                          | Same: present when set, absent when not.          |
| A buyer profile, from any endpoint               | **Always present**, `null` when untagged.         |
| An API key, from `POST /v1/api-keys`             | Present only when the new key is bound.           |
| An API key, from `GET /v1/api-keys`              | **Always present**, `null` for a tenant-wide key. |

Read it with a null-and-absent-tolerant accessor rather than a presence check, and none of this
matters.

## Listing one end user's runs

`GET /v1/checkouts?subject=user_a91f` narrows the list to one end user, so a tenant that tags from
an admin key can answer "what is this person's agent doing" without fetching every checkout:

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

The filter composes with `status`, `limit` and `cursor`, and takes the same shape as every other
subject — 1 to 200 characters, or a `400`. A well-formed subject that no checkout carries is an
empty page, not an error. Rows carry `subject` when the checkout has one, so you can also read it
off an unfiltered page.

A bound key does not need the parameter — its page is already narrowed to one person — and asking
for somebody else's is refused rather than ignored:

```json 403 theme={null}
{ "error": true, "message": "This API key is bound to subject 'user_a91f' and cannot act for another." }
```

Passing its **own** subject is fine, which means the same client code can run against either kind
of key. There is still no way to list the subjects you have used: that list is your user table.

## A profile cannot be reassigned

`PATCH /v1/buyer-profiles/{id}` does not accept `subject`, and does not ignore it either:

```json 400 theme={null}
{
  "error": true,
  "message": "Invalid profile",
  "issues": [
    {
      "code": "unrecognized_keys",
      "keys": ["subject"],
      "path": [],
      "message": "Unrecognized key(s) in object: 'subject'"
    }
  ]
}
```

The schema is strict on purpose. A caller trying to move a stored identity from one person to
another should be told it did not happen, rather than getting a `200` and a profile that is
still where it was. There is no move endpoint: create a profile under the right subject and
delete the wrong one.

## What isolation does not cover

Honest boundaries. These are tenant-level facts, and a `subject` does not partition them.

<AccordionGroup>
  <Accordion title="Quotas are per tenant, not per end user">
    The default 5 concurrent checkouts and 200 per rolling 24 hours are counted across the whole
    account. One end user's five parked runs will `429` everybody else's next create. If you are
    fanning a real user base through this, the concurrency cap is the number to talk to us about
    before the subject model is. See [Quotas](/errors-and-limits#quotas).
  </Accordion>

  <Accordion title="Webhook endpoints are tenant-wide">
    One endpoint receives the status transitions of every checkout on the account, for every end
    user. There is no per-subject endpoint, no per-subject delivery filter, and a bound key cannot
    register or read an endpoint at all (`403`).

    The payload does name the end user — `subject` is on it whenever the checkout has one — so
    routing a delivery to the right person needs neither a callback nor a map of your own. See
    [the event](/webhooks#the-event).
  </Accordion>

  <Accordion title="Usage and billing are the account's">
    `GET /v1/usage` reports completed checkouts and gross merchandise volume for the tenant.
    There is no per-subject breakdown, and a bound key cannot read it at all. Attribute spend
    yourself from the `subject` on each checkout — `?subject=` on the checkout list is the cheapest
    way to gather them.
  </Accordion>

  <Accordion title="Key mode is a separate axis">
    A bound key is still a `ck_test_` or `ck_live_` key, and mode is enforced independently — a
    bound test key is confined to sandbox merchants exactly like any other test key. Binding a
    key does not make it safe to spend, and making it live does not widen whose data it sees.
  </Accordion>

  <Accordion title="There is no subject registry">
    Subjects are not objects. Nothing creates, lists, renames or deletes one; a subject exists
    because a key or a row mentions the string. You cannot ask the API which subjects you have
    used, so keep that list on your side — it is your user table.
  </Accordion>

  <Accordion title="Isolation is enforced at the API surface">
    The rule is applied to authenticated requests. Our own orchestrator drives runs as the
    system rather than on behalf of a caller, because it has to be able to complete any checkout
    it is handed — so `subject` is a fence around your keys, not an encryption boundary. If your
    threat model needs the latter, talk to us rather than inferring it from this page.
  </Accordion>
</AccordionGroup>

## Adopting it on an existing account

<Steps>
  <Step title="Use the id you already have">
    Your user table's primary key. Anything stable and opaque, up to 200 characters.
  </Step>

  <Step title="Create the buyer profile under that subject">
    Either with a bound key (which stamps it automatically) or with your admin key and an
    explicit `"subject"`. Do this **before** the first checkout for that user, because a profile
    cannot be moved afterwards.
  </Step>

  <Step title="Mint one bound key per end user">
    Keys are cheap and revocation is a timestamp, so a key per user — or per session — is a
    reasonable shape. Narrow the scopes at the same time: `["checkouts.write"]` plus the
    `buyerProfileId` in your own config is the tightest useful pair.
  </Step>

  <Step title="Keep one tenant-wide key for the account">
    Wherever you keep your other deployment secrets. It is the only credential that can list
    keys, read usage, manage webhooks, and see across your users — which is the point of it
    being the one you do not hand to an agent.
  </Step>
</Steps>

<Warning>
  **Rows created before you adopted this cannot be stamped.** Existing checkouts and buyer
  profiles have no subject, `PATCH` will not set one, and there is no backfill endpoint. A
  tenant-wide key still sees them all; a bound key sees only what was created for that subject
  after you started tagging. Plan for a cut-over date rather than a migration.
</Warning>

## Idempotency keys are namespaced by subject

An [`Idempotency-Key`](/errors-and-limits#idempotency) is unique per `(tenant, subject)`, not
per tenant. Two of your end users independently choosing `"order-1"` — a timestamp, a retry
counter, a cart id — is ordinary, and they have no way to coordinate because they cannot see
each other. Each gets their own checkout; neither can replay the other's.

The window is 24 hours, and it is per key rather than per subject: after that the same string is
free again for whoever sends it next.

## Errors

| Status | Message                                                                     | Cause                                                                                                                                                         |
| ------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `Invalid request` / `Invalid profile` with `issues`                         | `subject` empty, whitespace-only, or over 200 characters; an unknown top-level field such as `subjectt`; or `subject` sent to `PATCH /v1/buyer-profiles/{id}` |
| `400`  | `buyerProfileId not found`                                                  | A bound key attaching another end user's profile                                                                                                              |
| `403`  | `buyerProfileId belongs to subject '<a>', but this checkout is for '<b>'.`  | A tenant-wide key mixing one end user's profile into another's checkout                                                                                       |
| `403`  | `This API key is bound to subject '<s>' and cannot act for another.`        | `POST /v1/checkouts` or `POST /v1/buyer-profiles` naming a different subject, or `GET /v1/checkouts?subject=` naming one                                      |
| `403`  | `This API key is bound to subject '<s>' and cannot issue keys for another.` | `POST /v1/api-keys` naming a different subject                                                                                                                |
| `403`  | `A subject-bound key cannot list account keys.`                             | `GET /v1/api-keys`                                                                                                                                            |
| `403`  | `A subject-bound key cannot revoke account keys.`                           | `DELETE /v1/api-keys/{id}`                                                                                                                                    |
| `403`  | `A subject-bound key cannot read account usage.`                            | `GET /v1/usage`                                                                                                                                               |
| `403`  | `A subject-bound key cannot manage webhook endpoints.`                      | Any `/v1/webhooks` call                                                                                                                                       |
| `404`  | `Checkout not found` / `Profile not found` / `Session not found`            | Another subject's row. Identical to "does not exist", deliberately — you learn nothing about what other end users have.                                       |

## Next

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/authentication#binding-a-key-to-one-end-user">
    Scopes, key mode, rotation — the other two dimensions of what a key may do.
  </Card>

  <Card title="Buyer profiles" icon="address-book" href="/buyer-profiles">
    The identity a checkout ships to, and the row `subject` most matters on.
  </Card>

  <Card title="Buyer sessions" icon="cookie-bite" href="/buyer-sessions">
    Stored merchant logins. They inherit isolation through the profile they belong to.
  </Card>

  <Card title="Idempotency" icon="rotate" href="/errors-and-limits#idempotency">
    Safe retries on `POST /v1/checkouts`, keyed per end user.
  </Card>
</CardGroup>
