Best practices
Patterns for production-grade webhook handlers.
Respond fast, work async
Dolores' delivery worker times out after 15 seconds. Your handler should:
- Verify the signature
- Validate the payload shape
- Enqueue or commit the work atomically
- Return 2xx
Anything heavy (DB writes that hit hot indexes, network round-trips to other services, AI calls) belongs in a worker — not the request path. A simple INSERT INTO inbound_events ... ON CONFLICT DO NOTHING is enough to hand-off the event durably.
app.post('/dolores/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
verifyDoloresSignature(req.body.toString('utf8'), req.headers['dolores-signature'], SECRET);
const event = JSON.parse(req.body.toString('utf8'));
await db.query(
'INSERT INTO dolores_events (event_id, payload) VALUES ($1, $2) ON CONFLICT (event_id) DO NOTHING',
[event.id, event],
);
res.status(200).send();
});The actual business logic runs out of band.
Be idempotent
Network blips and retries can cause the same event to land more than once. Three layers of defense:
- Dedup by
event.id. Every event has a unique ULID-style id. Store the set of ids you've processed and skip duplicates. - Upsert, don't insert. When syncing appointment state, use
INSERT ... ON CONFLICT DO UPDATE. - State-machine your writes. Don't blindly apply
previous_attributes— instead, treatdata.objectas the new desired state and reconcile.
Verify, then trust
Never branch on payload contents before verifying the signature. A spoofed delivery with an unverified payload that triggers a "send confirmation SMS" code path is the kind of bug that ships to production at 3 AM.
// Bad: parsing before verification
const event = JSON.parse(req.body);
if (event.type === 'appointment.confirmed') triggerSMS(); // <-- spoofable
verifyDoloresSignature(...);
// Good: verify first, parse and dispatch only on success
verifyDoloresSignature(...);
const event = JSON.parse(req.body);
if (event.type === 'appointment.confirmed') triggerSMS();Use the test endpoint
Before flipping a webhook on against live traffic, run the synthetic test:
curl -X POST "https://app.meetdolores.ai/v1/webhook_endpoints/whk_.../test" \
-H "Authorization: Bearer $DOLORES_API_KEY"It fires an appointment.created event with customer_id: "cust_TEST_EVENT" and id: "appt_TEST_EVENT". Use those sentinel ids to short-circuit your handler in production — don't actually create the synthetic appointment in your CRM.
Tolerate delivery delays
The worst-case latency is the delivery worker tick interval (~1s) plus your endpoint's response time. Under normal load this is sub-second. During backoff retries it can be hours. Don't tie operational SLAs to event arrival time — if you need real-time confirmation that an appointment is in your system, do an inline GET /v1/appointments/{id} instead.
Watch the deliveries log
The admin UI's Webhooks → Deliveries view shows the last 50 attempts per endpoint with status, attempt count, error message, and timing. Wire your monitoring to it — a sudden uptick in failed deliveries means your endpoint is misbehaving or your handler regressed.
You can also pull this data via GET /v1/events/{id}/deliveries for arbitrary windows.
Plan for endpoint downtime
Two strategies, depending on your tolerance for late events:
- Buffer at Dolores. Don't acknowledge events you can't handle yet. We retry for 24 hours; if your endpoint comes back online within that window, you get the events for free.
- Acknowledge and store. Always 200, write the event to a local queue, and process out of band. The risk: if your acking endpoint can't write the queue either, you lose the event.
For most teams, option 1 is the safer default. The 24-hour retry budget covers a typical incident response window.
Rotate secrets on suspicion of leak
If you suspect the signing secret has leaked, rotate it via POST /v1/webhook_endpoints/{id}/rotate_secret. Deploy the new secret to your handler before the next delivery. The old secret stops signing immediately.