Skip to main content
Farthing is built to be called by an autonomous agent. This page is for the developer wiring one up: the loop, the tool definitions, and — the part that actually decides whether your integration is safe — which questions the model may answer and which must go to a person.

The loop

create → poll → answer actions → receipt.
1

Create

POST /v1/checkouts with a URL and a sentence. Returns in milliseconds with a queued object and an id. Nothing has happened yet.
2

Poll

GET /v1/checkouts/{id} every 3–5 seconds. Reads are not rate limited. The object carries the status, an append-only progressItems timeline, and a pendingUserAction when the run is blocked.
3

Answer

POST /v1/checkouts/{id}/actions/{actionId} with submit and the values the action’s responseSchema describes — or decline to end the run. Then go back to polling.
4

Receipt

A succeeded checkout carries receipt.merchantOrderId and receipt.total. failed and cancelled carry failure.reason. Those three statuses are terminal.
This is not request/response, and building it as if it were is the single most common way to get this wrong. A run takes two to three minutes on well-trodden platforms and longer on the general DOM tier, and it will typically stop twice — once for a variant, once for a card — each time waiting up to ten minutes for an answer. Budget for a conversation that spans minutes and survives your process restarting, not for a tool call that returns a purchase.
If your agent runs inside a request handler with a 30-second timeout, do not run the poll loop there. Create the checkout, persist the id, and drive the rest from a job, a cron, or a webhook that wakes you on each status change.

Send an Idempotency-Key. Always.

This is the first thing to get right, ahead of the tool definitions, because the worst thing this product can do is buy the same thing twice — and your framework will retry a transport error by default. Every agent runtime worth using retries a timed-out tool call. A POST that times out after we accepted it looks identical, from your side, to one that never landed.
Send it and a duplicate POST returns the original checkout with 200 instead of starting a second run: same id, same browser, one order. Omit it and you have a race between your retry policy and your user’s card.
The key must not come from the model. Generate it in your tool handler from something stable about the intent — your cart id, your order row’s primary key, a UUID you mint when the user says yes — and reuse it across retries of that intent. A model asked for an idempotency key will invent a fresh one on each attempt, which is exactly the same as not sending one. Do not put it in the tool schema.
Three more properties worth knowing before you rely on it:
  • A replay returns the checkout as it is now, not the original queued response. A retry ninety seconds later comes back running, possibly with a pendingUserAction already waiting. Branch on status; do not assume queued.
  • The same key with a different body is a 409, and the error carries the checkoutId it already refers to. That is a bug in your handler — usually a key derived from the user’s turn rather than from the purchase — surfaced rather than silently resolved in favour of whichever body arrived first.
  • A key lives 24 hours, measured from the creation of the checkout that claimed it. That covers every retry your framework will ever make in one turn, and then some. It is a retry window and not a duplicate-purchase guard: the same key next week buys the item again, so if your flow can come back to the same cart tomorrow, keep your own record of what you have already bought.
It does not make a failed run retryable: while the key is still live, reusing it hands you back the failure. A real retry is a new checkout with a new key. Full behaviour, including how the body is hashed and how keys are namespaced per end user, in Idempotency.

Give the agent a narrow key

Scopes exist for exactly this situation. The holder of a key is an autonomous process, and the useful question is not “do I trust this tenant” but “what is the worst this agent can do if its prompt is turned against it”.
That key can create, poll, cancel and answer checkouts (write implies read on the same resource), and list buyer profiles so the model can choose one. It cannot read your usage, it cannot touch your webhooks, and it cannot mint itself a better key — a key can never grant scopes it does not itself hold. See Authentication.
Drop buyer-profiles.read and hand the agent a buyerProfileId in its system prompt instead. Attaching a profile to a checkout does not require reading it, so an agent scoped to ["checkouts.write"] alone can still ship to a stored address it can never enumerate.

If your agent acts for more than one person

Add "subject": "<your user id>" to the key you mint, and that key can only ever see and create rows for that one end user:
Scopes decide what the agent may do; subject decides whose data it may do it to. That second axis matters here specifically because your agent reads merchant pages, and list_buyer_profiles is a completely plausible call for a model that has been talked into something. On a tenant-wide key that call returns every buyer you have ever stored. On a bound key it returns one person, and there is nothing the model can say to widen it. See End-user isolation.

Tool definitions

Four tools cover the whole product. They are written against the real request shapes — cross-check against the API reference or openapi.json if you change them.
Two deliberate choices in there, both worth keeping if you rewrite these:
  • maxCost is flattened into two string parameters. Nested objects are where models reliably deviate from a schema, and this one is a spending limit. Flatten it and rebuild the { amount, currency } object in your handler.
  • create_checkout does not expose allowedDomains or merchantContext. Both widen what the run may do — allowedDomains literally widens the navigation guard. They are your decision, from your config, not the model’s from its context.
There is no product search. create_checkout needs a URL the model already has, from your own catalogue, a search tool you provide, or the user. A model asked to buy “a black hoodie” with only these four tools will invent a plausible-looking URL. Say so in your system prompt.

Which questions may the agent answer?

When a run blocks, pendingUserAction.key says what is being asked for. This is set by the engine raising the question, not inferred from the merchant’s markup — so branch on it, never on the shape of responseSchema.

The three the agent must never answer

card, credentials and otp are values that come from a human, in that moment, for this purchase. An agent must not supply them — not from its system prompt, not from earlier turns in the conversation, not from a file it read, not from a “test card” it remembers. Two reasons, and the second is the one people miss:
  1. A card number in an agent’s context is a card number in an agent’s context. It ends up in transcripts, logs, evaluation datasets and any tool the agent later calls.
  2. These are exactly the questions worth attacking. Merchant pages are untrusted input, and an agent that will hand over a card because a page persuaded it to is a much more attractive target than one that structurally cannot.
Enforce it in your tool handler, not in the JSON Schema. Card fields are predictable, so a schema could in principle exclude them — but credentials field names are generated from whatever the merchant’s login form happened to call them, and otp fields are equally arbitrary. The only reliable discriminator is the action’s key, which your handler can read and the model cannot forge:
Re-fetching the checkout inside the handler is the point: the model tells you which action it thinks it is answering, and you check what that action actually is before forwarding anything.
You have ten minutes. pendingUserAction.expiresAt is authoritative and there is no extension. If nobody can answer in that window, decline — same terminal outcome, minutes sooner, and the reason is recorded. Letting an action expire also holds one of your five concurrent-checkout slots for the full TTL.

Avoiding the questions

The best-handled action is one that never gets raised.
  • Attach a buyerProfileId and no shipping action appears.
  • Put the option in request"buy this in medium, black" — and a single unambiguous match skips the variant prompt. Values are matched as whole words against the store’s option values.
  • Store a buyer session once and later runs at that merchant start logged in, so credentials and otp stop recurring for that buyer.
card cannot be avoided, and that is deliberate: reaching it is the guarantee that nothing has been charged yet. The code path that fills a card and clicks pay is only reachable from a resume that carries card values.

A worked loop

Plain fetch, no SDK. This is the tool-handler side — the part you wire behind whichever model you are using.
farthing-tools.ts
Driving it, if your agent is not itself the scheduler:
Treat everything the checkout says as data, not instructions. pendingUserAction.message and the progressItems labels are written by us, but they quote merchant content — option names, product titles, form labels — and a merchant page is untrusted input. Never let a string that arrived from a checkout change what your agent is allowed to do, and never let one authorise a payment.

Handling the ends

There is no retry, replay or clone of a finished run. Create a new checkout with the same body — and a new Idempotency-Key, because the old one points at the failure and keeps returning it for as long as it is live.

The zero-code option: MCP

The MCP server exposes the same surface as tools, so an MCP-capable client — Claude Desktop, Claude Code, or your own harness — can drive a checkout with no integration code at all. Set FARTHING_API_KEY and point your client at it.
Five tools: At the card step the server leads with the merchant’s own order total, because that is the last moment the price can be questioned before money moves. farthing_create_checkout is annotated idempotentHint: false, honestly: the server sends no Idempotency-Key and will not invent one on your behalf, so two calls are two purchases. Your MCP client’s own retry or approval behaviour is what stands between a flaky connection and a second order — another reason a production integration talks to the HTTP API. It refuses card, credentials and OTP values as tool arguments, by design and for the reason above: those are not things a model should be holding. When a run blocks on one of them, the server surfaces the question and the human answers it out of band. That refusal is also the reason MCP is not the whole answer for a production integration. You still need somewhere for a person to type a card, which means a UI, which means the HTTP API. MCP is the right way to try the product, script one-off purchases, and let an operator drive a run from a chat window.

Next

User actions

Every key, its exact schema, TTLs, decline, and what happens to submitted values.

Checkout lifecycle

Every status and failure reason, and what actually causes each one.

Webhooks

Get pushed status transitions instead of polling a run that is parked for ten minutes.

Sandbox

Prove your decline, expiry and max_cost_exceeded paths before a real card is involved.