> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcard.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Purchase API

> One endpoint to buy from any merchant: natural language in, typed state out, a deterministic confirm.

The Purchase API — `POST /buy` — is the one-call purchase surface. Your agent
sends what the user wants in plain language; Agentcard's shopping agent
handles every merchant's flow — store selection, delivery address, carts,
per-merchant quirks — and hands back typed state your agent can verify. You
integrate one endpoint and get every merchant in the catalog, current and
future, without writing per-merchant logic.

<Note>
  **Authentication.** Every `/buy` call is authorized with the **user's
  connection `access_token`** as the bearer — the token pair your backend stored
  from [user authentication](/companies/api/user-authentication). A purchase
  always runs as one user (their carts, their address, their card), so the org
  client secret and platform `client_credentials` tokens are rejected here. On
  `401`, refresh the pair via `POST /api/v2/connect/refresh` and retry.
</Note>

<Note>
  **Over MCP:** the same loop is available as the
  [buy](/companies/mcp/tools/buy) tool on the Agentcard MCP server, using the
  same connection token — one surface, two transports.
</Note>

## Instructions for your agent

Paste this into your coding agent to integrate the Purchase API.
It assumes users are already connected (you store their tokens — see
[user authentication](/companies/api/user-authentication)).

```text Instructions for your agent theme={null}
You are integrating Agentcard's Purchase API (POST /buy) so this app's agent
can buy things for a connected user. Implement server-side; base URL
https://api.agentcard.sh.

AUTH: bearer = that user's connection access_token (from
POST /api/v2/connect/verify). Never the org client secret and never a
platform client_credentials token — a purchase always runs as ONE user.
On 401, refresh via POST /api/v2/connect/refresh (platform-token auth),
replace both stored tokens, retry.

1. ONE TURN PER CALL
POST /buy { "ask": "<what the user wants>" }             // first turn
POST /buy { "ask": "...", "conversation_id": "<id>" }    // every follow-up
-> 200 envelope { conversation_id, status, reply, messages, cart, carts,
   placements, catalog, error_code }. Thread conversation_id from the first
   response onward. Client timeout >= 120s: turns run against live merchants.

2. BRANCH ON status
- "needs_input": the NORMAL path, not an error — reply asks a question
  (address, store choice, confirmation). Relay it to the user and send their
  answer as the next ask.
- "order_placed": done; reply carries the confirmation and order id.
- "partially_placed": multi-cart confirm — check placements[] per merchant.
- "declined": the spend gate refused (budget, policy) — surface it, do not
  retry the same turn.

3. CONFIRM WITH THE HASH, NEVER WITH PROSE
When cart is non-null, verify items / qty / priceCents / totalCents as data,
then confirm by echoing the hash:
POST /buy { "confirm": "<cart.hash>", "conversation_id": "<id>" }
A drifted cart fails 409 and returns the fresh cart — re-verify, re-confirm.
Money can never move in the same turn a cart was first shown.
If a confirm TIMES OUT client-side, the outcome is UNKNOWN (the turn may
still be executing; server budget 10 min). Send NOTHING to /buy until
verified: a replayed hash only 409s after a completed placement — while the
cart is still live it RE-ATTEMPTS checkout — and a fresh ask that rebuilds a
cart is a second order, not a retry (there is no idempotency key on confirm).
Verify out of band (buy_order_history / merchant orders endpoint where
supported — retail has no order history — or the user's transaction feed via
list_transactions), and when still ambiguous, ask the human to check their
orders before doing anything else.

4. RELAY, DON'T FAIL
If the reply says a card, attachment, funding or approval is needed, relay
that to the user and retry once they've completed it. Don't hardcode the
merchant list — offer what the conversation (or buy_list_merchants over MCP)
reports as available.
```

## How it works

<Steps>
  <Step title="Send the ask">
    `POST /buy` with the user's request in plain language, authorized with
    that user's connection token. The first response returns the
    `conversation_id` you thread through every follow-up.
  </Step>

  <Step title="Answer questions">
    `needs_input` turns carry a question in `reply` — address, store choice,
    which item. Relay it to your user and send the answer as the next `ask`.
  </Step>

  <Step title="Verify the cart, confirm the hash">
    When a cart is on the table the response carries it as typed data with a
    `hash`. Your agent verifies the items and total as types and confirms by
    echoing the hash — never by parsing prose.
  </Step>

  <Step title="The order places">
    `order_placed` carries the confirmation. Track and manage follow-ups over
    the same conversation, or the [MCP shopping tools](/companies/mcp/overview).
  </Step>
</Steps>

```bash theme={null}
curl -X POST https://api.agentcard.sh/buy \
  -H "Authorization: Bearer $CONNECTION_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "ask": "get me a 24-pack of AA batteries, ship to 548 Market St, San Francisco, CA 94104, phone (415) 555-0134" }'
```

## The response envelope

Every **successful turn** (HTTP 200) carries the same fields — always present,
`null` when empty — so a typed client never guesses:

```json theme={null}
{
  "conversation_id": "cmsg…",
  "status": "needs_input",
  "reply": "Found them! Want the Duracell 24-pack at $14.99?",
  "messages": ["…"],
  "message_id": "cmsg…",
  "cart": null,
  "error_code": null
}
```

| Field        | Meaning                                                                                                                                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`     | `needs_input` \| `order_placed` \| `partially_placed` \| `declined` — what your agent branches on                                                                                                 |
| `reply`      | Prose for humans. **Never parse it programmatically** — it varies run to run                                                                                                                      |
| `messages`   | The same turn split into ordered bubbles, for chat surfaces that relay each one                                                                                                                   |
| `cart`       | The most recently shown machine-verifiable cart (below), else `null`. `merchant` is a stable public id; `merchant_name` is the display name                                                       |
| `carts`      | EVERY shown cart, one per merchant, oldest first — always an array                                                                                                                                |
| `placements` | Per-cart outcomes of a multi-cart confirm (below), else `null`                                                                                                                                    |
| `catalog`    | The last fresh product search as data — `{ merchant, merchant_name, store, items[{id, name, priceCents}], as_of }` — else `null`. Powers your own product picker; results expire after 15 minutes |
| `error_code` | Terminal loop errors (`disabled`, `api_error`, …), else `null`                                                                                                                                    |

`needs_input` is the **normal path, not an error**: the agent asked a question
(address, choice, confirmation). Send the answer as the next `ask` with the
same `conversation_id`.

Non-200 responses are compact error objects, not the envelope:

| HTTP  | Body                                     | When                                                                     |
| ----- | ---------------------------------------- | ------------------------------------------------------------------------ |
| `400` | `{ "error", "hint" }`                    | Invalid input (missing `ask`, malformed `confirm`)                       |
| `401` | `{ "error" }`                            | Missing or invalid bearer token                                          |
| `404` | `{ "error" }`                            | Unknown `conversation_id`                                                |
| `409` | `{ "error", "conversation_id" }`         | Conversation closed, or `confirm` sent before any cart was shown         |
| `409` | `{ "error", "conversation_id", "cart" }` | Confirm hash didn't match — the fresh cart is included (below)           |
| `502` | `{ "error", "conversation_id" }`         | The shopping agent failed unexpectedly mid-turn — safe to retry the turn |
| `503` | `{ "error" }`                            | Commerce temporarily unavailable                                         |
| `504` | `{ "error" }`                            | The turn exceeded the 10-minute server budget                            |

## The typed cart, and the deterministic confirm

A real purchase needs one thing to be exact: what gets charged. When the agent
has a cart ready, the response carries it as data:

```json theme={null}
"cart": {
  "merchant": "retail",
  "merchant_name": "Agentcard Retail",
  "items": [{ "name": "Duracell AA 24-Pack", "qty": 1, "priceCents": 1499 }],
  "serviceFeesCents": 37,
  "tipCents": 0,
  "totalCents": 1536,
  "hash": "7c24ac9456692b19"
}
```

Your agent verifies `items`, `qty`, `priceCents`, and `totalCents` **as
types** — no prose parsing — and confirms by echoing the hash instead of
saying "yes":

```bash theme={null}
curl -X POST https://api.agentcard.sh/buy \
  -H "Authorization: Bearer $CONNECTION_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "confirm": "7c24ac9456692b19", "conversation_id": "cmsg…" }'
```

The hash is checked **before any model turn** against the server's record of
the cart as last shown — the same record checkout executes from. If that shown
cart changed since your hash was issued (an item added or removed, a quantity,
a price, a fee — anything that re-recorded it), the call fails hard with `409`
and the fresh cart:

```json theme={null}
{
  "error": "cart changed since that hash was issued — verify the current cart and confirm its hash",
  "conversation_id": "cmsg…",
  "cart": { "…": "current cart with its new hash" }
}
```

Two gates protect the money moment. The **hash** is a deterministic
acknowledgment of the exact cart your agent verified (items, quantities,
prices, fees, tip) — a stale or wrong hash never reaches the shopping agent at
all. Then, at checkout itself, the server re-checks the live merchant
**total** against what was shown: a total that drifted after your confirm
makes checkout refuse and re-show rather than silently charge a different
amount. (The second gate is total-scoped — it does not detect a merchant-side
substitution at an identical total.) Prose confirmations
(`"ask": "yes, place it"`) pass through the same checkout gate — the hash path
adds the deterministic first gate that makes the verification yours.

<Note>
  Money only moves after a cart has been shown and confirmed in a **later**
  call — one-shot purchases are deliberately impossible. Checkout runs behind
  the same spend controls as everything else: budgets, per-merchant policies,
  approvals, and the order-markup disclosure.
</Note>

## Statuses

| `status`       | What happened                                                                      | What your agent does                            |
| -------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------- |
| `needs_input`  | Question asked, cart shown, or a recoverable merchant error (e.g. a missing phone) | Answer in the next `ask`, or `confirm` the hash |
| `order_placed` | The order is in; `reply` carries the confirmation and order id                     | Done — surface it                               |
| `declined`     | The spend gate refused (budget or policy), or the loop could not run               | Do not retry the same turn; surface the reason  |

A purchase that needs an owner **approval** reports `needs_input` — the reply
explains the pending approval, and once it is granted the next `ask` (or
`confirm`) proceeds.

## Multiple merchants, one conversation

A conversation can hold a shown cart at **several merchants at once** — ask
for sushi from DoorDash and batteries from Amazon in the same thread, and
`carts` carries one entry per merchant, each with its own `hash`. Confirm any
subset in one call by sending an array:

```json theme={null}
{ "confirm": ["7c24ac9456692b19", "2900ef13a2f724b2"], "conversation_id": "cmsg…" }
```

The hashes are all verified up front (any mismatch fails the whole call with
`409` and the fresh `carts` — nothing places). Then the orders place
**sequentially, each as its own turn**, and the response reports each outcome:

```json theme={null}
{
  "status": "partially_placed",
  "placements": [
    { "merchant": "retail", "merchant_name": "Agentcard Retail", "status": "order_placed", "reply": "Order placed at Agentcard Retail. …" },
    { "merchant": "doordash", "merchant_name": "DoorDash", "status": "declined", "reply": "Your DoorDash budget is exhausted. …" }
  ]
}
```

There is deliberately **no cross-merchant atomic transaction** — merchants
settle and fail independently at the money layer, so partial success is a real
outcome. Branch on `placements` entries, not just the top-level status:
`order_placed` = all placed, `partially_placed` = some, and a multi-cart
confirm cannot carry an `ask` (400) — send follow-ups as their own turns.

## Conversation threading

`conversation_id` is returned on every successful turn, including the first
(error bodies carry it only when a conversation is involved — see the table
above). Thread every follow-up with it. A conversation keeps its cart,
address, and merchant context across turns; a placed order clears the cart.
Continuing a closed conversation returns `409`.

## Merchants

The catalog behind `/buy` is Agentcard's merchant network — food delivery,
groceries, flights, retail, subscriptions — and it grows without any change on
your side: your integration is the endpoint, not the merchants. To shop with
your own retailer accounts on the retail catalog (order history, Prime and
loyalty benefits), see [Retailer accounts](/companies/guides/retailer-accounts).

## Timeouts and confirm retries

A turn can take up to a few minutes when it searches and builds carts against
live merchants — use a client timeout of at least 120 seconds.

A lost confirm **response** leaves the outcome unknown to your client — and a
turn's server-side budget is 10 minutes, so your client timing out does not
stop an in-flight placement. After a **completed** placement the cart is
cleared, so a replayed confirm returns `409` ("nothing to confirm") and can't
double-place. But while the outcome is unresolved — the first attempt still
running, or stopped before placing — the cart can still be live, and a
replayed hash **re-attempts checkout** rather than reporting status. There is
no idempotency key on confirm today.

So on a lost confirm response, send **nothing** to `/buy` — no replayed
confirm, and no fresh `ask` that rebuilds a cart (that's a second order, not
a retry) — until you have verified the outcome out of band. Merchants with order history
expose it typed —
[buy\_order\_history](/companies/mcp/tools/buy_order_history) over MCP or
`GET /buy/v1/merchants/<merchant>/orders` (the retail catalog does **not**
support order history) — and every placement ultimately shows in the user's
[transaction feed](/companies/mcp/tools/list_transactions). When the result
stays ambiguous, surface it to the human ("please check your orders before I
try again") rather than guessing. A first-class idempotent confirm with a
typed placement lookup is on our roadmap.
