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

# Integrating a merchant to the Purchase API

> Expose a catalog and an orders endpoint, and agents on Agentcard can buy from you.

The Purchase API is how agents on Agentcard buy things. An agent says what the user wants, Agentcard finds it at the merchant, shows the user an exact total, and places the order once they confirm. It works today at Amazon, Walmart, Target, DoorDash and the other merchants on the [Purchase API](/vault/integrations/ecommerce-apis/purchase-api) page.

If you are a merchant, this page lists what we need you to expose so we can add your store. You do not build against Agentcard. You give us a small read API for your catalog and a small write API for orders, and we do the integration on our side.

## How to get listed

Email **[founders@agentcard.sh](mailto:founders@agentcard.sh)** with:

1. The base URL of your API and a link to its docs, or the endpoints below if you are building them for us.
2. How to get us an API key, for staging first and production later. Do not paste keys in the email: a one-time secret link works, or invite [founders@agentcard.sh](mailto:founders@agentcard.sh) to your developer dashboard.
3. Which payment provider you charge through.
4. A few items we can order in staging without real inventory or money moving.

We build the connector, place test orders against your staging environment with you, and list you in `GET /buy/merchants` once real orders go through cleanly. Nothing to deploy, no SDK to install, no Agentcard account needed.

## What we need you to expose

Three groups: a catalog we can search, an orders endpoint we can quote and place against, and a way to pay. Field names below are a suggestion. If you already have an API that carries the same information under different names, send us that instead.

The endpoint names and JSON examples below illustrate a proposed merchant API contract. We agree the final contract with you during integration.

All money is an integer in the smallest unit of the currency, so `2250` is `$22.50`. Every id must be stable, so the same product returns the same id tomorrow.

## 1. Catalog

The agent needs to find what the user asked for and show them the price before anything is bought.

```
GET /products?q=&page=
GET /products/{id}
```

Search takes a free-text query and pages through results. Add whatever filters make sense for your catalog, such as `category`, `city`, `from` or `to`.

A product:

| Field                 | Notes                                                       |
| --------------------- | ----------------------------------------------------------- |
| `id`                  | Stable identifier.                                          |
| `name`, `description` | Shown to the user.                                          |
| `image_url`, `url`    | Product image and the page on your site.                    |
| `status`              | `available`, `sold_out` or `unavailable`.                   |
| `variants[]`          | Sizes, colors, ticket types. Anything the user has to pick. |

A variant:

| Field                            | Notes                                                                       |
| -------------------------------- | --------------------------------------------------------------------------- |
| `id`                             | Stable identifier. This is what we send back in an order.                   |
| `name`                           | For example `Large`, `General admission`.                                   |
| `price`, `currency`              | Base price in minor units.                                                  |
| `fee`                            | Any per-unit fee you add on top, so we can show the real price.             |
| `available`                      | A quantity, or `true` / `false` if you would rather not expose stock.       |
| `min_per_order`, `max_per_order` | Purchase limits, if you have them.                                          |
| `sales_end`                      | When the variant is no longer available for purchase, if it has a deadline. |

```json theme={null}
{
  "id": "prod_123",
  "name": "Colombian ground coffee",
  "description": "Medium roast, 16 oz bag.",
  "image_url": "https://shop.example.com/img/coffee.jpg",
  "url": "https://shop.example.com/products/colombian-ground",
  "status": "available",
  "variants": [
    { "id": "var_16oz", "name": "16 oz", "price": 2250, "currency": "usd", "fee": 0, "available": 40, "min_per_order": 1, "max_per_order": 10 }
  ]
}
```

### Events and tickets

If you sell events, expose them as events rather than products, so the agent can search by city and date and the user can see when and where it is.

```
GET /events?q=&city=&from=&to=&page=
GET /events/{id}
```

An event:

| Field                              | Notes                                          |
| ---------------------------------- | ---------------------------------------------- |
| `id`                               | Stable identifier.                             |
| `name`, `description`              | Shown to the user.                             |
| `starts_at`, `ends_at`, `timezone` | ISO 8601 times and the venue's IANA timezone.  |
| `venue`                            | `{ name, address, city }`.                     |
| `image_url`, `url`                 | Event image and the page on your site.         |
| `status`                           | `on_sale`, `sold_out` or `cancelled`.          |
| `ticket_types[]`                   | What the user picks. Same fields as a variant. |

A ticket type:

| Field                            | Notes                                                     |
| -------------------------------- | --------------------------------------------------------- |
| `id`                             | Stable identifier. This is what we send back in an order. |
| `name`                           | For example `General admission`, `VIP`.                   |
| `price`, `currency`              | Base price in minor units.                                |
| `fee`                            | Any per-ticket fee you add on top.                        |
| `available`                      | A quantity, or `true` / `false`.                          |
| `min_per_order`, `max_per_order` | Ticket limits.                                            |
| `sales_end`                      | When sales close for this ticket type.                    |

```json theme={null}
{
  "id": "evt_789",
  "name": "Sunset Sessions",
  "description": "Live set on the pier.",
  "starts_at": "2026-10-03T19:00:00-07:00",
  "ends_at": "2026-10-03T23:00:00-07:00",
  "timezone": "America/Los_Angeles",
  "venue": { "name": "Pier 70", "address": "420 22nd St", "city": "San Francisco" },
  "image_url": "https://tickets.example.com/img/sunset.jpg",
  "url": "https://tickets.example.com/events/sunset-sessions",
  "status": "on_sale",
  "ticket_types": [
    { "id": "tt_ga", "name": "General admission", "price": 4500, "currency": "usd", "fee": 300, "available": 120, "min_per_order": 1, "max_per_order": 6, "sales_end": "2026-10-03T18:00:00-07:00" }
  ]
}
```

If you also sell merchandise, keep `GET /products` alongside `GET /events`.

## 2. Orders

The user confirms an exact total before we place anything. So we need to quote first, then place, then read back the result.

```
POST /orders/quote
POST /orders
GET  /orders/{id}
POST /orders/{id}/cancel
```

### Quote

`POST /orders/quote` returns exact totals without committing anything. A `preview: true` flag on `POST /orders` works too.

Request:

| Field              | Notes                                                                                                        |
| ------------------ | ------------------------------------------------------------------------------------------------------------ |
| `items[]`          | `{ variant_id, quantity }`, or `{ ticket_type_id, quantity }` for events.                                    |
| `event_id`         | For events. The event the tickets belong to.                                                                 |
| `shipping_address` | For physical goods. `{ line1, line2, city, state, postal_code, country }`. Needed to price shipping and tax. |
| `buyer`            | Optional here. `{ first_name, last_name, email, phone }`.                                                    |

Response:

| Field        | Notes                                                                                                         |
| ------------ | ------------------------------------------------------------------------------------------------------------- |
| `subtotal`   | Items before fees and tax.                                                                                    |
| `fees`       | Service, delivery and shipping fees, all together.                                                            |
| `tax`        | Tax on the order.                                                                                             |
| `total`      | What the card will be charged. Must equal `subtotal + fees + tax`.                                            |
| `currency`   | ISO 4217, lowercase.                                                                                          |
| `quote_id`   | An id for this quote. We send it back on `POST /orders` so you can check the order matches what was approved. |
| `expires_at` | Optional. How long the quote holds.                                                                           |

```json theme={null}
{ "quote_id": "q_8f2c", "subtotal": 4500, "fees": 300, "tax": 394, "total": 5194, "currency": "usd", "expires_at": "2026-09-10T17:30:00Z" }
```

The total has to be exact and repeatable. We show it to the user, they confirm that number, and you must charge only that approved total. If the total changes between quote and order, we have to go back to the user, so include shipping, service fees and tax here rather than at order time.

### Place the order

`POST /orders` with an `Idempotency-Key` header.

Request:

| Field                        | Notes                                                                                                                          |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `items[]`                    | `{ variant_id, quantity }`, or `{ ticket_type_id, quantity }` for events.                                                      |
| `event_id`                   | For events.                                                                                                                    |
| `buyer`                      | `{ first_name, last_name, email, phone }`. Who the order and receipt go to.                                                    |
| `shipping_address`           | For physical goods. `{ line1, line2, city, state, postal_code, country }`.                                                     |
| `attendees[]`                | For tickets, when you need a name per ticket. `{ ticket_type_id, first_name, last_name, email }`.                              |
| `quote_id`                   | The quote the user approved.                                                                                                   |
| `expected_total`, `currency` | The total the user approved. If your recalculated total differs, do not charge: reject with `total_changed` and a fresh quote. |
| `payment`                    | See [Payment](#3-payment).                                                                                                     |

The example below shows the shared order fields. We agree the payment fields with you during integration.

```json theme={null}
{
  "quote_id": "q_8f2c",
  "expected_total": 5194,
  "currency": "usd",
  "items": [{ "variant_id": "var_16oz", "quantity": 2 }],
  "buyer": { "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "phone": "+14155550100" },
  "shipping_address": { "line1": "1900 Jefferson St", "city": "San Francisco", "state": "CA", "postal_code": "94123", "country": "US" }
}
```

Response:

| Field     | Notes                                                                                                                                                           |
| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`      | Your order id. We store it and use it for everything after this.                                                                                                |
| `status`  | `confirmed`, `pending` or `failed`. `pending` means you are still processing and we should poll `GET /orders/{id}`.                                             |
| `totals`  | `{ subtotal, fees, tax, total, currency }`, the same shape as the quote.                                                                                        |
| `items[]` | One per line. `{ id, variant_id, quantity, url }`. For tickets, `{ id, ticket_type_id, attendee, url }` where `url` is the ticket or QR code the user presents. |
| `url`     | The order page on your site, if you have one.                                                                                                                   |

```json theme={null}
{
  "id": "ord_456",
  "status": "confirmed",
  "totals": { "subtotal": 4500, "fees": 300, "tax": 394, "total": 5194, "currency": "usd" },
  "items": [
    { "id": "li_1", "variant_id": "var_16oz", "quantity": 2, "url": "https://shop.example.com/orders/ord_456" }
  ],
  "url": "https://shop.example.com/orders/ord_456"
}
```

Two things we lean on:

* Return the same order on retries. If we retry with the same `Idempotency-Key`, return the same order with its `id`, not an error. A retry after a dropped connection must never create a second order.
* **Specific failures.** If something cannot be bought, say which item and why. A generic `500` makes us ask the user to try again when the answer is really "sold out".

Error response:

| Field     | Notes                                                                                                                          |
| --------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `error`   | `items_unavailable`, `total_changed`, `invalid_address`, `payment_declined`, `limit_exceeded`, or your own code.               |
| `message` | Human-readable. We may show it to the user.                                                                                    |
| `items[]` | For `items_unavailable`, the `variant_id` or `ticket_type_id` values that could not be bought.                                 |
| `quote`   | For `total_changed`, a fresh quote in the same shape as `POST /orders/quote`. We take it back to the user before trying again. |

```json theme={null}
{ "error": "items_unavailable", "message": "16 oz is sold out.", "items": ["var_16oz"] }
```

```json theme={null}
{ "error": "total_changed", "message": "Shipping went up.", "quote": { "quote_id": "q_9a11", "subtotal": 4500, "fees": 600, "tax": 394, "total": 5494, "currency": "usd" } }
```

### Read the order

`GET /orders/{id}`. We poll this after placing to confirm the order went through, and to tell the user when something ships or is ready.

Response:

| Field                                      | Notes                                                                                                            |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `id`, `status`, `totals`, `items[]`, `url` | Same as the place response.                                                                                      |
| `status`                                   | `confirmed`, `pending`, `failed`, `cancelled`, plus fulfillment states if you have them: `shipped`, `delivered`. |
| `refund`                                   | `{ amount, currency, status }` when any refund has been issued. `status` is `pending` or `completed`.            |
| `tracking`                                 | Optional. `{ carrier, number, url, estimated_delivery }` once a package ships.                                   |

```json theme={null}
{
  "id": "ord_456",
  "status": "shipped",
  "totals": { "subtotal": 4500, "fees": 300, "tax": 394, "total": 5194, "currency": "usd" },
  "items": [{ "id": "li_1", "variant_id": "var_16oz", "quantity": 2 }],
  "refund": null,
  "tracking": { "carrier": "USPS", "number": "9400111899223", "url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223", "estimated_delivery": "2026-09-14" }
}
```

### Cancel

`POST /orders/{id}/cancel`. Optional. Cancels and refunds whatever your policy allows. If you cannot cancel through the API, tell us and we send users to your support flow instead.

Request: no body.

Response:

| Field          | Notes                                                                                              |
| -------------- | -------------------------------------------------------------------------------------------------- |
| `id`, `status` | The order, now `cancelled`.                                                                        |
| `refund`       | `{ amount, currency, status }`. `amount` is what is coming back, which may be less than the total. |

```json theme={null}
{ "id": "ord_456", "status": "cancelled", "refund": { "amount": 4800, "currency": "usd", "status": "pending" } }
```

### Webhooks

Optional. Share your existing webhook and signature documentation so we can plan how order updates reach Agentcard.

| Field         | Notes                                                                        |
| ------------- | ---------------------------------------------------------------------------- |
| `id`          | Unique per delivery. Retries reuse the same `id`, so we can drop duplicates. |
| `type`        | `order.confirmed`, `order.cancelled` or `order.refunded`.                    |
| `order_id`    | The order this is about.                                                     |
| `occurred_at` | ISO 8601.                                                                    |
| `order`       | The same object `GET /orders/{id}` returns, so we never need a second call.  |

```json theme={null}
{
  "id": "evt_01j9x",
  "type": "order.refunded",
  "order_id": "ord_456",
  "occurred_at": "2026-09-10T17:04:12Z",
  "order": { "id": "ord_456", "status": "cancelled", "refund": { "amount": 4800, "currency": "usd", "status": "completed" } }
}
```

We agree the receiving URL, signature verification, timestamp tolerance and retry policy with you during integration.

## 3. Payment

We pay each order through your own payment provider, and you charge it like any other order. Tell us which provider you use and we handle the rest on our side.

Providers we support today:

* Stripe
* Shopify Payments
* Adyen
* Braintree
* Square
* Recurly
* Razorpay

If yours is not on the list, tell us anyway and we will let you know what it takes.

## Checklist before you email us

* Catalog search and product detail, with stable ids and prices in minor units.
* A quote that returns the exact total including fees, tax and shipping, with a `quote_id`.
* Order create checks `quote_id` and `expected_total`, and rejects with `total_changed` instead of charging a different amount.
* Order create with `Idempotency-Key`, returning the order id on duplicates.
* Order read, with status.
* Order create charges exactly the quoted total through your payment provider.

Send the base URL, your API docs or the endpoints above, how to get a staging key, your payment provider, and a couple of test items to [founders@agentcard.sh](mailto:founders@agentcard.sh). A first test order is usually placed within a few days.
