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

# 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:

<div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px', margin: '20px 0 4px' }}>
  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/amazon.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Amazon logo" noZoom />

    <span style={{ fontSize: '14px' }}>Amazon</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/walmart.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Walmart logo" noZoom />

    <span style={{ fontSize: '14px' }}>Walmart</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/target.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Target logo" noZoom />

    <span style={{ fontSize: '14px' }}>Target</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/bestbuy.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Best Buy logo" noZoom />

    <span style={{ fontSize: '14px' }}>Best Buy</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/homedepot.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Home Depot logo" noZoom />

    <span style={{ fontSize: '14px' }}>Home Depot</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/lowes.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Lowe's logo" noZoom />

    <span style={{ fontSize: '14px' }}>Lowe's</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/macys.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Macy's logo" noZoom />

    <span style={{ fontSize: '14px' }}>Macy's</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/wayfair.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Wayfair logo" noZoom />

    <span style={{ fontSize: '14px' }}>Wayfair</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/staples.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Staples logo" noZoom />

    <span style={{ fontSize: '14px' }}>Staples</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/kohls.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Kohl's logo" noZoom />

    <span style={{ fontSize: '14px' }}>Kohl's</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/bhphotovideo.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="B&H Photo logo" noZoom />

    <span style={{ fontSize: '14px' }}>B\&H Photo</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/doordash.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="DoorDash logo" noZoom />

    <span style={{ fontSize: '14px' }}>DoorDash</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/goodeggs.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Good Eggs logo" noZoom />

    <span style={{ fontSize: '14px' }}>Good Eggs</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <img src="https://img.logo.dev/shoplocale.com?token=pk_TvMnR1n1RHyLHib6YoaMrw&size=48&retina=true" width="22" height="22" style={{ borderRadius: '5px', margin: 0, display: 'block' }} alt="Locale logo" noZoom />

    <span style={{ fontSize: '14px' }}>Locale</span>
  </div>

  <div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px 6px 8px', borderRadius: '10px', border: '1px solid rgba(128,128,128,0.25)' }}>
    <Icon icon="plane" size={18} />

    <span style={{ fontSize: '14px' }}>Flights</span>
  </div>
</div>

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"}'
```

### 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": "cmsq18x2m00a1", "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": "cmsq18x2m00a1",
  "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": []
}
```

Notes:

* `unmatched` lists anything requested that didn't make it into the cart (plus a reason). Show it instead of guessing from the prose.
* 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": "cmsq18x2m00a1", "confirm": "9f2c4a1b8e3d5f07", "payment_source": "vault"}'
```

### 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": "cmsq18x2m00a1",
  "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 (Face ID), repeat the same confirm call from step 4.

When the purchase is placed:

```json theme={null}
{
  "conversation_id": "cmsq18x2m00a1",
  "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`, `items_unavailable`, …             | Tell an approval pause from a real decline.      |
| `approval_url`  | string or null                                                | Send to the user when the confirm paused.        |
| `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.

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