What is the same
The mental model transfers wholesale, because it was copied on purpose: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.
What differs
Stated plainly, so you find these at build time rather than in production.Authentication and base URL
Authentication and base URL
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.Scope strings are named differently
Scope strings are named differently
agent-checkouts.buyer-profiles.*); ours are <resource>.read /
<resource>.write over the six route groups under /v1, plus *: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.Endpoint paths and naming
Endpoint paths and naming
/v1, and the noun is checkouts:POST /v1/checkoutsGET /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
intent to checkout throughout and most of your call sites resolve themselves.Buyer-profile PATCH replaces documents wholesale
Buyer-profile PATCH replaces documents wholesale
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.Action messages are contextual, not generic
Action messages are contextual, not generic
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.Variant prompts carry the store's own option names
Variant prompts carry the store's own 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 live view requires a minted token
The live view requires a minted token
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.Failure reasons are a closed set of four
Failure reasons are a closed set of four
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 sandbox tier exists, and key mode is enforced
A sandbox tier exists, and key mode is enforced
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
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
Bring your own merchant accounts
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
decline, expiry and max_cost_exceeded
handling before you point at a merchant that will actually charge someone.- Idempotent creates.
POST /v1/checkoutstakes anIdempotency-Keyheader; a replay returns the original checkout with200rather 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/usagereturns 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
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.Rename intent → checkout at your call sites
Fix your action-type branch
pendingUserAction.key — variant, shipping, card, credentials, otp,
and a fallback for anything the tier-2 agent invents.Re-scope your keys
scopes entirely for the
all-or-nothing behaviour you had before. Do the translation deliberately — omitting is
full access, not none.Fix your poll condition
pendingUserAction presence, not status === "awaiting_user_action".Re-point your failure handling at the four reasons
terminal progress item, not from failure.Run the whole flow in the sandbox
decline, expiry, and a deliberate max_cost_exceeded. Diff a real response
against your parser before you point at a real merchant.Add an Idempotency-Key to your create call
Then take the upgrades
subject-bound key.