---
title: Webhooks
description: How Receptiva notifies your own system when a call finishes or a lead changes, how to verify a delivery, how retries work, and how to test and troubleshoot an endpoint.
sidebarTitle: Webhooks
section: api
order: 35
updated: 2026-10-05
---

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](/docs/api-overview) with its [API key](/docs/api-keys). 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**.

1. Choose **Add endpoint** and enter the URL of your system's receiver. It must be a public `https://` address.
2. Pick the events it should receive.
3. 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.
4. 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:

```json
{
  "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](https://www.standardwebhooks.com) 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. |

```js
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 `id` of each event you have handled and skip one you have seen.
- **Events can arrive out of order.** `lead.created` can arrive before `call.completed` for 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](/docs/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](/docs/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.

## Next

- [Keep another system in sync](/docs/sync-guide)
- [API keys](/docs/api-keys)
- [API reference](/docs/api-reference)
