Skip to content

Webhooks

Webhooks push call events to your server as they happen — no polling required. You register an endpoint, Rindee POSTs a signed JSON payload to it for each matching event, and you verify the signature before trusting it.

Create a webhook endpoint (requires the webhooks capability):

Terminal window
curl -X POST https://api.rindee.dev/v1/webhook-endpoints \
-H "Authorization: Bearer rk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Call events",
"url": "https://example.com/hooks/rindee",
"secret": "whsec_choose_a_long_random_string",
"outcomes": []
}'
  • url — your HTTPS endpoint. Must start with http:// or https://.
  • secret — a secret you choose; used to sign every delivery so you can verify authenticity. Store it on your server. The API never returns it back (responses expose only has_secret: true).
  • outcomes — filter which call outcomes deliver. An empty list means all.

Each delivery is a JSON body with a stable envelope plus call fields:

{
"event": "call.completed",
"call_id": "0b9b…",
"org_id": "7c1a…",
"status": "completed",
"sequence": 7,
"occurred_at": "2026-06-29T12:34:56Z",
"phone_number": "+15551234567",
"direction": "outbound",
"outcome": "positive",
"end_reason": "completed",
"duration_seconds": 142,
"started_at": "2026-06-29T12:32:34Z",
"ended_at": "2026-06-29T12:34:56Z",
"agent_id": "a1b2…",
"contact_id": "c3d4…",
"campaign_id": null
}

event ranges over the call lifecycle (e.g. call.queuedcall.completed). Process events in sequence order — it carries causal ordering and never disagrees with the event timeline. Deliveries can arrive out of order or more than once, so make your handler idempotent (key on call_id + sequence).

Every request includes these reserved headers (they can’t be overridden by custom headers):

Header Value
Content-Type application/json
X-Rindee-Delivery-Id A UUID unique to this delivery attempt.
X-Rindee-Signature sha256=<hex> — HMAC-SHA256 of the raw body, keyed by your secret.

Compute HMAC-SHA256 over the raw request body (the exact bytes received, before any JSON parsing) using your endpoint’s secret, then compare it to the hex digest after sha256= using a constant-time comparison.

import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
received = header.removeprefix("sha256=")
return hmac.compare_digest(expected, received)
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const received = header.replace(/^sha256=/, "");
const a = Buffer.from(expected), b = Buffer.from(received);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Reject any request whose signature doesn’t match. Respond 2xx once you’ve accepted the event; non-2xx responses are retried.

Per endpoint you can reshape what’s sent (each capped at 20 entries):

  • field_aliases — rename payload keys, e.g. { "phone_number": "caller_phone" }.
  • extra_payload — add static keys to every delivery.
  • custom_headers — add HTTP headers (reserved headers above always win).

The signature is computed over the final, customized body — so verify against the raw bytes you actually receive.

Inspect delivery history and retry status:

Terminal window
curl https://api.rindee.dev/v1/webhook-deliveries \
-H "Authorization: Bearer rk_live_your_key_here"

Failed deliveries are retried automatically. See the API reference for the full delivery schema.