Webhooks
The query API hosts webhook receivers for third-party event sources. The v1.0 line ships one adapter — Stripe — with the architecture in place to add more in v1.x minor releases.
Endpoint
Section titled “Endpoint”POST /webhooks/:adapter/:siteIdThe adapter implementation:
- Verifies the inbound signature (per the adapter’s spec).
- Maps matched events to the canonical wire shape.
- Forwards each mapped event to the collector’s
/events, authenticated asBearer <siteId>.
Stripe adapter
Section titled “Stripe adapter”POST /webhooks/stripe/:siteIdStripe-Signature: t=…,v1=…Verifies via Stripe’s official spec — HMAC-SHA256 with a 5-minute tolerance window.
Event mapping
Section titled “Event mapping”| Stripe event | Mapped event |
|---|---|
charge.succeeded | purchase |
customer.subscription.created | subscription_created |
customer.subscription.updated | subscription_updated |
customer.subscription.deleted | subscription_canceled |
Unmapped Stripe types verify successfully and return 200 with zero
events forwarded. This is deliberate — Stripe will not retry, and the
adapter is allowed to grow its mapping table over time without breaking
existing customers.
Per-(site, adapter) secret
Section titled “Per-(site, adapter) secret”Webhook secrets live in the webhook_secrets table:
| Column | Purpose |
|---|---|
site_id | The Leatsmap site. |
adapter | stripe (more in v1.x). |
secret | The shared secret used for signature verification. |
created_at, revoked_at | Lifecycle timestamps. |
A missing row returns 401 with the same generic envelope as gdpr.ts
so an attacker cannot enumerate tenants by status-code differential.
Status codes
Section titled “Status codes”| Status | When |
|---|---|
200 { ok: true, forwarded: <n> } | Verified successfully. |
400 | Invalid signature OR malformed body. Stripe will not retry. |
401 | Webhook is not configured for this site. |
404 | Unknown adapter name. |
500 | Collector forward failed. Stripe will retry. |
Adding webhook secrets
Section titled “Adding webhook secrets”In v1.0, secrets are inserted via direct SQL (an admin endpoint to manage them lands in v1.1):
INSERT INTO webhook_secrets (site_id, adapter, secret)VALUES ('site_marketing', 'stripe', 'whsec_…');Rotate by setting revoked_at = now() on the old row and inserting a
new one — the verifier accepts any active (non-revoked) secret for a
short overlap window.
v1.0 adapter scope
Section titled “v1.0 adapter scope”Stripe is the only adapter in v1.0. The adapter pattern is in place so v1.x minor releases can add more (Shopify is the most-requested candidate, but landing it requires its own ticket and review cycle).
If you need to ingest from a custom source, do it directly via the
/s2s/events endpoint with your own server-side normalization — that
bypasses the adapter layer entirely.