This page covers the integration side of Vorel webhooks — subscribing, building a verified consumer, dealing with retries and dead-letters. The full event catalogue + retry ladder lives under API Reference → Webhooks; this page is the implementation guide.Documentation Index
Fetch the complete documentation index at: https://docs.vorel.ai/llms.txt
Use this file to discover all available pages before exploring further.
End-to-end setup
Build your endpoint
A standard HTTPS endpoint that accepts POST + JSON. Write the handler before subscribing —
the dashboard fires a test event on subscribe so you’ll want it ready to receive.
Subscribe in the dashboard
app.vorel.ai/(dashboard)/settings/integrations/webhooks. Provide:- Target URL (your endpoint, must be HTTPS).
- Subscribed events — pick from the 12 canonical event types.
- Description for the audit log.
Verify the signature on every request
HMAC-SHA256 of the raw request body, compared constant-time against the
X-Webhook-Signature header. Reject 4xx on mismatch.Idempotency-check on envelope.id
Vorel may redeliver the same event up to 6 times. Your consumer must dedupe on the envelope
id field. Persist id → processed-state in your own store; check + ack quickly.Process the event
Switch on
envelope.event; handle the payload. Keep this fast — 5xx triggers a retry, slow
consumers amplify the dispatcher’s per-attempt timeout (10 seconds).Canonical consumer (Node + Express)
Common failure modes
Signature verification failing on every request
Signature verification failing on every request
Cause is almost always one of:
- Body was JSON-parsed before signing. Re-serialising drops whitespace + reorders keys —
the signature won’t match. Capture raw bytes (use
express.raw()notexpress.json()). - Wrong secret — make sure
VOREL_WEBHOOK_SECRETmatches the dashboard subscription’s secret. Subscriptions get unique secrets; re-check the right one. - Hex case mismatch — Vorel emits lowercase hex. Most consumers handle both, but a
case-sensitive
===compare with an uppercase test string would fail.
timingSafeEqual against equal-length Buffers, and stick with lowercase hex throughout.Same event firing multiple times
Same event firing multiple times
This is expected — the retry ladder may redeliver after a 5xx or timeout. Idempotency on
envelope.id is non-negotiable. Re-deliveries can land minutes (or up to ~14h) after the
first attempt, so an in-memory Set is not enough; persist ids in your consumer store.Common idempotency-store choices:- Redis with TTL (e.g. 30 days).
- DB table with
(id, processed_at), unique index onid. - Conditional write to a key-value store (Cloudflare KV, DynamoDB).
Event in dead-letter
Event in dead-letter
A delivery moves to
webhook_deliveries.status='dead_letter' after the 6-attempt ladder
exhausts. Causes:- Your endpoint returned 5xx for the whole window. Check your error logs around the timestamps in the delivery row.
- Your endpoint was unreachable (DNS / TLS / firewall). Check ingress logs.
- Your endpoint took >10s on every attempt. Check processing time.
Slow processing causing 5xx → retries → load
Slow processing causing 5xx → retries → load
The dispatcher times out at 10 seconds per attempt. If your consumer takes 30s to process,
it’ll cascade: timeout → 5xx → retry. Pattern to fix:
- Ack first, process async. Receive the event, write to your queue, return 200 within a few hundred ms.
- Worker picks up the job and does the heavy lifting on its own time.
Subscribing programmatically
Webhook subscriptions live in the dashboard today. There’s no public API for managing them yet — operator-side mutation only. If you need to provision subscriptions across many tenants during onboarding, your operator runs the dashboard flow per tenant; mass-provisioning APIs are roadmap.Security posture
- HTTPS-only. HTTP target URLs are rejected at subscription time.
- HMAC-SHA256. Per-subscription secret; not a shared platform key.
- No replay protection beyond the envelope id. The id doubles as your idempotency key — reject duplicates yourself.
- Constant-time comparison. Vorel’s verification uses
timingSafeEqual; your consumer should too. - No PII in the URL. Endpoint URLs are stored in
webhooks.target_urlplain (high-cardinality audit log on access). Don’t put bearer tokens in the URL — use a header on your end if you need additional auth.
What about webhook ingress (the other direction)?
Vorel receives webhooks from telephony / messaging / Clerk vendors at/api/webhooks/*.
That surface is signature-verified per-vendor and rate-limited at the IP layer (500 req/min).
That’s the ingress side; this page covers Vorel-as-emitter.
Related docs
- API Reference → Webhooks — full event catalogue, envelope shape, retry ladder
- Automation — the n8n templates that consume webhooks
- SDKs — for the API calls your consumer might fan out to