customer.subscription.created, the source verifies the signature, finds the subscriber the payload is about, and records subscription.started on their timeline as if your backend had tracked it. The event carries source: "webhook" and data.$provider: "stripe", so segments, workflow triggers and cancel rules see it without any code on your side.
Reading needs the sources:read scope, writing needs sources:write. Both run inside a tenant.
Providers are templates
stripe, superwall, revenuecat and custom each fill in a verification scheme and a default mapping when the source is created, and give it a label and a logo. Both the scheme and the mapping are stored on the source and editable afterwards, so anything a Stripe source does, a custom source can be configured to do.
url, its verification, its mapping, a hasSecret flag and a status.
Setting a secret activates an unverified source. Activating without one is refused with
source_unverified.
Verification
The provider’s signature is the credential, soPOST /v1/sources/:id/ingest is unauthenticated. It carries no bearer key, and the raw body and headers are verified exactly as received rather than after any reserialization.
stripe preset uses the first, an HMAC in one header with a timestamp and a tolerance window, and RevenueCat signs the same way under its own header name. The superwall preset uses Standard Webhooks with the Svix header names. The custom preset compares a shared secret in x-buzzkit-secret in constant time. Any source may switch scheme with PATCH { verification }, and a shape that fails lint is refused with invalid_verification and details.problems.
Secrets are sealed at rest, like credentials and workflow secrets, and are never returned by the API.
The mapping
The mapping says how one provider payload becomes one event.
Paths are dotted and may index into arrays, as in
a.b.0.c. A mapping holds at most 50 mapped types and 20 picked data paths, and the event names it produces follow the ordinary tracking rules, so none of them may start with $. A mapping that fails lint is refused with invalid_mapping and details.problems, each entry naming a path and a message.
Previewing a mapping
POST /v1/sources/:id/preview runs a mapping over a sample payload exactly as ingest would, subscriber lookup included, without creating anything. Pass the payload on its own to try the stored mapping, or pass a mapping to try a candidate before saving it.
{ outcome, event?, reason?, detail?, suggestions }, where event carries the resolved externalId. The signature check and deduplication are skipped, so a stored delivery’s payload previews cleanly. suggestions carries the detected provider and candidate paths for type, id, timestamp, subscriber and data, which is the same detection the dashboard runs on a pasted payload.
The delivery ledger
Every request to the ingest URL is recorded as a delivery with exactly one outcome.
The response body is
{ outcome, reason }, though providers only need the status. A body above 256 KB is refused with a 400 payload_too_large, and an unknown or deleted source answers 404.
GET /v1/sources/:id/deliveries lists them newest first, filterable with outcome and cursor-paginated. Each row carries providerEventId, providerType, outcome, reason, detail, subscriberId, event, eventId, the raw payload and receivedAt. The provider type and event id are read through the mapping’s paths on every outcome the payload allows, so a rejected or filtered delivery still tells you what it was. Deliveries keep the raw payload for 30 days.
Managing sources
Changes are audit entries and public webhook events:
source.created, source.updated and source.deleted. An update carries its changes along with previousAttributes, and a replaced secret shows only as secret: "replaced" because secret material is never diffed.
Next
Events
Where a mapped delivery lands, and how to read it back.
Workflows
Trigger a run on the event a source produced.
Subscribers
The attributes a
{ path, attribute } lookup matches against.