Skip to content

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.

POST /webhooks/:adapter/:siteId

The adapter implementation:

  1. Verifies the inbound signature (per the adapter’s spec).
  2. Maps matched events to the canonical wire shape.
  3. Forwards each mapped event to the collector’s /events, authenticated as Bearer <siteId>.
POST /webhooks/stripe/:siteId
Stripe-Signature: t=…,v1=…

Verifies via Stripe’s official spec — HMAC-SHA256 with a 5-minute tolerance window.

Stripe eventMapped event
charge.succeededpurchase
customer.subscription.createdsubscription_created
customer.subscription.updatedsubscription_updated
customer.subscription.deletedsubscription_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.

Webhook secrets live in the webhook_secrets table:

ColumnPurpose
site_idThe Leatsmap site.
adapterstripe (more in v1.x).
secretThe shared secret used for signature verification.
created_at, revoked_atLifecycle timestamps.

A missing row returns 401 with the same generic envelope as gdpr.ts so an attacker cannot enumerate tenants by status-code differential.

StatusWhen
200 { ok: true, forwarded: <n> }Verified successfully.
400Invalid signature OR malformed body. Stripe will not retry.
401Webhook is not configured for this site.
404Unknown adapter name.
500Collector forward failed. Stripe will retry.

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.

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.