Skip to content
Webhooks SDK
Esc
navigateopen⌘Jpreview
On this page

The event envelope

A normalized wrapper around every webhook — with the provider's payload left untouched.

Every event your handlers receive has the same shape above payload, regardless of provider:

{
  id: 'evt_1abc',                    // used for idempotency
  provider: 'stripe',
  type: 'payment_intent.succeeded',  // provider-native name
  timestamp: Date,
  payload: { … },                    // the provider's own body, untouched
  raw: { headers, body, method, url, header(), text(), json() },
}
Field What it is
id The provider’s delivery/event id. Feeds the idempotency store.
provider The provider’s id ('stripe', 'github', …). Useful when one handler serves a router.
type The provider-native event name — payment_intent.succeeded, push, INTERACTION_CREATE. Exactly what the provider calls it, from wherever the provider puts it (body field or header).
timestamp When the provider says the event happened, as a Date.
payload The provider’s own body, parsed but not reshaped.
raw The verified request: headers, the body bytes, and lazy text() / json() accessors.

Why payload stays provider-native

A Stripe PaymentIntent and a GitHub push have nothing in common, and flattening them into a shared shape would lose information without buying much. You already know which provider you’re integrating; what you want is that provider’s documented payload, not a lossy abstraction over it.

A cross-provider semantic view for the domains where it genuinely fits — payments, git, messaging — is planned as an opt-in layer, not a replacement.

Typed payloads

Providers ship event maps, so handlers narrow by event name:

createWebhookHandler({
  provider: stripe({ secret }),
  on: {
    // event.payload is typed from the event name
    'payment_intent.succeeded': async (event) => {
      event.payload.data.object // PaymentIntent
    },
  },
})

Was this page helpful?