Skip to main content
Farthing is a from-scratch implementation of the same product category as Crossmint Agent Checkouts, built from a full teardown of that product. The data model and the request/response shapes were mirrored deliberately, so that a client written against that quickstart is a port, not a rewrite. This page is written for someone with working Crossmint integration code in front of them.

What is the same

The mental model transfers wholesale, because it was copied on purpose: The status vocabulary is the same set of names:
The card action is the same three fields — card_number, card_expiry, card_cvc — and the cardholder name is deliberately still not among them. If your integration is structured as create → poll → inspect pendingUserAction → build values from responseSchema → submit → read receipt, that loop is unchanged. In most codebases the port is: swap the base URL, swap the auth header, adjust field names at the edges, and rename your intent variables.
Field-for-field parity is not something we can promise on your behalf. The contract was mirrored from a teardown, not from a spec we control, and their API has moved on since. Treat this page as “the same shape, verify the details” — and diff a real response from each against your parser before you cut over. The API reference is the authority for what we actually return.

What differs

Stated plainly, so you find these at build time rather than in production.
https://api.farthing.ai, and Authorization: Bearer ck_test_… / ck_live_….Keys are created and revoked through the API itself (/v1/api-keys), stored only as a hash, and shown once. See Authentication.
Both products scope keys; the strings are not interchangeable. Theirs are namespaced under the product (agent-checkouts.buyer-profiles.*); ours are <resource>.read / <resource>.write over the six route groups under /v1, plus *:
Two behaviours to check against whatever you are porting from: write implies read on the same resource, and a key issued with no scopes at all holds * — omission means full access here, not zero access. Narrowing is opt-in.A key also cannot mint a key with scopes it does not hold, so you cannot bootstrap a wildcard key from a narrow one. See Scopes.
Everything lives under /v1, and the noun is checkouts:
  • POST /v1/checkouts
  • GET /v1/checkouts (cursor-paginated summaries)
  • GET /v1/checkouts/{id}
  • DELETE /v1/checkouts/{id}
  • POST /v1/checkouts/{id}/actions/{actionId}
  • POST /v1/checkouts/{id}/embed-token
Rename intent to checkout throughout and most of your call sites resolve themselves.
We now match their buyer-profile CRUD — PATCH and DELETE both exist. The semantics are worth checking before you port an update path.name, contact and shipping are each stored as one JSON document. PATCH is partial at the top level only: omit a key and the document is untouched, supply one and it is replaced entirely. There is no deep merge, so {"shipping":{"postalCode":"…"}} does not change one field of an address — it throws the address away.DELETE is a real row delete: referencing checkouts survive with a null reference, stored merchant sessions for that buyer cascade away. See Buyer profiles.
Our pendingUserAction.message is written against the page the run is actually on (“This product comes in multiple options — pick one to continue.”, “The combination you picked (Size: M, Color: Blue) isn’t available — please choose again.”).If you were substituting your own copy because the upstream text was too generic to show a user, try rendering ours first.
The variant action’s schema property names are the merchant’s option names — Size, Color, Grind — with oneOf values restricted to combinations that exist and are in stock. Values are matched back by option name, not by position, so key order in your submitted object is irrelevant.
The checkout id alone gets you nothing. You mint a short-lived, single-checkout JWT via POST /v1/checkouts/{id}/embed-token and use the returned url.This is a deliberate divergence. The browser stream we tore down accepted any cross-origin connection that knew the intent id — an id that gets logged, pasted into tickets, and shared in URLs. Ours does not. See Live view.
max_cost_exceeded, user_cancelled, user_action_expired, automation_failed, and nothing else. The human-readable detail lives in the final terminal progress item’s data.message, not in failure. See Failure reasons.
A self-serve signup is confined to merchants we control until the account is activated, and — separately — any request made with a ck_test_ key is confined too, even on an activated account.If you are used to being live from the first call, expect a 403 the first time you point at a real merchant. The body carries sandboxMerchants and says which of the two reasons applied. See Sandbox.

What we add on top

The four things worth migrating for.

Webhooks

Their API is poll-only. We push checkout.status_changed to registered endpoints with an HMAC-SHA256 signature, durable retries, and per-endpoint delivery. Stop burning requests polling a run that is parked on a user action for ten minutes.

Token-gated live view

Watch the browser work, over a stream that authorises on a minted, expiring, single-checkout token rather than on an id anyone might know. Read-only by construction — no input path exists.

Bring your own merchant accounts

Merchants that refuse to sell to a logged-out visitor are a hard wall for guest-only automation. A credentials action logs in once; the resulting cookies are encrypted at rest with AES-256-GCM, scoped to that merchant’s domains, and reused on later runs. Listed and revocable via the API.

A real sandbox

Not a mock — a real browser placing a real order on a store we control, through the same engine and the same code paths. Prove your decline, expiry and max_cost_exceeded handling before you point at a merchant that will actually charge someone.
Four smaller ones worth knowing about:
  • Idempotent creates. POST /v1/checkouts takes an Idempotency-Key header; a replay returns the original checkout with 200 rather than buying the item again. Worth wiring in during the port, while you are already touching the create call — see Idempotency.
  • Per-end-user key isolation. A key can be bound to one of your end users with subject, after which it sees and creates only that person’s checkouts and buyer profiles and is refused on the account-wide endpoints. See End-user isolation.
  • Usage you can query. GET /v1/usage returns completed checkouts by day plus gross merchandise volume by currency. We meter completed checkouts, never attempts — charging for attempts would mean earning more when the agent fails, which is backwards for a product whose entire problem is reliability.
  • Marketing opt-ins refused in code. Not “the prompt says not to”. The click is refused outright unless we can positively observe that the box is already ticked and we would be unticking it. This came out of a real production purchase where a prompted-not-to agent subscribed a buyer to a mailing list anyway.

A porting checklist

1

Swap base URL and auth

https://api.farthing.ai, Authorization: Bearer ck_…. Sign up with POST /v1/signup for a sandbox key in one call.
2

Rename intent → checkout at your call sites

Paths, variable names, and any stored ids. Ours are UUIDs.
3

Fix your action-type branch

Branch on pendingUserAction.keyvariant, shipping, card, credentials, otp, and a fallback for anything the tier-2 agent invents.
4

Re-scope your keys

Translate your scope strings (ours are listed above), or omit scopes entirely for the all-or-nothing behaviour you had before. Do the translation deliberately — omitting is full access, not none.
5

Fix your poll condition

Branch on pendingUserAction presence, not status === "awaiting_user_action".
6

Re-point your failure handling at the four reasons

And read the detail message from the final terminal progress item, not from failure.
7

Run the whole flow in the sandbox

Including decline, expiry, and a deliberate max_cost_exceeded. Diff a real response against your parser before you point at a real merchant.
8

Add an Idempotency-Key to your create call

While you are in there. Whatever your old client did on a timeout, it now has a safe answer: the same key returns the same checkout instead of a second order.
9

Then take the upgrades

Register a webhook endpoint and delete your polling loop. Add the live view to your operator console. If one agent acts for many of your users, issue it a subject-bound key.