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

# Webhook handler

> Verifies each delivery to your webhook endpoint, skips events it already handled, and hands your code a message ready to post into the shopper's chat.

`agentcardWebhook(options)` builds the handler for your webhook endpoint. The handler checks each delivery's signature, skips an event it already handled, reads the message the event carries for a shopper, drops a message whose link or window has closed, and calls `onMessage` with your own id for the shopper. The handler answers Agentcard when your callback is done, so a failed post is delivered again.

```ts server.ts theme={null}
import express from 'express';
import { agentcardWebhook } from '@agent-cards/commerce-sdk';

const app = express();

app.post(
  '/agentcard/webhooks',
  express.raw({ type: 'application/json' }), // before any express.json()
  agentcardWebhook({
    secret: process.env.AGENTCARD_WEBHOOK_SECRET ?? '', // returned once, when you created the endpoint
    onMessage: (shopperId, message) => postToChat(shopperId, message.text),
  }),
);
```

`postToChat` stands for your own code. `shopperId` is the `externalUserId` you gave the buy tool, so the message lands in the chat the purchase started in. Use the handler on a host where no process keeps running. On a server that keeps running, [`listen`](https://docs.agentcard.com/api-reference/commerce-sdk/listen) needs no endpoint.

## Create the handler

```ts theme={null}
agentcardWebhook(options: AgentcardWebhookOptions): AgentcardWebhookHandler
```

The function throws a `TypeError` when `secret` is empty, when `onMessage` is not a function, or when `agentcard` is not an `AgentcardClient`.

| Option | Type | Description |
| - | - | - |
| `secret` | `string`, required | The secret Agentcard returned once, when you created the endpoint. |
| `onMessage` | `(externalUserId, message, event) => void` or a promise, required | Receives each message for one of your shoppers. Runs once per event, never for a stale message. |
| `onEvent` | `(event: AgentcardEvent) => void` or a promise, optional | Receives every verified event that is no message for one of your shoppers: another event type, and a shopper message for a shopper you gave Agentcard no id for. |
| `agentcard` | `AgentcardClient`, optional | The client your buy tools share. Once `onMessage` returns, the client keeps the message for that shopper's next `buy.turn`, as `listen` does. |
| `onError` | `(error, event) => void` or a promise, optional | Receives what went wrong. `event` is `undefined` when no event was read. Default: `console.error`. A reporter that throws is ignored. |
| `toleranceSeconds` | `number`, optional | How far the signed timestamp may sit from now. Default `300`. |

Pass `agentcard` so the model reads each message through `turn.messages(history)`. The [Taking the next step](https://docs.agentcard.com/commerce-sdk/guides/taking-the-next-step.md) guide shows what to do with a message whose `advances` is `true`.

## The handler

`AgentcardWebhookHandler` is a function with a `fetch` member.

| Form | Use it for |
| - | - |
| `handler(req, res)` | A Node or Express `(req, res)` route. |
| `handler.fetch(request)` | A route that takes a web `Request`, such as a Next.js route handler. Returns a `Response`. |

```ts app/agentcard/webhooks/route.ts theme={null}
import { agentcardWebhook } from '@agent-cards/commerce-sdk';

const webhook = agentcardWebhook({
  secret: process.env.AGENTCARD_WEBHOOK_SECRET ?? '',
  onMessage: (shopperId, message) => postToChat(shopperId, message.text),
});

export const POST = webhook.fetch;
```

The route exports the handler's `fetch`, so a Next.js route handler verifies and answers each delivery.

### Raw body

The signature covers the exact bytes Agentcard sent, and a JSON parser that runs first throws those bytes away. Mount the route with `express.raw` before any `app.use(express.json())`. If a JSON parser has to run first, keep the bytes for the handler:

```ts server.ts theme={null}
app.use(express.json({ verify: (req, _res, buf) => { Object.assign(req, { rawBody: buf }); } }));
```

The handler reads the bytes from `req.rawBody`, from a string or bytes in `req.body`, or from the unread request stream. Without the raw bytes, the handler answers `500` and `onError` receives the fix.

## What each delivery answers

| Status | Body | When |
| - | - | - |
| `200` | `{ "received": true }` | The callback returned. The same status answers a duplicate, a stale message, and a purchase message the package cannot read, because a second delivery would read the same. |
| `400` | `{ "error": "invalid_signature", "reason": "<reason>" }` | The signature is missing, malformed, stale or wrong. `reason` is one of the values in [Signature reasons](#verify-a-delivery). Your code never sees the delivery. |
| `400` | `{ "error": "invalid_body" }` | The body is signed correctly but is not an Agentcard event. |
| `400` | `{ "error": "unreadable_body" }` | The handler could not read the request body. |
| `413` | `{ "error": "body_too_large" }` | The body is larger than 1 MiB. |
| `500` | `{ "error": "callback_failed" }` | `onMessage` or `onEvent` threw. Agentcard delivers the event again. |
| `500` | `{ "error": "raw_body_unavailable" }` | A JSON parser consumed the body and kept no raw bytes. |
| `500` | `{ "error": "verification_failed" }` | The check failed for a reason other than the signature or the body. |
| `500` | `{ "error": "handler_failed" }` | The `fetch` handler failed unexpectedly. |

Agentcard waits 10 seconds for the answer, so keep `onMessage` short: post the text, or queue the agent turn, then return. Agentcard delivers each event at least once, and again after any answer other than a `2xx`.

The handler remembers the last 10,000 events it finished, so a redelivery of one runs nothing. A second delivery that lands while the first still runs waits for its answer. That memory belongs to one process: when more than one server takes deliveries, also skip a `message.eventId` you already stored.

## Subscribe the endpoint

Subscribe your endpoint to each shopper event by name. A catch-all `*` alone does not turn `purchase.message` on. `SHOPPER_EVENT_TYPES` holds the three names: `purchase.message`, `order.confirmed` and `order.updated`.

Create the endpoint with [Create a webhook endpoint](https://docs.agentcard.com/api-reference/webhook-endpoints/create.md). The [Webhooks overview](https://docs.agentcard.com/webhooks/overview.md) describes the envelope, and [Order events](https://docs.agentcard.com/webhooks/orders/overview.md) describes the order events.

## `purchaseAgentWebhook`

```ts theme={null}
purchaseAgentWebhook(
  secret: string,
  onMessage: (message: PurchaseMessage, event: AgentcardEvent) => void,
  opts?: PurchaseAgentWebhookOptions,
): (req: WebhookRequest, res: WebhookResponse) => Promise<void>
```

The handler from earlier versions hands you `purchase.message` events only and leaves skipping repeated events to you. New code takes `agentcardWebhook`. An empty `secret` throws when the server starts.

| Option | Type | Description |
| - | - | - |
| `onEvent` | `(event: AgentcardEvent) => void` or a promise, optional | Receives every verified event that is not a purchase message, such as `merchant.connected`. |
| `onError` | `(error, event) => void` or a promise, optional | Receives an error thrown by `onMessage` or `onEvent`, a purchase message the package cannot read, or a request whose raw body was already gone. |
| `toleranceSeconds` | `number`, optional | The signature tolerance. Default `300`. |

## Verify a delivery

```ts theme={null}
verifyAgentcardWebhook(
  rawBody: string | Uint8Array,
  signatureHeader: string | undefined,
  secret: string,
  opts?: { toleranceSeconds?: number; now?: number },
): AgentcardEvent
```

Checks one delivery and returns its event. Pass the raw request body, the `AgentCard-Signature` header and the endpoint's secret. The function reads nothing in the body before the signature holds. The header reads `t=<unix seconds>,v1=<64 hex characters>`.

```ts verify.ts theme={null}
import { verifyAgentcardWebhook, readShopperMessage } from '@agent-cards/commerce-sdk';

// Node types a header as a string or a list of strings; the check takes one string.
const signature = req.headers['agentcard-signature'];
const event = verifyAgentcardWebhook(rawBody, Array.isArray(signature) ? signature.join(',') : signature, secret);
const message = readShopperMessage(event); // null for an event that carries no message
```

The call returns the verified event, and `message` holds the shopper message or `null`.

The function throws `AgentcardSignatureError` when the delivery did not come from Agentcard. `error.reason` says why.

| `reason` | What it means |
| - | - |
| `missing_header` | The request has no `AgentCard-Signature` header. |
| `malformed_header` | The header does not read `t=<unix seconds>,v1=<64 hex characters>`. |
| `wrong_signature` | The signature does not match this body under the secret you passed. Pass the secret of the endpoint that received the delivery, and the raw request bytes. |
| `stale_timestamp` | Agentcard signed the delivery more than `toleranceSeconds` away from now. Default 300 seconds. |

A correctly signed body that is not an Agentcard event throws a `TypeError`. Empty `secret` and a body that is neither a string nor bytes throw a `TypeError` too.

### AgentcardEvent

The envelope every event arrives in. Delivery is at least once: deduplicate on `id`.

| Field | Type | Description |
| - | - | - |
| `id` | `string` | Stable event id, such as `evt_9f8e7d6c5b4a`. The same on every redelivery. |
| `type` | `string` | The event type. |
| `created` | `number` | Unix seconds. |
| `livemode` | `boolean` | `false` for a sandbox event. |
| `data` | `Record<string, unknown>` | The event's payload. |

## Read a message

### `readShopperMessage`

```ts theme={null}
readShopperMessage(event: AgentcardEvent): ShopperMessage | null
```

Returns the message an event carries for a shopper, or `null` when it carries none: an event type that is not a shopper event, an order event that names no shopper, or a delivery stage the package does not know and the store gave no line for. Pass the event `verifyAgentcardWebhook` returned. The function throws a `TypeError` when a `purchase.message` lacks its `kind`, `user_id` or `text`.

### `readPurchaseMessage`

```ts theme={null}
readPurchaseMessage(event: AgentcardEvent): PurchaseMessage | null
```

Returns the message a `purchase.message` event carries, or `null` for any other event type. A `PurchaseMessage` has the fields of a [`ShopperMessage`](https://docs.agentcard.com/api-reference/commerce-sdk/messages) except `type`, `conversationId` and `advances`. The function throws a `TypeError` under the same condition as `readShopperMessage`.

### `isStale`

```ts theme={null}
isStale(message: Pick<ShopperMessage, 'facts'>, now?: number): boolean
```

Returns `true` when the message stopped holding before `now`, in Unix milliseconds. The default is the system clock. A message stops holding when the link or window its text names has closed, as `facts.validUntil` says. Drop a stale message instead of sending it.


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