Skip to main content
When an agent’s purchase settles, Alyte can push a signed payment.settled event to your server the moment it happens. Polling GET /v1/integration/payments?since=… keeps working and remains the documented catch-up path — webhooks are an upgrade, not a replacement. Keep the poller running at a relaxed cadence as your safety net.

Register an endpoint

The secret is shown only in this response. Store it like a password (an environment secret, not your database in plaintext). Listing endpoints never returns it; if you lose it, delete the endpoint and register a new one.
URLs must be https (localhost is allowed for development). Each tenant can have at most 3 active endpoints. GET /v1/integration/webhooks lists yours; DELETE /v1/integration/webhooks/:id disables one — delivery stops immediately.

The delivery

Each delivery is a POST with three headers and a JSON body:
data is the same payment row shape your poller reads from /v1/integration/payments — one parser serves both paths. Fulfil deterministically on tierId + quantity (never by amount); buyerRef is a stable opaque buyer id, never an email.

watch.failed — when an agent tried and could not buy

A queued purchase (buy-at-the-drop or join-the-line) that fires and is refused is terminal: it does not retry, and without a signal the buyer would never learn what happened. So every terminal failure pushes watch.failed through the same signed pipeline:
Common reason.code values: spend_cap_exceeded, mandate_invalid (a quantity guardrail or scope rule), not_found (the saved card is no longer usable), sale_ended, payment_refused, sca_required. Relay the message to the buyer — it is written to be shown, and it tells them what to change (raise the limit, buy fewer, re-add the card).
watch.failed is not a payment: nothing was charged and nothing must be fulfilled. Branch on type — fulfil only payment.settled.

Verify the signature

The signature is an HMAC-SHA256 over the raw request body, prefixed with the timestamp — the same shape as Stripe’s, so an existing verifier ports directly:
Verify against the raw body bytes, before any JSON parsing or re-serialization — a re-stringified body will not match. Most frameworks need a raw-body route or content-type parser for the webhook path.

Test your endpoint before a real purchase

Sends a real, signed delivery through the same pipeline — so verifying it proves your signature check and raw-body handling work before any money is involved.
The test event’s type is webhook.test, not payment.settled, and it carries no payment fields. Your fulfilment path must branch on type and ignore anything that is not payment.settled — never issue goods on a test.

Rotate a leaked secret

If a signing secret is exposed (a log, a screenshot, a shared terminal), rotate it rather than deleting and re-registering — the endpoint id and any queued retries survive:
Returns a new whsec_… once. The old secret stops verifying immediately, so deploy the new one promptly; deliveries attempted in between fail signature verification on your side and are retried, arriving correctly once the new secret is live.

Delivery semantics

  • At-least-once. A settle durably enqueues the event, then delivery is attempted immediately and retried with exponential backoff (30s doubling, capped at 1h, up to 10 attempts) on the sweep tick. Any non-2xx response, timeout, or connection failure counts as a failed attempt.
  • Dedupe by eventId. Retries and edge cases can deliver the same event twice. eventId is the payment’s intentId — process each eventId once (the same discipline your poller already uses).
  • Answer fast. Respond 2xx as soon as you’ve recorded the event; do your fulfilment work asynchronously. Slow handlers risk the 10-second delivery timeout and a redundant retry.
  • Disabled means stopped. Deleting an endpoint stops deliveries immediately, including queued retries.

Keep the poller as the fallback

Webhooks tell you now; the poller guarantees eventually. The recommended integration:
  1. Webhook receiver verifies, records by eventId, fulfils.
  2. Poller runs every few minutes with ?since=<your newest createdAt>, fulfilling anything the webhook path missed — deduped by the same intentId key.
That pair gives you real-time fulfilment with no single point of failure.