CanopyAPI docs

Webhook events

Canopy sends settlement results to a server endpoint you register. Verify each delivery's signature before you trust its body.

You set up this endpoint in the quickstart's watch-for-settlement step. This page covers how to verify each delivery and which events it carries.

Verify the signature first

Canopy follows the Standard Webhooks convention (github.com/standard-webhooks/standard-webhooks). Every delivery has three headers:

  • webhook-id: the event's ID. It is the same on every retry of that delivery.
  • webhook-timestamp: when the delivery was signed, as a Unix timestamp in seconds.
  • webhook-signature: one or more space-separated v1,<base64 signature> entries.

Verify with the Webhook class from the standardwebhooks package. Do not compute the HMAC yourself.

verify-webhook.tsts
import { Webhook } from 'standardwebhooks';

const wh = new Webhook(endpointSecret); // your whsec_... signing secret
const payload = wh.verify(rawBody, headers); // throws if no signature matches

verify() throws a WebhookVerificationError in two cases:

  • none of the signature entries match your secret
  • webhook-timestamp is more than five minutes before or after the current time

A delivery can have more than one v1,... entry, one for each active signing secret. This lets you rotate secrets without downtime. verify() accepts the delivery if any one entry matches. You do not need to know which secret was used.

Key rotation

Rotating a signing secret creates a new secret and marks the old one retiring. Both secrets sign every delivery until you revoke the old one.

Deliveries already in flight still verify against the retiring secret. Rotating does not revoke the old secret. Revoke it yourself once every service that verifies webhooks uses the new secret.

Events

Canopy defines two settlement events. Both use the same payload. It holds the payment intent's full current state, not only what changed.

ts
type SettlementEventPayload = {
  intentId: string | null;
  inboxAddress: string;
  created: false;
  state: string;
  feeUnits: string;
  netUnits: string;
  txHash: string;
  chainId: number;
  merchantReference: string | null;
};

Use state in the verified body to tell the events apart. The headers do not include the event type, so state is the only place to read it.

ts
// payload is the verified body -- never branch before verifying.
if (payload.state === "settled") {
  // The intent settled. Fund the order.
} else {
  // Settlement did not complete for this intent. Do not fund.
}

Events can arrive out of order. Each event carries the full state, so you can act on it alone. This holds even if a final event arrives first and you have not seen an earlier event for that intent.

Retries and delivery

Canopy retries a delivery if your endpoint does any of these:

  • returns a status outside 200–299
  • does not respond, for example because it timed out
  • is disabled

Return a 2xx status only after you have saved the event somewhere durable. If your processing could fail after you respond, save the event first and process it in the background.

See also

  • Quickstart: the full walkthrough, including the watch-for-settlement step.