DoloresDocs
Webhooks

Best practices

Patterns for production-grade webhook handlers.

Respond fast, work async

Dolores' delivery worker times out after 15 seconds. Your handler should:

  1. Verify the signature
  2. Validate the payload shape
  3. Enqueue or commit the work atomically
  4. 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:

  1. Dedup by event.id. Every event has a unique ULID-style id. Store the set of ids you've processed and skip duplicates.
  2. Upsert, don't insert. When syncing appointment state, use INSERT ... ON CONFLICT DO UPDATE.
  3. State-machine your writes. Don't blindly apply previous_attributes — instead, treat data.object as 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:

  1. 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.
  2. 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.