Webhooks
Webhooks push real-time notifications to your server when events happen in your store.
How It Works
- Register a webhook endpoint URL in your admin dashboard
- When an event occurs, we send a POST request to your URL
- Your server processes the payload and returns a 2xx response
- If delivery fails, we retry with exponential backoff
Event Types
| Event | Trigger |
|---|---|
order.created | New order placed (online or POS) |
order.status_changed | Order status updated |
product.created | New product added |
product.updated | Product details changed |
customer.created | New customer registered |
inventory.changed | Stock 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
| Header | Description |
|---|---|
Content-Type | application/json |
X-durj-Signature | HMAC-SHA256 signature |
X-durj-Event | Event type |
X-durj-Delivery | Unique 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
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
| 5th retry | 12 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.