The envelope
Every delivery has the same shape. Onlydata changes between events.
Headers
Verify the signature on every request before you act on it — see
Verify Signature. An unverified webhook endpoint is
a URL that anyone who guesses it can use to mark orders as paid.
Delivery
Be aware of how delivery actually behaves before you build on it:- Respond within 10 seconds. We give up after that and mark the delivery
failed, even if your handler eventually finishes. Acknowledge with a
200first and do the slow work afterwards. - Return 2xx to acknowledge. Any other status counts as a failure.
- Order isn’t guaranteed. Deliveries go out in parallel, so
payment.paidcan reach you beforepayment.created. Treat each event as a statement about the object’s current state rather than a step in a sequence. - Handle duplicates. A resent delivery repeats an event you may already
have processed. Key your handler on the object’s
idand the event name, and make repeat deliveries a no-op. - Reconcile, don’t rely. Because there are no automatic retries, a webhook outage on your side loses events. For anything that must not be missed — granting access, releasing goods — reconcile against the REST API on a schedule as well.
Environments
An endpoint is scoped tosandbox, to production, or to both. One registered
for a single environment only ever receives that environment’s events, so you
can point sandbox at a tunnel on your laptop and production at your real server
without them crossing.
Each environment signs with its own secret key. A live delivery is signed with
your live key, and you cannot verify it with your test key.
Event catalogue
Payments
data is the payment object:
transactionHash is added on payment.paid and payment.underpaid; it is the
same value as tx_hash, kept for older integrations.
status is overpaid rather than paid when more arrived than was owed. Both
mean the payment settled — check for either, or use payment.paid as the
trigger and read amount_paid for the figure.
Subscriptions
The two that matter are
subscription.activated and subscription.expired.
Wire those and you have a working integration; the rest is refinement.
data is the subscription object, with plan and customer always expanded:
terms, not from plan. terms is what this subscriber
actually pays, frozen when they signed up; plan may since have been repriced
for new customers. When the two differ, on_legacy_terms is true. See
Subscriptions.
Subscription payments fire the payment events too. A renewal arrives as a
payment.paid with type: "subscription", the subscription’s id in
subscription, and the cycle it covers in billing_period_start and
billing_period_end — so an existing payments integration keeps working
unchanged. If you handle both, make sure you don’t fulfil twice.Managing endpoints
In the dashboard: Developer → Webhooks → Add endpoint, then pick the events you want. Developer → Logs shows every delivery with the response we got back, and lets you resend one.Over the API
Endpoints can also be managed with your secret key, which is how an installable integration registers itself. The key decides the environment — a test key registers a sandbox endpoint, a live key a production one.Registering is an upsert keyed on the URL. Posting the same URL again updates
its event list instead of creating a second endpoint, so an integration can
call this on every startup without piling up duplicates.
An unknown event name is rejected with
Unsupported events: ... rather than
silently ignored, so a typo fails loudly instead of leaving you waiting for a
delivery that never comes.
List your endpoints for the key’s environment:
webhook.test event — same envelope, same headers, same
signature — and reports back what your server returned:
webhook.test by
verifying the signature and returning 200; there’s nothing to act on.