Skip to content
Merchant guide
HTTPS · Signed events

Identifiers and dedupe — at-least-once

Delivery is guaranteed at-least-once, not exactly-once — here you learn when duplicates happen and how to handle them correctly without double processing.

Reviewed against implementation
YOUR INTEGRATION, AT A GLANCE
Node.js — a dedupe store
// 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
// }
See the implementation
On this page

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.
Node.js — a dedupe store
// 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.