Delivery
Webhooks
Receive signed payment events, return 2xx only after durable acceptance, and use delivery ids for idempotency.
Delivery model
ZamaPay emits project-level webhook events after the payment read model and finality gate agree. Each delivery records attempt count, HTTP status, response body, error, retry state, and dead-letter state.
Webhook payloads are at-least-once. The merchant backend must use `x-zamapay-webhook-id` or the event id as an idempotency key.
| Header | Example | Use |
|---|---|---|
| x-zamapay-webhook-id | deliv_... | Delivery id for idempotent processing. |
| x-zamapay-event-id | evt_... | Immutable event id. |
| x-zamapay-webhook-timestamp | 2026-05-07T05:00:00Z | Signed timestamp. |
| x-zamapay-webhook-signature | v1=0x... | Payload authenticity proof. |
| x-zamapay-webhook-algorithm | keccak256.secret_prefix.v1 | Signature algorithm version. |
Verify a webhook
Verification canonicalizes the JSON body, builds `deliveryId.timestamp.canonicalBody`, and checks the keyed digest with the webhook secret. Reject missing headers before reading business fields.
import { keccak256, toUtf8Bytes } from "ethers"
export function verifyZamaPayWebhook(headers, body, secret) {
const deliveryId = headers["x-zamapay-webhook-id"]
const timestamp = headers["x-zamapay-webhook-timestamp"]
const signature = headers["x-zamapay-webhook-signature"]
const algorithm = headers["x-zamapay-webhook-algorithm"]
if (algorithm !== "keccak256.secret_prefix.v1") throw new Error("unsupported algorithm")
const canonicalBody = JSON.stringify(body)
const base = `${deliveryId}.${timestamp}.${canonicalBody}`
const expected = "v1=" + keccak256(toUtf8Bytes(`${secret}.${base}`))
if (signature !== expected) throw new Error("invalid signature")
return body
}Ready to wire a merchant project?
Create the project in the console, then keep external checkout creation on the project API-key path.