A webhook is an HTTPS request Receptiva sends to your system when something happens at your firm: a call finishes, a lead is created, a lead's status or your own details on it change. Your system no longer has to poll to find out.
A webhook is a notice, not the data. It carries ids and a few routing facts, never the caller's name, number, summary or transcript. Your system reads the details from the REST API with its API key. So a mistyped webhook URL never sends a caller's details to the wrong place.
Set up an endpoint#
Only the firm's owner can add or change endpoints, in the Receptiva dashboard under Integrations, then Webhooks.
- Choose Add endpoint and enter the URL of your system's receiver. It must be a public
https://address. - Pick the events it should receive.
- Copy the signing secret. It starts with
whsec_and is shown once. Store it where your receiver can read it, the way you store an API key. - Choose Send test event and check that your receiver answers.
A firm can have up to 5 endpoints. To change an endpoint's URL, delete it and add a new one, which gives it a new secret.
Events#
| Event | Sent when |
|---|---|
call.completed |
A call has ended and Receptiva has finished processing it. Sent for every call, including a caller who hung up before the receptionist spoke. |
lead.created |
A call produced a lead. |
lead.updated |
A lead's status changed through the API or an AI assistant. |
webhook.test |
You asked for a test event. Sent to that one endpoint, whatever it is subscribed to. |
To act only on leads, subscribe to lead.created. To act on some calls only, subscribe to call.completed and check category, urgency, answered or lead_id in the payload.
What a delivery looks like#
Every delivery is a POST with a JSON body in the same envelope:
{
"id": "evt_9hQ2mT4aBk7Q2xP9mT4aBk7Q",
"type": "call.completed",
"timestamp": "2026-10-05T14:03:22.000Z",
"org": { "id": "5f0c1a52-6f0e-4a7e-9d59-0f2a3a1f8c11" },
"test": false,
"data": {
"call_id": "k7Q2xP9mT4aB",
"occurred_at": "2026-10-05T13:58:41+00:00",
"answered": true,
"duration_s": 214,
"category": "new_injury_lead",
"urgency": "high",
"outcome": "connected",
"lead_id": "0b6c7c0e-4a51-4f0e-8f5e-7b1d2c3a4e5f",
"has_recording": true,
"url": "https://api.receptiva.ai/v1/calls/k7Q2xP9mT4aB",
"dashboard_url": "https://your-firm.receptiva.ai/?call=k7Q2xP9mT4aB"
}
}org.id is your firm's stable id, the same one GET /v1/account returns. test is true only on a webhook.test event.
data by event:
| Event | Fields |
|---|---|
call.completed |
call_id, occurred_at, answered, duration_s, category, urgency, outcome, lead_id (or null), has_recording, url, dashboard_url |
lead.created |
lead_id, call_id, status (always new), category, urgency, url |
lead.updated |
lead_id, call_id, status, previous_status, changes, changed_by, url |
webhook.test |
endpoint_id, message |
The values mean what they mean on the REST API: call_id and lead_id are the ids the calls and leads endpoints use, and url is the REST address of the call or lead.
On lead.updated, changes lists the lead's fields that changed: status, external_id and status_reason (your own id for the lead and your note on its status). When the status did not change, previous_status equals status. changed_by says who made the change: { "kind": "api_key", "id": "<the key's id>" } or { "kind": "connector", "id": "<the connected app>" }. If your own system set the status with its API key, the event that follows carries that key's id (the api_key.id from GET /v1/account), so you can skip it.
New fields may be added to data, and new event types may be added. Ignore fields and types you do not know.
Verify a delivery#
Check every delivery before you trust it. Receptiva signs deliveries the Standard Webhooks way, so you can use one of its libraries in your language.
Each delivery has three headers:
| Header | Value |
|---|---|
webhook-id |
The event's id. The same on every retry of that event. |
webhook-timestamp |
When this attempt was sent, in seconds since 1970. |
webhook-signature |
v1, followed by the signature. |
import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.RECEPTIVA_WEBHOOK_SECRET);
// rawBody must be the exact bytes of the request body, before any JSON parsing.
app.post("/receptiva", express.raw({ type: "application/json" }), (req, res) => {
let event;
try {
event = wh.verify(req.body, req.headers);
} catch {
return res.status(400).end();
}
res.status(204).end(); // answer first, then do the work
handle(event);
});Without a library: the signature is the base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of your secret after whsec_. Compare it in constant time, and refuse a delivery whose timestamp is more than 5 minutes from your clock.
Answer quickly#
Answer with any 2xx status within 10 seconds. Receptiva treats anything else as a failed attempt: another status, a redirect, a timeout or a connection error. Save the event and do slow work after you have answered.
Retries#
A failed delivery is tried again, up to 8 attempts in all, with the wait growing each time: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, then 24 hours. That is about two days.
- The same event can arrive more than once. Keep the
idof each event you have handled and skip one you have seen. - Events can arrive out of order.
lead.createdcan arrive beforecall.completedfor the same call. Treat an event as a reason to read the current call or lead from the REST API, not as the current state. - If an endpoint answers
410 Gone, Receptiva turns it off straight away. - A test event is sent once and is not retried, and a failed test never counts against the endpoint.
- If a delivery runs out of attempts, Receptiva emails the firm's owner. If the endpoint has had no successful delivery for 3 days by then, Receptiva turns it off and emails the owner again. The owner can turn it back on in the dashboard.
Events sent while an endpoint was off are not sent again by themselves. Read what you missed with GET /v1/events, or resend single deliveries.
See what was sent#
The dashboard shows each endpoint's recent deliveries: the event, whether it succeeded, each attempt's status code and the start of your receiver's response. From there the owner can resend a delivery or send a test event.
The same is available on the REST API, so an integration, or an AI coding assistant working on it, can check its own deliveries. These need an API key with the webhook permissions; keys created before webhooks were available do not have them, so create a new key.
| Request | Scope | Returns |
|---|---|---|
GET /v1/webhook-endpoints |
webhooks:read |
Your endpoints and their health. Never a signing secret. |
GET /v1/webhook-deliveries |
webhooks:read |
Deliveries, newest first. Filter with endpoint_id, event_id or status. |
GET /v1/events |
webhooks:read |
Your firm's events, oldest first. Pass after=<event id> to read the ones that came after it. |
GET /v1/events/{event_id} |
webhooks:read |
One event. |
POST /v1/webhook-endpoints/{endpoint_id}/test |
webhooks:test |
Sends a webhook.test event to that endpoint. Answers 202 with the new delivery. |
POST /v1/webhook-deliveries/{delivery_id}/redeliver |
webhooks:test |
Sends that delivery's event to its endpoint again, as a new delivery. |
Tests and resends are limited to 5 per endpoint per minute, and an endpoint that is turned off answers 409. Events and deliveries are kept for 30 days. The full request and response shapes are in the API reference.
Adding, changing and deleting endpoints is done in the dashboard only.
Catch up after downtime#
Keep the id of the last event you handled. When your system comes back, call GET /v1/events?after=<that id> and follow next_after until has_more is false. Events older than 30 days are gone; if after names one of those, the request answers 400, and a full sync with updated_since (sync guide) brings you back in step.
Webhooks and polling work well together: let a webhook tell you something changed, and run the updated_since poll from the sync guide once a day as a safety net.