Skip to main content
Vorel posts JSON webhooks to URLs you register, on tenant-scoped lifecycle events (lead created, conversation handoff requested, booking rescheduled, etc.). Bodies are HMAC-SHA256 signed; consumers dedupe on the envelope id; we run a 6-attempt backoff ladder before dead-lettering.

Subscribe to events

Register webhook endpoints at /(dashboard)/settings/integrations/webhooks. Each subscription carries:
  • target_url: your HTTPS endpoint. Vorel POSTs the envelope here.
  • secret: auto-generated; used as the HMAC key. Surfaced once at create time, after that only the prefix is visible (rotate by recreating).
  • Subscribed event list: pick the subset you want; ignore the rest.
You can register multiple URLs per tenant (one for ops Slack, one for warehouse ETL, etc.) with overlapping or distinct event subscriptions.

The 19 event types

Call events carry deterministic envelope ids, so an at-least-once carrier callback cannot produce two distinct events for the same call transition. Deduplicate on the envelope id as you would for any other event.
conversation.created fires today only on the public-API write path (POST /v1/conversations). Internal ingest paths (inbound WhatsApp, inbound voice, webform handler) don’t emit it yet. Adding emission there has wider blast radius (every existing customer message would suddenly produce a webhook). When that lands, the emission point is the conversation-row insert in the inbound message handlers; the enum already covers it.

Envelope shape

Every webhook body is a JSON object of this shape:
  • id is a unique event id, formatted evt_<uuid>. Use it as your idempotency key. Vorel’s retry ladder may redeliver the same event up to 6 times; a 5xx that succeeded server-side but failed at the network layer will replay. Idempotent processing on id is non-negotiable.
  • event: one of the 19 names above.
  • created_at: ISO 8601 UTC.
  • tenant_id: the tenant whose data triggered the event. Useful when one URL receives events for multiple tenants (or for sanity-checking).
  • data: event-specific payload. Shapes are documented per-event (TODO: per-event payload schemas live in the OpenAPI spec at the corresponding resource).

Signature verification

Every delivery carries these headers (sent lowercase; HTTP header names are case-insensitive, so match accordingly):
The x-event-type and x-delivery-id headers are conveniences; the authoritative event name and event id live in the envelope body (event + id). Verify the signature before trusting the payload:
Verify before parsing. A failed verification means don’t trust the body: drop the request, log the failure, return a 4xx. We retry on 5xx but not 4xx, so a verification failure correctly results in no retry storm. The signature is computed over the raw request body bytes: JSON-decode-and-reserialize will break the verification (key ordering, whitespace, etc.). Capture the raw bytes before passing to JSON parser.

Retry ladder

Total retry window: ~14h 35min from first attempt to dead-letter. The classifier rules:
  • 2xx responsedelivered. Terminal. delivered_at recorded.
  • 4xx responsepermanent_fail. Terminal. No retry: a 4xx means we built a request the consumer rejected, and replaying it won’t change that.
  • 5xx response, timeout, or network error → retry per the ladder if budget remains; else dead_letter.
The 10-second per-attempt timeout is set at the dispatcher level so a hanging consumer can’t amplify the attempt cost.

Dead-letter handling

When a delivery exhausts the 6-attempt ladder:
  1. The row’s status flips to dead_letter.
  2. The response_body carries the last error text (truncated).
  3. A tenant-admin alert fires (error-reporting capture + dashboard surface).
Replaying a dead-lettered delivery is operator-driven today: the dashboard surfaces a “replay” button that re-enqueues the delivery for another 6-attempt cycle. We don’t auto-replay dead-letters because the cause is almost always a structurally-broken consumer, not a transient fault.
Three things to internalise:
  • Idempotency on id is a contract, not a suggestion. We will redeliver under transient failure.
  • Verify before parsing. Tampered bodies must return 4xx + we won’t retry them.
  • Ack fast. 5xx triggers a retry; 4xx triggers permanent_fail. If your processing is slow, enqueue and ack; don’t make Vorel wait.

Before you build on a webhook

Check whether a trigger rule already covers it. The trigger engine consumes the same events and can assign, prioritise, label or call without you hosting anything. Webhooks are the right choice when the action belongs in your systems. Triggers are the right choice when the action belongs in Vorel.

What’s NOT supported today

  • Replay individual deliveries via API. Operator-only via the dashboard.
  • Multi-event filters per subscription beyond the event-name set. No “deliver lead.created only when attributes.budget_max > 5_000_000” today; filter consumer-side.
  • offering.deleted: there’s no DELETE-offering API path; soft-delete happens via the operator dashboard. The event will be added in lockstep when the tenant-side delete surface lands.
  • Per-event payload schemas in this docs site. Documented in the OpenAPI spec per-resource for now; a dedicated payload page is roadmap.