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

# Buy catalog items for a person

> Send Agentcard a product from the catalog. Agentcard builds the cart at the store, opens the person's approval of the store's own total, and places the order.

Send Agentcard a product from the Channel3 catalog, or the store's own product page, and the person it is for. Agentcard builds the cart at the store, reads the store's own total, and opens the person's approval of that total in their Vault. Once they approve, Agentcard places the order with their own card.

> [!NOTE]
>
> Catalog purchases are in beta. A field can still change before the API is final.

## Check what you need

* Your company's `client_id` and `client_secret`, exchanged for an access token. Agentcard refuses an API key when you create, answer or cancel a purchase. Your company's API key reads a purchase too.
* The person connected to your app with a card in their Vault. See [Adding a card](https://docs.agentcard.com/vault/adding-a-card.md).
* Catalog purchases turned on for your company. Ask Agentcard to turn them on.
* Commerce billing set up in the dashboard, for live purchases.

## Create a purchase

Name the catalog, the person, and the product, with an `Idempotency-Key` you keep when you retry the same purchase. The answer comes back at once, with the purchase building its cart:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/checkout/authorizations \
  -H "Authorization: Bearer $AGENTCARD_ORG_TOKEN" \
  -H "Idempotency-Key: 8f14e45f-ea7c-4c3a-9a51-3b2f1d0c7a10" \
  -H "Content-Type: application/json" \
  -d '{
    "vendor": "channel3",
    "external_user_id": "shop_42",
    "url": "https://www.allbirds.com/products/mens-tree-runners",
    "options": { "Size": "10" },
    "quantity": 1,
    "user_details": { "email": "ada@example.com", "first_name": "Ada", "last_name": "Lovelace", "phone": "+14155550100" },
    "return_url": "https://app.example.com/done"
  }'
```

```json theme={null}
{
  "id": "cauth_206d9cfc3e959c7b85694904",
  "object": "checkout_authorization",
  "kind": "catalog",
  "status": "building_cart",
  "user": "cmvkbknxm0002ccw08ki55v9i",
  "external_user_id": "shop_42",
  "vendor": "channel3",
  "items": [{ "url": "https://www.allbirds.com/products/mens-tree-runners", "options": { "Size": "10" }, "quantity": 1 }],
  "cart": null,
  "amount": null,
  "currency": null,
  "sandbox": true,
  "return_url": "https://app.example.com/done",
  "createdAt": "2026-10-23T01:57:07.225Z",
  "expiresAt": "2026-10-23T02:27:07.190Z"
}
```

| Field | What to send |
| - | - |
| `vendor` | Required. The catalog the product comes from: `channel3`. |
| `user` or `external_user_id` | Exactly one. `user` is the id Agentcard gave you when the person connected; `external_user_id` is your own id for them. |
| `url` | The store's own product page. Never a `buy.trychannel3.com` link. |
| `options` | The choices the catalog lists for the product, such as `{ "Size": "10" }`, at most 8. |
| `quantity` | How many, 1 to 20. Defaults to 1. |
| `user_details` | Optional. The person's `email`, `first_name`, `last_name` and `phone` for the store's form. |
| `delivery_address` | Optional. Where the order ships. Without it, Agentcard uses the address the person confirmed with your app, or asks you for one. |
| `return_url` | Optional. A registered redirect URI the approval page returns the person to. |

Send an `Idempotency-Key` header with each create. A retry with the same body, the same credentials and the same mode within 24 hours gets the first answer back instead of making another purchase. Agentcard keeps refusals too, so use a new key once you correct the body: Agentcard refuses the same key with a changed body as `idempotency_key_reused`. Agentcard refuses a retry while the first request still runs as `idempotency_key_in_flight`; wait, and retry with the same key. After 24 hours the key expires, and reusing it can make another purchase.

## Buy several products

Send `catalog.items` instead of `url` to name products by their catalog id, each with the store that sells it:

```json theme={null}
{
  "vendor": "channel3",
  "user": "cmvkbknxm0002ccw08ki55v9i",
  "catalog": { "items": [{ "product_id": "IEgrQSZ", "store": "allbirds.com", "options": { "Size": "10" }, "quantity": 1 }] }
}
```

One purchase buys at one store. At a store Agentcard orders from directly, one purchase holds up to 10 products. At a store whose own checkout page Agentcard fills in, one purchase holds one product. A `product_id` with no `store` buys the cheapest offer in stock at a store Agentcard can order from.

## Follow the purchase

Read where the purchase stands at any time:

```bash theme={null}
curl https://api.agentcard.sh/api/v2/checkout/authorizations/cauth_206d9cfc3e959c7b85694904 \
  -H "Authorization: Bearer $AGENTCARD_ORG_TOKEN"
```

| `status` | What it means | What to do |
| - | - | - |
| `building_cart` | Agentcard is building the cart at the store. | Wait, or listen for the next message. |
| `needs_input` | The store asked something first. | Answer it, as the next section shows. |
| `awaiting_approval` | The cart is ready at the store's total. | Send the person `approvalUrl`. |
| `placing` | The person approved, and the order is going through. | Wait. |
| `placed` | The store took the order. `order.number` holds its order number. | Tell the person. |
| `unresolved` | The card reached the store, and the store has not confirmed yet. | Wait. Never buy the same thing again meanwhile. |
| `approved` | Sandbox only: the person approved, and Agentcard places no order. | Nothing. |
| `failed` | No order went through: the store placed none, or cancelled the one it took, with `failure.code` `store_cancelled`. `failure.message` says why. | Start a new purchase, or tell the person. |
| `declined` | The person declined. | Nothing. |
| `expired` | Nobody answered or approved in time. | Start a new purchase. |
| `cancelled` | You cancelled it. | Nothing. |

## Answer the store

A purchase in `needs_input` carries the store's question in `input`. Here the size the person chose sold out, and the store still has two others:

```json theme={null}
{
  "status": "needs_input",
  "input": {
    "kind": "options",
    "question": "allbirds.com has Men's Tree Runners in Size 8, 11, and Size 10 is sold out. Which Size?",
    "options": [{ "name": "Size", "values": ["8", "11"] }]
  }
}
```

Send the answer, and the purchase builds its cart again:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/checkout/authorizations/cauth_206d9cfc3e959c7b85694904/input \
  -H "Authorization: Bearer $AGENTCARD_ORG_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "options": { "Size": "8" } }'
```

| `input.kind` | What the store needs | What to send |
| - | - | - |
| `options` | A choice, such as a size. | `{ "options": { "Size": "8" } }` |
| `address` | Where the order ships. | `{ "delivery_address": { ... } }`, as below |
| `email` | The email for the store's order confirmation. | `{ "email": "ada@example.com" }` |
| `card` | A card in the person's Vault. Send them `input.url`. | `{}` once they add it |
| `store_sign_in` | The person signs in to the store. Send them `input.url`. | `{}` once they sign in |

A delivery address names the street, the city, a two-letter state or province code, and a US ZIP or ZIP+4 or a Canadian postal code. The name, a second address line, a two-letter country code and the phone are optional:

```json theme={null}
{
  "delivery_address": {
    "name": "Ada Lovelace",
    "street": "1900 Jefferson St",
    "address2": "Apt 4",
    "city": "San Francisco",
    "state": "CA",
    "zip": "94123",
    "country": "US",
    "phone": "+14155550100"
  }
}
```

`delivery_address` on the create takes the same fields.

A `card` question for a sandbox person carries no `input.url`, because the add-a-card link is a live one. Open a Vault session for them with `POST /api/v2/vault_sessions`, as [Adding a card](https://docs.agentcard.com/vault/adding-a-card.md) shows, and send `{}` once they saved a card.

Agentcard refuses an answer that leaves out what the store asked, and the question stays open:

```text theme={null}
400 {"error":{"code":"answer_missing","message":"Send options with Size.","status":"needs_input"}}
```

Send the answer again with the option the message names:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/checkout/authorizations/cauth_206d9cfc3e959c7b85694904/input \
  -H "Authorization: Bearer $AGENTCARD_ORG_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "options": { "Size": "8" } }'
```

A question waits 30 minutes for its answer. After that the purchase ends `expired`.

## Send the approval link

A purchase in `awaiting_approval` carries the cart, the store's total, and the approval link:

```json theme={null}
{
  "status": "awaiting_approval",
  "cart": {
    "store": "allbirds.com",
    "lines": [{ "title": "Men's Tree Runners", "product_id": "IEgrQSZ", "options": { "Size": "10" }, "quantity": 1, "unit_price_cents": 10500, "image_url": null }],
    "total_cents": 11373,
    "currency": "usd"
  },
  "amount": 11373,
  "approvalUrl": "https://vault.agentcard.sh/authorize?id=cauth_docs_round_8d0103ec",
  "approval_expires_at": "2026-10-23T02:07:07.680Z"
}
```

Send the person `approvalUrl` right away. They see the store and the total, and approve with their passkey. `approval_expires_at` says when the link stops working; after that the purchase ends `expired`. When the store may authorize more than its total, `amount` is that larger figure and `cart.total_cents` is what the person pays.

## Hear each step

Subscribe your [webhook endpoint](https://docs.agentcard.com/webhooks/overview.md) to `purchase.message` or `purchase.*` in the purchase's mode. A catch-all `*` alone does not turn these messages on.

Agentcard tells your company about each step as a `purchase.message` event. The step is in the event's `data`: `data.facts.authorization_id` names the purchase, and `data.text` is ready to send to the person as it is:

```json theme={null}
{
  "id": "evt_8c1f3e9a2b7d4f60a1e2c3d4",
  "type": "purchase.message",
  "created": 1792720027,
  "livemode": true,
  "data": {
    "kind": "cart_ready",
    "user_id": "cmvkbknxm0002ccw08ki55v9i",
    "external_user_id": "shop_42",
    "text": "Your cart at allbirds.com is ready: Men's Tree Runners, $113.73 with tax and shipping. Approve it here: https://vault.agentcard.sh/authorize?id=cauth_docs_round_8d0103ec",
    "note": "[event: the cart this user asked for is ready at the store \"allbirds.com\": a product the store titles \"Men's Tree Runners\", at $113.73 with tax and shipping, read from the store's own checkout. The user approves at the approval link, and nothing is paid until they do. This note was generated by the system, not typed by the user.]",
    "facts": {
      "authorization_id": "cauth_c0dfb60db01a017c7b64321d",
      "merchant_name": "allbirds.com",
      "total_cents": 11373,
      "currency": "usd",
      "approval_url": "https://vault.agentcard.sh/authorize?id=cauth_docs_round_8d0103ec",
      "approval_expires_at": "2026-10-23T02:07:07.680Z",
      "valid_until": "2026-10-23T02:07:07.680Z"
    },
    "connection": { "client_id": "24faba58-ebc6-49cd-a291-4956802e31ee", "scope": "user_connection" }
  }
}
```

`data.note` is the same step written for your own agent to read.

| `kind` | When Agentcard sends it |
| - | - |
| `cart_ready` | The cart is ready, with the approval link. |
| `cart_needs_input` | The store asked something first. |
| `cart_failed` | The store built no cart, and Agentcard charged nothing. |
| `order_placed` | The store took the order. |
| `order_failed` | The person approved, and the order did not go through. |
| `order_unconfirmed` | The card reached the store, and the store has not confirmed the order yet. |

Drop an event that arrives after its `data.facts.valid_until`: its link no longer works.

## Cancel a purchase

Cancel a purchase the person has not approved yet:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/checkout/authorizations/cauth_206d9cfc3e959c7b85694904/cancel \
  -H "Authorization: Bearer $AGENTCARD_ORG_TOKEN"
```

The purchase ends `cancelled`, and an approval link already sent stops working. Once the person approved, or the purchase ended, Agentcard refuses the cancel and the purchase goes on as it stands:

```text theme={null}
409 {"error":{"code":"not_cancellable","message":"This purchase is placed: only one the shopper has not approved can be cancelled.","status":"placed"}}
```

A purchase at a store Agentcard orders from directly stops being cancellable once its cart is built, while it still reads `building_cart`, because the store can take the order at once. Agentcard refuses that cancel too, and the purchase goes on to its order:

```text theme={null}
409 {"error":{"code":"not_cancellable","message":"The store is taking this purchase's order now, so it can no longer be cancelled.","status":"building_cart"}}
```

## Test in sandbox

Your sandbox credentials make sandbox purchases. Agentcard builds the cart at the store and the approval opens as it does live, and after the person approves the purchase ends `approved` with no order placed and nothing charged. Stores Agentcard orders from directly take live purchases only. A sandbox credential never reads, cancels or answers a live purchase.

## Fix a refusal

A create that Agentcard refuses makes nothing. Each refusal names what to change:

```text theme={null}
400 {"error":{"code":"vendor_required","message":"vendor names the catalog the items come from. Send one of: channel3.","accepted":["channel3"]}}
```

Add the catalog and send the create again, with a new `Idempotency-Key`:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/checkout/authorizations \
  -H "Authorization: Bearer $AGENTCARD_ORG_TOKEN" \
  -H "Idempotency-Key: 2c9d7f41-5b8e-4a6f-9d3c-7e1a0b4f8c22" \
  -H "Content-Type: application/json" \
  -d '{ "vendor": "channel3", "external_user_id": "shop_42", "url": "https://www.allbirds.com/products/mens-tree-runners", "options": { "Size": "10" } }'
```

| `error.code` | What it means | What to do |
| - | - | - |
| `vendor_required` | The create named no catalog. | Add `"vendor": "channel3"`. |
| `invalid_vendor` | The catalog is not one Agentcard takes. | Send a value from `accepted`. |
| `invalid_request` | A field is missing, malformed or unknown, or the create names the person twice or not at all. | Fix the field `message` names. |
| `invalid_item` | An item names both or neither of `product_id` and `url`, or a `store` beside a `url`. | Name each product one way. |
| `invalid_url` | `url` is not the secure address of a store's product page. | Send the page's full address, starting with `https://`. |
| `invalid_return_url` | `return_url` is not one of your app's registered redirect addresses. | Register it, or send one already registered. |
| `invalid_delivery_address` | The address leaves out the street, city, state or ZIP, or one of them is malformed. | Send the fields the address example above shows. |
| `channel3_link_refused` | `url` is a Channel3 link. | Send the `product_id`, or the store page the link leads to. |
| `items_span_stores` | The items name more than one store. | Send each store's items in a purchase of their own. |
| `connection_not_found` | No person connected to your app carries that id in this mode. | Connect the person first, with the same credentials. |
| `external_user_id_ambiguous` | Several connected people carry that `external_user_id`. | Send their `user` instead. |
| `catalog_checkout_not_enabled` | Catalog purchases are not turned on for your company. | Ask Agentcard to turn them on. |
| `commerce_billing_required` | Live purchases need commerce billing. | Set it up in the dashboard. |
| `idempotency_key_reused` | You sent the `Idempotency-Key` before with a different body. | Send a new key. |
| `idempotency_key_in_flight` | The first request with this key is still running. | Wait, and retry with the same key. |
| `answer_missing` | An answer leaves out what the store asked. | Send the field `message` names. |
| `not_waiting_for_input` | The purchase is not waiting for an answer. | Read the purchase, and answer only a `needs_input` one. |
| `not_cancellable` | The person already approved, or the purchase ended. | Read the purchase: it goes on as it stands. |

## Know the limits

* One purchase buys at one store, with one product at a store whose checkout page Agentcard fills in.
* A purchase never takes an amount: the amount is always the store's own total.
* A cart that takes long to build leaves the person less time to approve. `approval_expires_at` always says how long.


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