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

# Buyer profiles

> Create, get, list, update and delete reusable identity and shipping details.

Concepts and field guidance in [Buyer profiles](/buyer-profiles).

## Create

```http theme={null}
POST /v1/buyer-profiles
```

Success: `201`.

```bash theme={null}
curl -sX POST https://api.farthing.ai/v1/buyer-profiles \
  -H "authorization: Bearer $FARTHING_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "label": "ada-home",
    "name": { "first": "Ada", "last": "Lovelace" },
    "contact": { "email": "ada@example.com", "phone": "+15555550123" },
    "shipping": {
      "addressLines": ["1 Analytical Way"],
      "locality": "Louisville",
      "administrativeAreaCode": "KY",
      "postalCode": "40202",
      "countryCode": "US"
    }
  }'
```

### Body

Every field is optional — `{}` is a valid profile — but the agent can only fill what you gave
it. The top level is **strict**, exactly like `PATCH`: an unknown field is a `400` with an
`unrecognized_keys` issue rather than a field we quietly drop, because the field most worth
misspelling here is `subject`, and a `201` carrying an untagged profile is a fence that is not
there. The nested `name`, `contact` and `shipping` objects are not strict.

<ParamField body="label" type="string">
  Yours, for bookkeeping. Never sent to a merchant.
</ParamField>

<ParamField body="subject" type="string">
  Your own id for the end user this profile belongs to. 1 to 200 characters after trimming, opaque
  to us, never sent to a merchant. `""` is a `400`.

  On a [subject-bound key](/end-user-isolation) it is filled in for you — omit it and the profile
  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 you tag a profile so that a bound key can later reach it and no other
  end user's key can.

  **Create-only.** `PATCH` rejects it; a profile cannot be reassigned.
</ParamField>

<ParamField body="name" type="object">
  <Expandable title="name">
    <ParamField body="first" type="string" />

    <ParamField body="last" type="string" />
  </Expandable>

  Also used as the cardholder name at the payment step, which is why the card action never
  asks for one.
</ParamField>

<ParamField body="contact" type="object">
  <Expandable title="contact">
    <ParamField body="email" type="string">
      Where the merchant's order confirmation goes. Most checkouts refuse to proceed without
      it.
    </ParamField>

    <ParamField body="phone" type="string">
      Required by a fair number of merchants, particularly delivery.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="shipping" type="object">
  <Expandable title="shipping">
    <ParamField body="addressLines" type="string[]">
      Only the **first** line is filled on some platforms. Put an apartment number on line
      one if it matters.
    </ParamField>

    <ParamField body="locality" type="string">City / town.</ParamField>

    <ParamField body="administrativeAreaCode" type="string">
      State / province. Send the short code (`KY`, `ON`); the code is tried first, then the
      full name as a visible label. Effectively required for US and CA — Shopify offers no
      shipping method without it.
    </ParamField>

    <ParamField body="postalCode" type="string" />

    <ParamField body="countryCode" type="string">
      ISO 3166-1 alpha-2. Filled before the state, since changing country repopulates the
      province list.
    </ParamField>
  </Expandable>
</ParamField>

No validation beyond types. No normalisation, no address correction — values are stored and
typed exactly as sent.

### Response

```json theme={null}
{
  "id": "b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30",
  "label": "ada-home",
  "subject": null,
  "name": { "first": "Ada", "last": "Lovelace" },
  "contact": { "email": "ada@example.com", "phone": "+15555550123" },
  "shipping": {
    "addressLines": ["1 Analytical Way"],
    "locality": "Louisville",
    "administrativeAreaCode": "KY",
    "postalCode": "40202",
    "countryCode": "US"
  },
  "createdAt": "2026-07-29T09:12:04.881Z"
}
```

Omitted sub-objects come back as `null`, not as `{}`. `subject` is always present and `null` on
an untagged profile — unlike a checkout, which omits the field entirely.

`400 Invalid profile` with `issues` on a type mismatch, an unknown top-level field, or an empty
`subject`.

***

## Get one

```http theme={null}
GET /v1/buyer-profiles/{id}
```

Success: `200`.

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

Same object as create. `404 Profile not found` if it does not exist, is not yours, or belongs to
another end user on a [subject-bound key](/end-user-isolation).

***

## List

```http theme={null}
GET /v1/buyer-profiles
```

Success: `200`.

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

<ParamField query="limit" type="integer" default="20">
  Caps at 100. Send an integer.
</ParamField>

<ParamField query="cursor" type="string">
  The previous page's `nextCursor` — an ISO timestamp. Returns profiles created strictly
  before it.
</ParamField>

```json theme={null}
{
  "data": [
    {
      "id": "b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30",
      "label": "ada-home",
      "subject": null,
      "name": { "first": "Ada", "last": "Lovelace" },
      "contact": { "email": "ada@example.com", "phone": "+15555550123" },
      "shipping": { "addressLines": ["1 Analytical Way"], "locality": "Louisville", "administrativeAreaCode": "KY", "postalCode": "40202", "countryCode": "US" },
      "createdAt": "2026-07-29T09:12:04.881Z"
    }
  ],
  "nextCursor": null
}
```

Newest first. `nextCursor: null` means the last page.

On a [subject-bound key](/end-user-isolation) this page contains only that end user's profiles.
There is no `?subject=` filter here — unlike the checkout list, which has one — so a tenant-wide key
reads `subject` off each row.

***

## Update

```http theme={null}
PATCH /v1/buyer-profiles/{id}
```

Success: `200`, returning the full updated profile.

```bash theme={null}
curl -sX PATCH https://api.farthing.ai/v1/buyer-profiles/$PROFILE_ID \
  -H "authorization: Bearer $FARTHING_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "shipping": {
      "addressLines": ["221B Baker Street"],
      "locality": "London",
      "postalCode": "NW1 6XE",
      "countryCode": "GB"
    }
  }'
```

### Body

Same fields as create, all optional — but partial **only at the top level**.

<Warning>
  `name`, `contact` and `shipping` are each one JSON document. Omit a key and that document is
  untouched; **supply a key and its document is replaced wholesale**. There is no deep merge.

  `{"shipping":{"postalCode":"NW1 6XE"}}` does not change the postcode on the stored address — it
  throws the address away and stores one that is only a postcode. Send the whole object.
</Warning>

<ParamField body="label" type="string | null">
  Replaces the stored label. `null` clears it.
</ParamField>

<ParamField body="name" type="object | null">
  Replaces the whole `name` document. `null` clears it; `{}` stores an empty object, which is a
  different thing.
</ParamField>

<ParamField body="contact" type="object | null">
  Replaces the whole `contact` document. `null` clears it.
</ParamField>

<ParamField body="shipping" type="object | null">
  Replaces the whole `shipping` document. `null` clears it — this is how you remove an address you
  no longer want us holding, short of deleting the profile.
</ParamField>

`subject` is **not** accepted here. Both schemas are strict, so this is a `400` rather than a
silently dropped field.

### Errors

| Status | Message                                                                        |
| ------ | ------------------------------------------------------------------------------ |
| `400`  | `No fields to update` — the body parsed but contained none of the four keys    |
| `400`  | `Invalid profile` (with `issues`) — type mismatch, or any key outside the four |
| `404`  | `Profile not found`                                                            |

An unexpected key is a `400` rather than a silently dropped field. That is most likely to bite you
with `subject`:

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

A profile cannot be moved to another end user after creation. See
[End-user isolation](/end-user-isolation#a-profile-cannot-be-reassigned).

***

## Delete

```http theme={null}
DELETE /v1/buyer-profiles/{id}
```

Success: `200`.

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

```json theme={null}
{ "deleted": true, "id": "b3a1c0de-2f77-45a0-9c8e-6f4b2a1d9e30" }
```

A row delete, not a soft delete. This is the endpoint to call when a buyer asks you to remove
what you hold about them.

The blast radius is fixed by foreign keys, so it is the same every time:

| Related data                    | What happens                               | Why                                                                              |
| ------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- |
| Checkouts that used the profile | Survive, with `buyerProfileId` set to null | Purchase history should outlive the identity                                     |
| Buyer sessions for the profile  | Deleted (cascade)                          | Keeping a live merchant login for an erased buyer is the opposite of the request |

`404 Profile not found` covers both "no such id" and "another tenant's". A repeat delete is a
`404` — treat it as success when retrying.

<Note>
  Deleting a profile does not stop a checkout already running against it. The run loaded the
  address it needed before this call; the reference simply nulls out. Cancel the checkout as well
  if nothing more should ship.
</Note>
