Where agents can buy
Here are a few examples of merchants supported today:Reservations
Flights
GET /buy/merchants with the same bearer token you send to /buy.
What you need
- A user with a card in the Vault (Adding a card).
- A user-scoped bearer token.
/buyalways runs as a single user, so org tokens are rejected.- Use the user’s connection
access_token, or mint abuy_tokenfor a cardholder you own:
Quickstart
Every call is aPOST /buy. A single purchase follows a turn-based loop, tied together by conversation_id.
1) Start (send an ask)
If your app manages the user’s addresses
Send the address as data instead of prose. Passdelivery_address on every call for the conversation, starting with the first. That includes the call that asks for the cart and the call that confirms it. The agent ships to exactly these values, never asks the user to confirm or correct them, and does not read or update the wallet’s saved default address for that conversation.
Re-sending the same address costs nothing. It keeps the original binding, so a cart shown earlier still confirms. A different address replaces the old one.
Send it late and the order fails. A cart is bound to the address that was set when the cart was shown, so a cart shown before your first delivery_address cannot be confirmed against it. A confirm of that cart is refused:
street, city, state (a two-letter state or province code) and zip (a US ZIP or a Canadian postal code) are required. address2, phone, name, can_leave_at_door and country (ISO 3166-1 alpha-2) are optional; retail shipping needs phone. A malformed address is refused with 400 invalid_delivery_address and a message naming the problem, before any turn runs.
For a merchant account the user linked that keeps its own address book (DoorDash, for example), the agent picks the saved entry that matches the address you sent, and says so when none does.
2) Loop: reply → ask (until a cart is ready)
If the API needs more info, it responds withstatus: "needs_input" and a natural-language reply.
Show reply to the user, then send the user’s answer back as the next ask with the same conversation_id:
3) Read the cart (use structured fields)
Once the API has enough details, the response includes acart and a cart.hash. Use these structured fields (not the prose) to show the user exactly what will be purchased.
catalog, with a link and a photo per product:
unmatchedlists anything requested that didn’t make it into the cart (plus a reason). Show it instead of guessing from the prose.catalog.itemsis the last search as data: each item’sidis the merchant’s product link andimage_urlits photo when the merchant has one. Show the photo and send the link from these fields; a URL your agent composes from the product name is a 404 on the user’s phone.catalogis null on a turn that did not search again, so keep the last one.- The status can still be
needs_inputhere, because the API is asking for confirmation. - If the user wants a change (for example, “make it two bags”), send that as another
ask. You’ll get an updated cart with a newhash.
4) Confirm the cart hash (and choose a payment source)
When the user wants to proceed, confirm the exact cart you showed by sending thecart.hash (not a free-form “yes”). To pay with the user’s own card, pass payment_source: "vault".
5) If approval is required: send the approval URL, then retry confirm
A confirm may pause while the user approves:approval_url to the user. After they approve with their passkey, repeat the same confirm call from step 4.
When the purchase is placed:
The fields you branch on
Edge cases
If the price changed
A confirm authorizes one cart at one price. If anything drifted since the cart was shown, confirm returns409 with a fresh cart and a new hash. Show the user the new total, then confirm the new hash.
If the destination changed
A cart is also bound to the address it was shown for, so a confirm is refused when that address is no longer the one bound to the conversation. Twocode values say which case it is:
Nothing was replaced in the second case. The ordering was wrong, and the fix is to send the address earlier. Both refusals carry the current
cart and carts, so you never have to guess the new hash.
Track the order
Retail orders confirm asynchronously (often ~1 minute after placement).order.placed and order.confirmed webhooks instead of polling.
If a confirm times out
Don’t resend. First, read the conversation:turn_in_progress to clear, then check orders. A duplicate confirm while a turn is running returns 409 turn_in_progress, so the same cart can never place twice.
Over MCP
The same loop is available as thebuy tool on https://mcp.agentcard.sh/mcp, using the same bearer. The agent relays each turn, the user confirms in words, and the tool places the order.
Sandbox
Sandbox runs the real loop against real merchants up to the confirm. The confirm returnsdeclined with decline_code: "sandbox_mode" by design, because sandbox cards can’t pay a real merchant. Everything before it (conversation, cart, hash) is identical to production.