Build the mental model
A webhook payload almost always follows the same general shape, even though field names differ by provider: a field naming the event type, and a data object holding the details specific to that event.
This matters because a real integration rarely registers a separate URL for every event type. Your endpoint has to look at the event type on each request and route it to the right piece of logic.
- Map each event type string to the function that should handle it.
- Fall back to a safe default (acknowledge and ignore) for event types you don't recognize yet.
- Keep the endpoint stable even as providers add new event types you haven't built support for.
One honest caveat: the event-type-plus-data pattern is common but not universal, and field names vary. Always read the specific provider's webhook documentation before writing a handler.
ONE ENDPOINT, MULTIPLE EVENT TYPES
----------------------------------
-----
All events arrive at one URL: POST /webhooks/orders
payment.completed -----> handlePaymentCompleted()
payment.failed -----> handlePaymentFailed()
user.created -----> handleUserCreated()
(anything else) -----> log it, acknowledge, do not crashConnect it to a real scenario
The code below builds a small dispatch table — a plain object mapping event type strings to handler functions — and a router that looks up the right handler. This is the pattern you'll reuse in nearly every real webhook endpoint.
Four payloads come in: two payment events, a new user event, and one event type the dispatch table has never seen before. That last one falls through to a default response instead of crashing.
Providers add new event types over time. A router that fails loudly on an unrecognized type will take your whole handler down; one that reports and moves on stays stable.
Try the working example
const handlers = {
"payment.completed": (data) => `Order ${data.orderId} marked paid`,
"payment.failed": (data) => `Order ${data.orderId} marked failed, notifying customer`,
"user.created": (data) => `Welcome email queued for ${data.email}`
};
function routeWebhookEvents(payloads) {
return payloads.map((raw) => {
const event = JSON.parse(raw);
const handler = handlers[event.type];
if (!handler) {
return { type: event.type, result: "No handler registered, ignored" };
}
return { type: event.type, result: handler(event.data) };
});
}
const incomingBatch = [
JSON.stringify({ type: "payment.completed", data: { orderId: "ord_100" } }),
JSON.stringify({ type: "payment.failed", data: { orderId: "ord_101" } }),
JSON.stringify({ type: "user.created", data: { email: "mia@example.com" } }),
JSON.stringify({ type: "invoice.paid", data: { invoiceId: "inv_9" } })
];
console.log(routeWebhookEvents(incomingBatch));[
{ type: 'payment.completed', result: 'Order ord_100 marked paid' },
{
type: 'payment.failed',
result: 'Order ord_101 marked failed, notifying customer'
},
{
type: 'user.created',
result: 'Welcome email queued for mia@example.com'
},
{ type: 'invoice.paid', result: 'No handler registered, ignored' }
]5-minute try-it
Add a handler for an 'invoice.paid' event type to the dispatch table, and confirm it's picked up correctly on the next router run.
One important caution
Hard-coding logic that assumes only one event type will ever arrive at your endpoint.
Letting an unrecognized event type crash or throw instead of falling back to a safe default response.
GitHub Docs — Webhook events and payloads — API Integration & Webhooks