How do I normalize webhooks from multiple vendors?
Define one event shape of your own, write one small mapping per vendor into it, and keep the vendor's raw payload beside the result. Your code then reads a single shape no matter who sent the event, a new vendor is one new mapping instead of a change everywhere, and a mapping bug can be fixed and re-run against the stored raw payloads. Reject an event its mapping cannot fill; never pass it on half-empty.
One shape, one mapping per vendor
// One internal shape for every sender: { source, id, type, occurredAt, data }.
// Add one entry per vendor; an event from a vendor without a mapping is rejected.
const mappings = {
stripe: (body) => ({ id: body.id, type: body.type, occurredAt: Number.isFinite(body.created) ? new Date(body.created * 1000).toISOString() : undefined, data: body.data.object }),
shopify: (body, headers) => ({ id: headers['x-shopify-webhook-id'], type: headers['x-shopify-topic'], occurredAt: body.updated_at, data: body }),
}
export function normalize (source, body, headers = {}) {
const map = Object.hasOwn(mappings, source) ? mappings[source] : null
if (!map) throw new Error(`no mapping for source ${source}`)
const event = { source, ...map(body, headers) }
for (const field of ['id', 'type', 'occurredAt']) {
if (event[field] == null || String(event[field]).trim() === '') throw new Error(`${source} event missing ${field}`)
}
return event
}Where each vendor keeps the basics
Vendors disagree on where the event type lives. Stripe puts it in the body's top-level type; Shopify and GitHub send it in a header; LinkedIn puts it one level down, at data.type. A mapping written against the wrong place does not error — it reads nothing — so check every mapping against a payload you actually received. The LinkedIn recipe is that failure, step by step.
Keep the raw payload
The normalized event is your reading of the vendor's payload. Keep the raw payload next to it, so when a mapping turns out wrong you fix it and re-run the stored payloads instead of asking the vendor to resend.
In Slashbin
A Golden Model is the shape you define, validated on every event before delivery; on the Pro plan, Golden Model Inheritance reuses one master schema across data streams, so each vendor's stream maps into the same shape. See Golden Model.
Need somewhere for the next project's webhooks to land? Slashbin's Free plan takes an account, not a card.
Start Free
(opens in a new tab)