Skip to main content
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.
server.ts
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 needs no endpoint.

Create the handler

The function throws a TypeError when secret is empty, when onMessage is not a function, or when agentcard is not an AgentcardClient. Pass agentcard so the model reads each message through turn.messages(history). The Taking the next step guide shows what to do with a message whose advances is true.

The handler

AgentcardWebhookHandler is a function with a fetch member.
app/agentcard/webhooks/route.ts
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:
server.ts
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

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. The Webhooks overview describes the envelope, and Order events describes the order events.

purchaseAgentWebhook

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.

Verify a delivery

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>.
verify.ts
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. 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.

Read a message

readShopperMessage

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

Returns the message a purchase.message event carries, or null for any other event type. A PurchaseMessage has the fields of a ShopperMessage except type, conversationId and advances. The function throws a TypeError under the same condition as readShopperMessage.

isStale

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.