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

# Agentcard's Purchase API

> Let your agent buy online without building merchant integrations or running an agent browser.

Use Agentcard's Purchase API to let your agent buy things online without building merchant integrations or using an agent browser. Merchants supported today include Amazon, Walmart, Target, Best Buy, and many more.

## Where agents can buy

Here are a few examples of merchants supported today:

![Amazon logo](https://img.logo.dev/amazon.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Amazon

![Walmart logo](https://img.logo.dev/walmart.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Walmart

![Target logo](https://img.logo.dev/target.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Target

![Best Buy logo](https://img.logo.dev/bestbuy.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Best Buy

![Home Depot logo](https://img.logo.dev/homedepot.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Home Depot

![Lowe's logo](https://img.logo.dev/lowes.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Lowe's

![Macy's logo](https://img.logo.dev/macys.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Macy's

![Wayfair logo](https://img.logo.dev/wayfair.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Wayfair

![Staples logo](https://img.logo.dev/staples.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Staples

![Kohl's logo](https://img.logo.dev/kohls.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Kohl's

![B\&H Photo logo](https://img.logo.dev/bhphotovideo.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

B\&H Photo

![DoorDash logo](https://img.logo.dev/doordash.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

DoorDash

![Gomry logo](https://mintcdn.com/agentcard/vPG8gov4ipUDThvN/images/logos/gomry.png?fit=max\&auto=format\&n=vPG8gov4ipUDThvN\&q=85\&s=c797d2394104ea1e72cb6c2f235fa96f)

Gomry

![Good Eggs logo](https://img.logo.dev/goodeggs.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Good Eggs

![Locale logo](https://img.logo.dev/shoplocale.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Locale

![Ophelia logo](https://img.logo.dev/opheliaos.com?token=pk_TvMnR1n1RHyLHib6YoaMrw\&size=48\&retina=true)

Ophelia

Reservations

Flights

For the live list, call `GET /buy/merchants` with the same bearer token you send to `/buy`.

## What you need

1. **A user with a card in the Vault** ([Adding a card](/vault/adding-a-card)).
2. **A user-scoped bearer token.**
   * `/buy` always runs as a single user, so org tokens are rejected.
   * Use the user's connection `access_token`, or mint a `buy_token` for a cardholder you own:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v1/cardholders/CARDHOLDER_ID/buy_token \
  -H "Authorization: Bearer $ORG_TOKEN"
```

**Recommended:** set a client timeout of at least 120 seconds. Each turn runs against a live merchant.

## Quickstart

Every call is a `POST /buy`. A single purchase follows a turn-based loop, tied together by `conversation_id`.

### 1) Start (send an ask)

```bash theme={null}
curl -X POST https://api.agentcard.sh/buy \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"ask": "a 16 oz bag of Colombian ground coffee from Amazon, ship it to 1900 Jefferson St, San Francisco"}'
```

### If your app manages the user's addresses

Send the address as data instead of prose. Pass `delivery_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:

```text theme={null}
{
  "error": "the cart was shown before a delivery address was bound to this conversation: send delivery_address on the same call that asks for the cart, then confirm the hash that call returns",
  "code": "delivery_address_bound_after_cart",
  "conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f",
  "cart": {
    "merchant": "retail",
    "merchant_name": "Amazon, Walmart, Target & Best Buy",
    "items": [
      { "name": "Cafe Mesa de los Santos Colombian Ground Coffee, 16 oz", "qty": 1, "priceCents": 2250, "product_id": "https://www.amazon.com/dp/B07ZQ5C4RB" }
    ],
    "serviceFeesCents": 56,
    "tipCents": 0,
    "totalCents": 2306,
    "approvedCeilingCents": null,
    "hash": "9f2c4a1b8e3d5f07"
  },
  "carts": [
    {
      "merchant": "retail",
      "merchant_name": "Amazon, Walmart, Target & Best Buy",
      "items": [
        { "name": "Cafe Mesa de los Santos Colombian Ground Coffee, 16 oz", "qty": 1, "priceCents": 2250, "product_id": "https://www.amazon.com/dp/B07ZQ5C4RB" }
      ],
      "serviceFeesCents": 56,
      "tipCents": 0,
      "totalCents": 2306,
      "approvedCeilingCents": null,
      "hash": "9f2c4a1b8e3d5f07"
    }
  ]
}
```

Ask for the cart again and confirm the hash that call returns.

```bash theme={null}
curl -X POST https://api.agentcard.sh/buy \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "ask": "a grande iced latte from the nearest Starbucks",
    "delivery_address": {
      "street": "1900 Jefferson St",
      "address2": "Apt 4",
      "city": "San Francisco",
      "state": "CA",
      "zip": "94123",
      "phone": "+14155550100",
      "name": "Ada Lovelace"
    }
  }'
```

`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 with `status: "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`:

```bash theme={null}
curl -X POST https://api.agentcard.sh/buy \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f", "ask": "the Cafe Mesa one"}'
```

Repeat this loop as needed. Nothing can be charged during it.

### 3) Read the cart (use structured fields)

Once the API has enough details, the response includes a `cart` and a `cart.hash`. Use these structured fields (not the prose) to show the user exactly what will be purchased.

```json theme={null}
{
  "conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f",
  "status": "needs_input",
  "reply": "Found it. Cafe Mesa de los Santos Colombian Ground Coffee, 16 oz, at $22.50. Total $23.06 shipping to 1900 Jefferson St. Want me to place it?",
  "cart": {
    "merchant": "retail",
    "merchant_name": "Amazon",
    "items": [
      { "name": "Cafe Mesa de los Santos Colombian Ground Coffee, 16 oz", "qty": 1, "priceCents": 2250, "product_id": "https://www.amazon.com/dp/B07ZQ5C4RB" }
    ],
    "totalCents": 2306,
    "hash": "9f2c4a1b8e3d5f07"
  },
  "unmatched": []
}
```

The same response carries the search as data in `catalog`, with a link and a photo per product:

```json theme={null}
{
  "catalog": {
    "merchant": "retail",
    "merchant_name": "Amazon",
    "items": [
      {
        "id": "https://www.amazon.com/dp/B07ZQ5C4RB",
        "name": "Cafe Mesa de los Santos Colombian Ground Coffee, 16 oz",
        "priceCents": 2250,
        "image_url": "https://m.media-amazon.com/images/I/71x0Q1e2sPL._AC_UL320_.jpg"
      }
    ],
    "as_of": "2026-09-28T19:26:04Z"
  }
}
```

Notes:

* `unmatched` lists anything requested that didn't make it into the cart (plus a reason). Show it instead of guessing from the prose.
* `catalog.items` is the last search as data: each item's `id` is the merchant's product link and `image_url` its 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. `catalog` is null on a turn that did not search again, so keep the last one.
* The status can still be `needs_input` here, 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 new `hash`.

### 4) Confirm the cart hash (and choose a payment source)

When the user wants to proceed, confirm the exact cart you showed by sending the `cart.hash` (not a free-form "yes"). To pay with the user's own card, pass `payment_source: "vault"`.

```bash theme={null}
curl -X POST https://api.agentcard.sh/buy \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f", "confirm": "9f2c4a1b8e3d5f07", "payment_source": "vault"}'
```

### Cap what the card is held for

Send `max_total_cents` with `payment_source: "vault"` on the confirm to cap the hold on the user's card. For a cart holding one `$5.00` item at Amazon, with hash `4b7e1d0c9a2f6e35`, sending `max_total_cents: 1000` asks the user to approve `$10.00` instead of the uncapped `$15.85` hold:

```bash theme={null}
curl -X POST https://api.agentcard.sh/buy \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f", "confirm": "4b7e1d0c9a2f6e35", "payment_source": "vault", "max_total_cents": 1000}'
```

A store that adds shipping and tax only when it places the order asks for the uncapped `$15.85`, because it does not know the total yet. Under the cap the card is charged only the final total. If the store's final total with shipping and tax comes in above `$10.00`, the store does not place the order, the hold is released, and nothing is charged.

A cap below what the cart costs before shipping and tax could never place the order, so the confirm is declined before the user is asked:

```json theme={null}
{
  "conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f",
  "status": "declined",
  "decline_code": "max_total_below_estimate",
  "approval_url": null,
  "charge_status": "none"
}
```

Confirm the `$5.00` cart again with `max_total_cents: 1000`:

```bash theme={null}
curl -X POST https://api.agentcard.sh/buy \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f", "confirm": "4b7e1d0c9a2f6e35", "payment_source": "vault", "max_total_cents": 1000}'
```

To stay under a lower cap, send another `ask` to trim the cart, then confirm the new hash. `max_total_cents` caps the hold on a vault payment, for one cart, in the cart's currency. A purchase paid from the user's Agentcard balance or from an added card ignores the cap, so send `payment_source: "vault"` with it rather than leaving the source to the user's default.

### 5) If approval is required: send the approval URL, then retry confirm

A confirm may pause while the user approves:

```json theme={null}
{
  "conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f",
  "status": "declined",
  "decline_code": "vault_approval_required",
  "approval_url": "https://vault.agentcard.sh/authorize?id=cauth_2q9d1x8f3k2m4t7w",
  "charge_status": "none"
}
```

This is a pause, not a verdict. Nothing was charged. Send `approval_url` to the user. After they approve with their passkey, repeat the same confirm call from step 4.

When the hold is above what the cart is expected to cost, the approval page says so under the amount. The estimate is the cart plus any order fee and card processing the store adds. For the `$5.00` item confirmed with `max_total_cents: 1000`, the user reads `Estimated $6.58 before shipping and tax. Your card is held for up to $10.00; only the final total is charged.`

When the purchase is placed:

```json theme={null}
{
  "conversation_id": "conv_9a8b7c6d5e4f3a2b1c0d9e8f",
  "status": "order_placed",
  "reply": "Order placed at Amazon. Charged $23.06 to your Visa ending 4832.",
  "order_id": "3f9a8c1b-7d2e-4c5a-9b1f-2e8d4a6c0b17",
  "payment_source": { "source": "vault", "brand": "visa", "last4": "4832" },
  "charge_status": "settled"
}
```

## The fields you branch on

| Field | Values | Use it to |
| - | - | - |
| `status` | `needs_input`, `order_placed`, `partially_placed`, `declined` | Decide what to do next. Never branch on `reply`. |
| `cart.hash` | string | Confirm exactly this cart. |
| `decline_code` | `vault_approval_required`, `max_total_below_estimate`, `items_unavailable`, `unsupported_currency`, `currency_changed`, `no_cart`, … | Tell an approval pause from a real decline. The payment refusal codes are listed with what to do next on [`order.failed`](/webhooks/orders/order-failed). |
| `approval_url` | string or null | Send to the user when the confirm paused. |
| `next_action` | `shipping_details`, `guest_details`, `add_card`, `confirm`, `verification_code`, `approve`, or null | Ask the user for this one step. `approve` with `status: "declined"` and `decline_code: "vault_approval_required"` is a pause: nothing was charged, and the user approves at `approval_url` before you repeat the confirm. |
| `charge_status` | `none`, `confirming`, `settled`, `unknown` | Tell the user whether money moved. |
| `order_id` | string or null | Reconcile and track. |

## Edge cases

### If the price changed

A confirm authorizes one cart at one price. If anything drifted since the cart was shown, confirm returns `409` 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. Two `code` values say which case it is:

| `code` | What it means | What to do |
| - | - | - |
| `delivery_address_changed` | A later call replaced the address the cart was shown for. | Ask for the cart again, show the user the new total, confirm the new hash. |
| `delivery_address_bound_after_cart` | You sent your first `delivery_address` after the cart was shown. | Send `delivery_address` on the call that asks for the cart, then confirm the hash it returns. |

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).

```bash theme={null}
curl "https://api.agentcard.sh/buy/v1/merchants/retail/orders/ORDER_ID/track" \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN"
```

This returns the retailer's order number, delivery window, final total, and shipments once a package ships. Or listen for the `order.placed` and `order.confirmed` webhooks instead of polling.

### If a confirm times out

Don't resend. First, read the conversation:

```bash theme={null}
curl https://api.agentcard.sh/buy/conversations/CONVERSATION_ID \
  -H "Authorization: Bearer $USER_ACCESS_TOKEN"
```

Wait for `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 the `buy` 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 returns `declined` 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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.