01The two duplicate cases
- A retry of the same delivery: the network dropped after you processed the event and the platform never recorded your response, or the attempt genuinely failed — either way the same webhook-delivery-id returns.
- A manual replay (Retry or Replay from the dashboard): a brand-new delivery with a new id for the same event — the constant there is webhook-event-id.
// Dedupe store: durable (database/Redis) in production — an in-memory
// Set dies with the process. Expire the keys after 30 days (the delivery
// retention window) so the store does not grow forever.
async function isDuplicate(store, deliveryId, eventId) {
// Layer 1: the same delivery retried -> same webhook-delivery-id.
if (await store.has(`d:${deliveryId}`)) return true;
// Layer 2: the same event replayed -> new delivery id, same event id.
if (await store.has(`e:${eventId}`)) return true;
await store.set(`d:${deliveryId}`, 1);
await store.set(`e:${eventId}`, 1);
return false;
}
// In your route, after signature verification passes:
// if (await isDuplicate(store, headers["webhook-delivery-id"], headers["webhook-event-id"])) {
// return res.status(200).end(); // already processed: acknowledge, do nothing
// }02Event identities and ordering
Every event has a stable textual id (event_id) following the general pattern mwh:<type-dashed>:<tenant_id>:<resource_id>. Types repeating for the same source carry a <seq> suffix: mwh:order-status:<tenant>:<order>:<seq> per status transition (with the historical order-status prefix), mwh:product-updated:<tenant>:<product>:<seq> per update, mwh:kitchen-status-changed:<tenant>:<order>:<seq> per kitchen transition. And mwh:ping:<tenant>:<uuid> with a fresh id per test press.
COMPLETE YOUR INTEGRATION
