[ BACK_TO_TRANSMISSIONS ]
[ENGINEERING]2026-04-20·8 min read

WEBHOOK CHAOS: NORMALIZE STRIPE + TWILIO + GITHUB IN ONE PLACE

[EVENTS]

Stripe sends payment_intent.succeeded with a nested data.object. Twilio sends CallStatus=completed as form-encoded. GitHub sends a push event with a ref field. Three providers, three payload shapes, three retry policies, three signature verification schemes. Every service your product integrates adds another bespoke webhook handler.

THE NORMALIZATION PROBLEM

Business logic shouldn't know which provider fired an event. It should receive a standard shape: what happened, who it happened to, when, and the raw payload if needed. That normalization is infrastructure work — yet most teams build it inline with product code.

[CURRENT REALITY]

Three webhook routes, three signature checks, three payload parsers, three retry handlers — before writing a single line of business logic.

HZRELAY EVENT SESSIONS

An event session accepts inbound webhooks from any configured provider, verifies signatures, normalizes the payload into a standard EventFrame, and fans it out to your configured sinks.

webhooks.ts
const session = createSession({
inbound: { type: 'webhook', sources: [
{ provider: 'stripe', secret: env.STRIPE_SECRET },
{ provider: 'twilio', secret: env.TWILIO_SECRET },
{ provider: 'github', secret: env.GH_SECRET },
]},
outbound: { type: 'fanout', subscribers: [
{ type: 'webhook', url: 'https://crm.internal/events' },
{ type: 'websocket', id: 'slack_notifier' },
{ type: 'websocket', id: 'ai_agent' },
]},
});
 
// normalized EventFrame — same shape regardless of source
session.on('event', (e) => {
console.log(e.provider, e.type, e.subject, e.raw)
})
[ SYS_LOG ] LIVE
EVTstripe: payment_intent.succeeded → EventFrame
NORMprovider=stripe type=payment.success subject=cus_abc123
OUT→ crm.internal + slack_notifier + ai_agent
EVTgithub: push → EventFrame
NORMprovider=github type=push subject=main branch
OUT→ crm.internal + slack_notifier + ai_agent
_