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

# Buy through a merchant agent

> Use MCP or A2A with existing customer sessions to prepare and pay for merchant offers.

A published customer deployment can expose any configured merchant agent over
[A2A 1.0](https://a2a-protocol.org/latest/specification/). It uses the same agent
and customer capabilities as storefront chat. No marketplace is required for
native direct purchases. The existing [UCP checkout API](https://docs.omnicommerce.ai/api-reference.md) remains
available to authorized commerce integrations.

## Prerequisites

The merchant must publish an active customer deployment, enable direct checkout,
and configure currency, delivery countries, shipping amount, tax terms and a
payment destination. Shopify-listed products use Shopify checkout and return a
supported checkout handoff instead of a native offer.

## Connect a custom buyer client with MCP

For clients that manage a private customer-session bearer token, use:

```text theme={null}
https://your-running-build/api/embed/{deploymentId}/mcp
```

Create a customer session as described below, then send its token in the MCP
client's `Authorization: Bearer ...` header. Use the same allowed `Origin` when
sending browser requests. Token expiry, deployment configuration checks,
storefront protection and buyer ownership are the same as customer HTTP and A2A.
Keep tokens in client credential storage, outside model messages. Preserve the
session to read its offers and orders later.

The endpoint uses the official MCP SDK with stateless Streamable HTTP. The
existing `/api/mcp` endpoint remains the public read-only catalog interface.

| Tool | Behavior |
| - | - |
| `get_merchant_capabilities` | Current organization checkout availability |
| `search_merchant_catalog` | This merchant's public catalog and structured product cards |
| `prepare_purchase_offers` | Up to five offers with stable `requestId` values |
| `get_purchase_offers` | Up to 20 deduplicated `sessionIds`, including payment/order status |
| `cancel_purchase_offers` | Up to 20 owned unpaid offers |
| `start_purchase_checkouts` | Up to five approved Stripe hosted checkout operations |

When checkout is disabled, new-offer and payment tools are absent. Execution
also rechecks availability. Owned status reads and cancellation remain available
for existing offers.

After the buyer approves an offer's exact cart version, total and currency, call:

```json theme={null}
{
  "payments": [
    {
      "sessionId": "acs_example",
      "approval": { "cartVersion": 1, "totalAmount": 15.81, "currency": "SGD" }
    }
  ]
}
```

The result includes ordered per-offer results and explicit success/failure counts.
It returns hosted checkout URLs; the buyer enters card details on Stripe. The
MCP endpoint never accepts payment credentials. Confirm payment using
`get_purchase_offers`; a checkout URL or browser return does not prove payment.

Run a read-only MCP smoke test against your running build:

```bash theme={null}
A2A_BASE_URL=https://your-running-build \
A2A_DEPLOYMENT_ID=<published-customer-deployment-id> \
A2A_ORIGIN=https://your-allowed-client-origin \
yarn tsx scripts/a2a/mcp-smoke-test.ts
```

This uses the official MCP client to establish a customer session, list tools,
read capabilities and search one catalog result. It creates no offers or payments
and prints no credentials. Native platform account linking is outside this
customer-session integration.

## Discover and establish a customer session

### Run an external smoke test

Against a running build containing these endpoints, from the repository root:

```bash theme={null}
A2A_BASE_URL=https://your-running-build \
A2A_DEPLOYMENT_ID=<published-customer-deployment-uuid> \
yarn tsx scripts/a2a/smoke-test.ts
```

This standalone HTTP client uses `@a2a-js/sdk` 1.3.0 with A2A 1.0. It discovers
the card, obtains a customer session, sends a catalog request and checks the task
for up to two minutes. It prints the task state and artifacts without printing
the customer token. Its default request does not ask for an offer or payment.
Pushing a PR does not make these endpoints available on a deployed application.

Set `A2A_ORIGIN` to a merchant-allowed buyer origin when needed; otherwise the
application origin is used. If bot verification is enabled, supply a fresh
`A2A_VERIFICATION_TOKEN` obtained through the merchant's existing browser
verification flow. The script does not bypass verification.

To test offer preparation, set `A2A_MESSAGE` to your product request, delivery
country, and buyer details, and ask the merchant to prepare an offer. This may
reserve real stock, so use a dedicated test merchant and cancel unwanted offers.
Look for a `purchase-offers` artifact containing ordered batch tool results.
The command never calls `pay` or `complete` automatically.
It keeps the customer token in memory, so any offers it prepares expire normally
after it exits. For explicit cancellation or payment, use your own client below
and retain its customer session.

### Use your own buyer agent

Fetch `/api/embed/{deploymentId}/.well-known/agent-card.json`. The card advertises
the A2A JSON-RPC endpoint and customer bearer authentication. It excludes private
prompts and model settings.

Check whether a skill has the `checkout` tag before requesting a new offer.
When the organization disables direct checkout, the card advertises catalog
discovery only and its agents cannot prepare new offers. Existing offers can
still be read or cancelled; their customer HTTP responses include the current
`checkoutEnabled` boolean. A2A task metadata includes the same boolean and only
advertises creation/payment endpoints while checkout is enabled. The server
rechecks the setting before creating offers or starting payment.

Create a session with `POST /api/embed/{deploymentId}/session`, an `Origin` header,
and `{}` as the JSON body. Use a merchant-allowed origin or the application's
own public origin. Keep the returned `token` and `sessionId`; send the same
`Origin` and `Authorization: Bearer <token>` on subsequent requests. Renew the
owned session using the existing session endpoint and its previous token.

```ts theme={null}
import { ClientFactory, JsonRpcTransportFactory } from "@a2a-js/sdk/client";
import { AgentCard, Message, SendMessageRequest } from "@a2a-js/sdk";

const headers = { Origin: origin, Authorization: `Bearer ${token}` };
const factory = new ClientFactory({
  transports: [
    new JsonRpcTransportFactory({
      fetchImpl: (url, init) =>
        fetch(url, {
          ...init,
          headers: {
            ...Object.fromEntries(new Headers(init?.headers)),
            ...headers,
          },
        }),
    }),
  ],
});
const card = AgentCard.fromJSON(await (await fetch(cardUrl)).json());
const checkoutEnabled = card.skills.some((skill) =>
  skill.tags.includes("checkout"),
);
const client = await factory.createFromAgentCard(card);
const task = await client.sendMessage(
  SendMessageRequest.fromJSON({
    message: Message.toJSON(
      Message.fromJSON({
        messageId: crypto.randomUUID(),
        role: "ROLE_USER",
        contextId: sessionId,
        parts: [
          {
            text: checkoutEnabled
              ? "Find two suitable items, deliver to SG, and prepare an offer."
              : "Find two suitable items and show their purchase links.",
          },
        ],
      }),
    ),
    configuration: { returnImmediately: true },
  }),
);
```

Text user messages accept up to 4,000 characters. Retain the UUID `messageId` for
retrying the same message. Each new message creates a task in the owned customer
context; use A2A GetTask to retrieve its state. Completed tasks expose model text
and structured purchase-offer artifacts when the agent used offer tools. The agent
asks any follow-up question in its reply text; answer it with your next message.
Streaming and push notifications are not advertised. Merchant bot-verification and rate
limits also apply to external agents; supply `X-Storefront-Verification` when
verification is enabled.

### Identify your buyer agent

Buyer agents may sign their requests with
[Web Bot Auth](https://datatracker.ietf.org/doc/draft-meunier-webbotauth-httpsig-protocol/)
HTTP Message Signatures. Send `Signature`, `Signature-Input` (tag `web-bot-auth`,
covering `@authority` and your `Signature-Agent` member) and
`Signature-Agent: sig1="https://your-agent.example"`. Publish your Ed25519 or
RSA-PSS/SHA-512 public keys at
`https://your-agent.example/.well-known/http-message-signatures-directory`.
Signatures must be created within the last five minutes.

The verified agent origin is recorded on each offer and order created by that
request, so the merchant can see which agent made the purchase. Unsigned
requests still work and are treated as anonymous. A signature that fails
verification returns 401. Signing identifies your agent; it does not bypass
merchant bot verification or rate limits.

## Prepare and inspect offers

The agent can use `prepare_purchase_offers`, `get_purchase_offers`, and
`cancel_purchase_offers`. Prices and delivered totals are calculated by the
merchant service. Proposed discounts must satisfy the configured agent's
existing pricing and margin policies. Exact purchasable variant IDs are required
when a product has active variants.

Buyer software may also use
`POST /api/embed/{deploymentId}/checkout-sessions/{operation}` with the same
customer session. `create` accepts `offers[]` with stable request IDs; `get` and
`cancel` accept `sessionIds[]`. See the generated API schema for bounds and fields.
Batches return ordered results with aggregate success and failure counts. Offers
are owned by the customer, expire after 30 minutes, and hold inventory. A customer
may hold at most ten unpaid offers. Cancel unwanted offers to release stock.

### Verify signed offers

When the platform has a signing key configured (`UCP_SIGNING_PRIVATE_JWK`, a
P-256 private JWK with a `kid`), each payable offer returned over A2A, MCP or the
checkout-sessions API includes `checkoutJwt`. It is an ES256 JWT of the offer
terms: `merchant`, `cart_version`, UCP-style `line_items` and `totals` in minor
units, `delivery_terms` and `expires_at`. Its `exp` is the offer expiry.
Storefront chat does not receive it.

Verify it with the key named by its `kid` in `signing_keys` at
`/.well-known/ucp` on the same host:

```ts theme={null}
import { importJWK, jwtVerify, decodeProtectedHeader } from "jose";

const { signing_keys } = await (
  await fetch(`${baseUrl}/.well-known/ucp`)
).json();
const { kid } = decodeProtectedHeader(offer.checkoutJwt);
const key = signing_keys.find((candidate) => candidate.kid === kid);
const { payload } = await jwtVerify(
  offer.checkoutJwt,
  await importJWK(key, "ES256"),
  {
    issuer: baseUrl,
    subject: offer.sessionId,
  },
);
```

The token proves which terms the merchant offered; it does not replace buyer
approval. An AP2 shopping agent can use it as the `checkout_jwt` of a closed
Checkout Mandate. The merchant does not yet accept AP2 mandates at completion.

## Approve and complete a purchase

### Card Pay button and external checkout handoff

For an offer whose `paymentRequirements.details.cardCheckoutAvailable` is true,
storefront chat displays a **Pay** button. External buyer agents can request the
same hosted card page after obtaining buyer approval:

```ts theme={null}
const response = await fetch(
  `${baseUrl}/api/embed/${deploymentId}/checkout-sessions/pay`,
  {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      sessionId: offer.sessionId,
      approval: {
        cartVersion: offer.cartVersion,
        totalAmount: offer.totalAmount,
        currency: offer.currency,
      },
    }),
  },
);
if (!response.ok) throw new Error("Card checkout could not be opened.");
const { checkoutUrl } = await response.json();
// Present checkoutUrl to the buyer for card entry on Stripe's hosted page.
```

The page uses the organization's **seller Stripe key** from Company Settings →
Agentic Checkout. An OmniCommerce subscription Stripe customer is not a seller
payment connection. Use a seller sandbox/test key for testing; a live key can
take real payments. The key must permit Checkout, PaymentIntent reads and webhook
endpoint creation. A public HTTPS application URL and the registered Inngest
functions are required. This is not usable against an unexposed localhost URL.

The first payment registers a signed webhook in that seller's Stripe account.
The hosted page collects the buyer's delivery address, limited to the country
already quoted, and phone number. The delivered total and currency remain fixed.
The full address is copied to the native order only after server verification of
the paid Stripe receipt. Card data never enters chat or A2A messages.

Repeated `pay` calls reuse the same hosted checkout for that offer version.
The payment tab returning to the application is not proof of payment. Signed
webhooks trigger the existing durable payment/order service; the same service
checks abandoned checkouts once at a 31-minute deadline. An open page is expired
before stock is released. Unconfirmed provider failures keep the reservation
pending for reconciliation. Refresh the offer using `get` to obtain its confirmed
state and order ID. Storefront cards refresh when the conversation regains focus
and offer a manual **Refresh status** button.

### Agent-native payment proofs

Inspect the offer's items, `deliveryTerms`, `paymentRequirements`, delivered total,
currency, expiry and `cartVersion`. Obtain buyer authorization and the payment
rail's proof. Send the proof directly to the completion endpoint; keep it out of
agent messages and tool arguments.

```ts theme={null}
await fetch(`${baseUrl}/api/embed/${deploymentId}/checkout-sessions/complete`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    sessionId: offer.sessionId,
    idempotencyKey: paymentRequestId,
    approval: {
      cartVersion: offer.cartVersion,
      totalAmount: offer.totalAmount,
      currency: offer.currency,
    },
    paymentProof, // ACP shared payment token or configured x402 proof
  }),
});
```

Retain `paymentRequestId` for completion retries. A stale approval is rejected.
Successful completion returns the native order ID. Definitive payment failures
return 402. Pending confirmation freezes the offer and is recovered durably;
inspect its state before initiating any further payment. Indeterminate x402
confirmation returns `payment_review_required` and requires merchant
reconciliation. It does not establish a successful purchase.

## Delivery configuration and fulfillment

Company Settings → Agentic Checkout configures one organization-wide currency,
allowed destination countries, flat shipping charge, tax percentage or
tax-inclusive pricing, and a delivery description. Offers snapshot these terms;
the same calculation is used by A2A and storefront chat. There are no carrier,
postcode, weight, or service-level rate rules. A delivery description is a
merchant-provided statement, not a carrier ETA or a scheduled delivery.

After verified payment, checkout creates a native `agentic` order in
`ready_to_ship`, with items, contact details, delivery address and shipping fee,
and commits the reserved stock. Agent-native proof callers must provide a usable
full shipping address when preparing the offer; the quote currently validates
the country only. Hosted card checkout additionally collects the full address.

Dispatch and delivery remain merchant-managed. The existing marketplace shipment
actions require a supported marketplace and store connection, so they do not
book shipments for native agentic orders. No fulfillment provider is integrated
by this flow: carrier quotes, courier booking, labels, tracking events, delivery
notifications and automated refund settlement remain outside its scope. A
completed checkout confirms purchase, not delivery.


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