SelSup Developers

End-to-end workflow · Guides

Webhooks and event-driven integration

Outbound webhook setup, a secure receiver, duplicate protection, and recovery of missed events through the API.

Sections: 5Developer guide
→

Webhooks reduce change latency but do not replace the source of truth. Receivers must be idempotent and periodic reconciliation remains essential.

Event-driven integrations and backend services

1

Define events and ownership

Start with one business event such as a new or changed order, stock change, or price change. Define the data owner and receiver action for every event.

Retain the original webhook payload separately from your normalized model for a limited period. It enables replay after the integration has been fixed.

Version your internal handler. A new optional field must not break parsing of a known event structure.

2

Build a secure HTTP receiver

  1. 1

    Constrain the request

    Accept only required methods and content types, enforce a body-size limit, and use a short read timeout.

  2. 2

    Verify the source

    Verify X-SelSup-Signature with the per-webhook signingSecret: HMAC-SHA256 over ASCII(t + "." + id + ".") followed by the raw body bytes. Reject stale timestamps and duplicate delivery IDs.

  3. 3

    Persist the event

    Store the payload and deduplication key in one transaction, then return a successful response quickly.

  4. 4

    Process asynchronously

    Call ERP, WMS, and other systems from a queue instead of the incoming HTTP request.

!
Keep the response path short

A slow response increases timeout and duplicate-delivery risk. Persist first and process from a queue afterward.

Minimal receiver pattern · JavaScript
const crypto = require('node:crypto');
const signingSecret = process.env.SELSUP_WEBHOOK_SECRET;

// Register this route before express.json() so req.body is a Buffer.
app.post('/selsup/events', express.raw({type: 'application/json'}), async (req, res) => {
  const header = req.get('X-SelSup-Signature') || '';
  const match = /^t=(\d+),id=([0-9a-f-]{36}),v1=([0-9a-f]{64})$/.exec(header);
  if (!match) return res.sendStatus(401);
  const [, timestamp, deliveryId, hex] = match;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401);
  const signed = Buffer.concat([
    Buffer.from(`${timestamp}.${deliveryId}.`, 'ascii'), req.body
  ]);
  const expected = crypto.createHmac('sha256', Buffer.from(signingSecret, 'hex'))
    .update(signed).digest();
  const actual = Buffer.from(hex, 'hex');
  if (actual.length !== expected.length || !crypto.timingSafeEqual(actual, expected)) {
    return res.sendStatus(401);
  }
  const inserted = await inbox.insertOnce(deliveryId, req.body);
  return res.sendStatus(inserted ? 202 : 200);
});

3

Duplicates, ordering, and deduplication

Treat duplicate delivery as normal. Use a stable event identifier when present; otherwise derive a key from entity type, entity id, modification time, and a hash of meaningful data.

Events may arrive out of order. Compare modifiedDate before writing or reload the current entity through the API.

Return a success status for an event already processed. Otherwise the sender may continue retrying completed work.

i
Inbox pattern

An incoming-events table with unique eventKey, status, and attempt count simplifies deduplication, replay, and auditing.

4

Recover missed events

Run periodic API reconciliation even when webhooks are healthy. Use modifiedDate with overlap for orders and a warehouse or changed-SKU check for inventory.

After receiver downtime, drain the inbox and request changes for the complete outage window. The same deduplication logic should combine webhook and API results.

5

Manage webhook lifecycle

Expose the active URL, event type, last successful receipt, and latest error in your integration. This enables diagnosis without direct server-log access.

Before replacing or deleting a URL, verify that the new receiver accepts events. Revoke the old secret after migration.