Skip to content

Webhooks

Webhooks push real-time notifications to your server when events happen in your store.

How It Works

  1. Register a webhook endpoint URL in your admin dashboard
  2. When an event occurs, we send a POST request to your URL
  3. Your server processes the payload and returns a 2xx response
  4. If delivery fails, we retry with exponential backoff

Event Types

EventTrigger
order.createdNew order placed (online or POS)
order.status_changedOrder status updated
product.createdNew product added
product.updatedProduct details changed
customer.createdNew customer registered
inventory.changedStock level changed

Payload Format

{
"id": "delivery-uuid",
"event": "order.created",
"entity_type": "order",
"entity_id": "order-uuid-123",
"data": { "id": "order-uuid-123", "status": "pending", "total": 80.0 },
"timestamp": "2026-04-20T14:30:00Z"
}

Headers

HeaderDescription
Content-Typeapplication/json
X-durj-SignatureHMAC-SHA256 signature
X-durj-EventEvent type
X-durj-DeliveryUnique delivery ID

Verifying Signatures

Python

import hmac, hashlib
def verify_signature(payload: bytes, secret: str, signature: str) -> bool:
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)

JavaScript (Node.js)

const crypto = require('crypto')
function verifySignature(payload, secret, signature) {
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex')
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}

PHP

function verifySignature(string $payload, string $secret, string $signature): bool {
return hash_equals(hash_hmac('sha256', $payload, $secret), $signature);
}

Retry Policy

AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
5th retry12 hours

After 5 failed attempts, the delivery moves to the Dead Letter Queue (DLQ). You can manually retry from the admin dashboard.

Idempotency (you MUST dedupe)

Every delivery — including retries of a previously sent event — carries an X-durj-Delivery header. The value is stable across retries of the same event: if our dispatcher receives a non-2xx response or times out, it re-sends the identical payload with the same X-durj-Delivery value at the next retry slot.

Your handler MUST deduplicate by this header — not by checking whether your previous write “looks the same”:

async function handleWebhook(req, res) {
const deliveryId = req.headers['x-durj-delivery']
// Reject the duplicate without touching downstream state.
if (await alreadyProcessed(deliveryId)) {
return res.status(200).json({ deduplicated: true })
}
await processEvent(req.body)
await markProcessed(deliveryId) // do this AFTER the side effect, in the same tx
return res.status(200).json({ ok: true })
}

Why this matters: a transient 5xx on your side will be retried up to 5 times. Without deduplication you would charge a customer 5 times, create 5 orders, or send 5 emails for the same order.created.

The combination of (X-durj-Delivery, your-side processed-ledger) makes the contract exactly-once even though the wire delivery is at-least-once. Don’t try to dedupe by (event_type, resource_id) alone — the same resource can legitimately fire multiple events (e.g. order.created then order.updated) and you’d drop the second one.