Skip to main content
Bouclay sends an event to your server whenever something changes: a trial converts, an invoice is paid, a card is saved. Webhooks keep your app in step without polling.

Register an endpoint

Add your URL in the dashboard under Developers → Webhooks, or with the API:
The response carries the endpoint’s signing secret (whsec_…). It is shown once, so store it right away. Every active endpoint receives every event. To replace a secret, call POST /webhook-endpoints/{id}/rotate-secret: the old secret stops working at once.

What an event looks like

data.object is the full object, exactly as the REST API returns it.

Verify the signature

Anyone can send a request to your URL, so check that each one came from Bouclay. Every delivery carries a header:
t is when it was sent (Unix seconds). v1 is an HMAC-SHA256 of "<t>.<raw body>" keyed with your signing secret, written in hex. To verify:
  1. Read the raw request body, before any JSON parsing.
  2. Compute the HMAC of t + "." + body with your secret.
  3. Compare it with v1 using a constant-time comparison.
  4. Reject the request if t is more than five minutes old, so an old delivery can’t be replayed.

Handle each event once

Your endpoint must answer with a 2xx within 10 seconds. Redirects are not followed. A delivery that fails is tried up to 5 times in all, waiting 1 minute, 5 minutes, 30 minutes and 2 hours between tries. So the same event can arrive more than once, and a retried one can arrive after a newer one.
  • Answer fast. Return 200 as soon as the signature checks out, then do the work in a background job.
  • Skip events you’ve seen. Store each id you handle and ignore repeats.
  • Skip stale events. Compare the event’s created time with the last event you applied to that object, and ignore older ones.

Read the state, not the event name

Each object sends exactly two events: *.created when it first exists, and *.updated for every change after that. There is no invoice.paid or subscription.canceled. Read status and the other fields off data.object instead:
This keeps the list short, and a new status never breaks your handler: it arrives on an event you already handle. subscription.* events also carry data.object.customer.entitlements, the codes the customer holds right now, so you can update your paywall without calling back.

Event list

Prices, discounts, payments and refunds send no events of their own. A payment arrives on invoice.updated as data.object.payment.

Missed something?

Every event is also kept in the API. List them with GET /events, newest first, to backfill after downtime.