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

# Agentic Commerce

> Let AI agents discover and sell Gomry events through the Agentic Commerce Protocol

# Agentic Commerce

Gomry implements the [Agentic Commerce Protocol](https://developers.openai.com/commerce) (ACP), the open standard maintained by OpenAI and Stripe for letting AI agents discover products and complete purchases on a merchant's own stack.

There are two halves, and they are independent:

<CardGroup cols={2}>
  <Card title="Catalog" icon="magnifying-glass" href="/api-reference/catalog/list-catalog-events">
    A cross-organization, read-only feed of live public events. This is how an agent finds out an event exists.
  </Card>

  <Card title="Checkout" icon="credit-card" href="/api-reference/checkout-sessions/create-checkout-session">
    ACP checkout sessions. This is how an agent sells a ticket without sending the buyer to a browser.
  </Card>
</CardGroup>

A correct checkout is invisible without a catalog, and a catalog with no checkout still drives buyers to your event page. Most integrations start with the catalog.

<Note>
  **Can't hold a payment token?** Most agents can't — minting one needs a browser and a Stripe agent account. Every ticket type in the catalog carries a `checkout_url` that opens Gomry checkout with that tier preselected, so your agent can do the discovery and hand the buyer a link. No onboarding, no signing secret, works today. See [Buying without a payment token](/api-reference/catalog/get-catalog-event#buying-without-a-payment-token).
</Note>

## What makes this surface different

Every other resource in this API resolves your key to exactly one organization and reads inside it. The commerce surface does not:

* **Catalog is cross-organization.** One key returns live public events from every organizer on Gomry.
* **Checkout sells on another organizer's behalf** and takes payment against their Stripe account.

Because of that, both are off by default on every key, and checkout carries additional gates described below.

## The purchase flow

<Steps>
  <Step title="Discover">
    `GET /v1/catalog/events` to page the catalog, then `GET /v1/catalog/events/{event_id}` for the one you intend to sell. The detail endpoint is the only place `ticket_types` and `registration_questions` are populated.
  </Step>

  <Step title="Create a session">
    `POST /v1/checkout_sessions` with `items[].id` set to a **ticket type id** from `ticket_types[].id`. The session comes back with line items, totals, and a `status`.
  </Step>

  <Step title="Resolve blockers">
    While `status` is `not_ready_for_payment`, read `messages[]`. Each `error` message says what is missing — a buyer name, a required registration answer, an out-of-stock line. Patch the session with `POST /v1/checkout_sessions/{id}` until `status` is `ready_for_payment`.
  </Step>

  <Step title="Complete">
    `POST /v1/checkout_sessions/{id}/complete` with a delegated payment token. On success the session reports `completed` and carries an `order` with a `permalink_url` your buyer can open.
  </Step>
</Steps>

## Payment is token-only

<Warning>
  **No endpoint on this API accepts a card number, and none ever will.** Payment reaches Gomry only as a delegated token — a Stripe Shared Payment Token in `payment_data.token`.
</Warning>

This is a hard architectural boundary, not a current limitation. Gomry has no raw-PAN code path; every paid flow is Stripe-hosted. Request bodies are validated strictly, so a body carrying an unexpected field such as `card_number` is **rejected with a `400`** rather than silently ignored.

Practically, this means an agent platform must be able to mint a delegated payment token. An agent that holds only a raw card number cannot complete a Gomry checkout, and should send the buyer to the event `url` from the catalog instead.

## Ids: what to pass where

The single most common integration error is passing an **event id** where a **ticket type id** belongs.

| You have           | Where it comes from                           | Where it goes                                   |
| ------------------ | --------------------------------------------- | ----------------------------------------------- |
| Event id           | `data[].id` on the catalog list               | The path of `GET /v1/catalog/events/{event_id}` |
| **Ticket type id** | `ticket_types[].id` on the catalog **detail** | **`items[].id` when creating a session**        |
| Question id        | `registration_questions[].id` on the detail   | `attendee_answers[].question_id`                |

Passing an event id as `items[].id` returns `item_not_found`.

## Versioning

Send an `API-Version` header on every checkout request. This deployment implements:

```
API-Version: 2025-09-12
```

A header naming a **different** version is refused with `unsupported_api_version` rather than served best-effort, so you never ship against fields we do not send.

An **absent** header is accepted and treated as the current version. The ACP spec lists it as required and we recommend always sending it — but with exactly one version in service, refusing a partner mid-integration over a missing header helps nobody. Do not rely on that leniency: send the header, so the day a second version exists your client keeps getting the one it was built against.

The catalog endpoints ignore this header entirely.

## Access

<Note>
  The commerce surface is granted per partner. Contact [support@gomry.com](mailto:support@gomry.com) to be onboarded.
</Note>

**Catalog** needs `catalog: read` on your API key.

**Checkout** needs three independent things, all of which must be true:

1. `checkout: write` on the key.
2. The key id added to Gomry's ACP partner allowlist — a deploy-time change, not a dashboard toggle.
3. A **signing secret** issued to you, used to sign every request with a body.

The second and third exist because a scope can be granted by anyone with settings access, and a surface that sells arbitrary organizers' tickets needs a gate that a misconfigured dashboard cannot open. See [Scopes](/scopes).

## Request signing

Every checkout request **with a body** (`create`, `update`, `complete`) must carry two headers:

<ParamField header="Signature" type="string" required>
  Base64 HMAC-SHA256 over `{timestamp}.{raw_body}`, using your signing secret.
</ParamField>

<ParamField header="Timestamp" type="string" required>
  RFC 3339. Must be within **5 minutes** of Gomry's clock, or the request is refused.
</ParamField>

The timestamp and body are signed together so a captured signature cannot be replayed with a fresh timestamp.

<Warning>
  Sign the **raw body bytes you actually send**. `JSON.parse` followed by `JSON.stringify` does not round-trip — key order, whitespace and number formatting all change — so signing a re-serialized object produces a signature that fails on a request that is otherwise perfectly authentic. Build the body string once, sign that string, and send that string.
</Warning>

`GET` and `cancel` carry no body and need no signature. They still require an allowlisted key, and should still send `API-Version`.

```javascript Signing a request theme={null}
import crypto from "crypto";

const body = JSON.stringify({ items: [{ id: ticketTypeId, quantity: 2 }] });
const timestamp = new Date().toISOString();

const signature = crypto
  .createHmac("sha256", process.env.GOMRY_ACP_SIGNING_SECRET)
  .update(`${timestamp}.${body}`)
  .digest("base64");

await fetch("https://www.gomry.com/api/v1/checkout_sessions", {
  method: "POST",
  headers: {
    "X-API-KEY": process.env.GOMRY_API_KEY,
    "Content-Type": "application/json",
    "API-Version": "2025-09-12",
    Timestamp: timestamp,
    Signature: signature,
    "Idempotency-Key": crypto.randomUUID(),
  },
  body, // the same string that was signed
});
```

Secrets are rotated additively: during a cutover both the old and new secret verify, and the old one is retired once you confirm.

## Headers

| Header            | Applies to      | Notes                                                                                              |
| ----------------- | --------------- | -------------------------------------------------------------------------------------------------- |
| `X-API-KEY`       | All             | Your Gomry API key.                                                                                |
| `API-Version`     | Checkout        | `2025-09-12`. Always send it; a *different* value is refused, an absent one is treated as current. |
| `Signature`       | Checkout writes | Required. See above.                                                                               |
| `Timestamp`       | Checkout writes | Required. RFC 3339, ±5 min.                                                                        |
| `Idempotency-Key` | Checkout writes | Recommended. Echoed back on the response.                                                          |
| `Request-Id`      | Checkout        | Optional. Echoed back, including on errors, so you can correlate a failure in your own logs.       |

Checkout responses are always `Cache-Control: no-store` — they are per-buyer and must never be shared by a cache.

## Errors

Checkout uses the ACP error envelope, which is **different from the rest of this API**:

```json theme={null}
{
  "type": "invalid_request",
  "code": "item_not_found",
  "message": "No ticket type matches the requested id.",
  "param": "items[0].id"
}
```

`type` is one of `invalid_request`, `processing_error`, or `service_unavailable`. See [Checkout Errors](/commerce/errors) for every code and its status.

The catalog endpoints use the [standard Gomry error format](/errors).
