Skip to main content
signingSecret is in the create response and nowhere else. Store it. Every delivery carries x-thinnest-signature: sha256=…, an HMAC of the raw body under this secret, and x-thinnest-signature-v2, which also signs the delivery time — see Delivery.

Endpoints

Fields

string
required
Whose events. Required on create — unless includeCustomers is true.
boolean
For a workspace with customers: one endpoint for the events of every agent in every one of your customers. It has no agent (send one or the other, not both). Each delivery’s data carries workspaceId — the customer it is about — inside the signed body.
string
required
An https:// address, or an email address — a helpdesk that opens tickets by mail gets one email per event. Addresses inside our own network are refused.
string[]
Which events to send. Leave it out for every event, now and as new ones are added; the response then reads ["*"].
boolean
Switching an endpoint back on forgives its past failures.
call.started is not a webhook event you subscribe to. It is the request your own system receives from an agent’s Who-is-calling lookup, signed the same way, which you answer with values for the agent to use.

Problems on a live call

call.problem is sent while the call is still going on, so you can alert somebody or flag the call before it ends. Each kind is sent at most once per call, and a burst across many calls is held to about 20 events a minute per agent. An event held back by that limit is not queued: it is sent if that kind happens again on the same call once the agent is back under the limit, and otherwise it is not sent at all. The same problems are also on the call itself, under Problems on this call in Usage.
callId is the id Get Call takes. detail is a sentence you can show a person.

Call quality

Every call the agent ran carries quality, written once when the call ends — on Get Call and in data.quality on call.completed and call.analysed:
quality is null for a call that was not measured (a call from before these figures were recorded, or one that never connected), and a figure inside it is null when it could not be measured on that call. On call.completed it can also be null when the call ended before its figures were ready; call.analysed and Get Call have them by then. In the console the same figures are under How the call went when you open the call in Usage.

Calls handed to another agent

When an agent passes a live call to another of your agents (how), call.analysed and Get Call list it in handedTo, oldest first — empty when the call stayed with agent, which is always the agent the call was placed or answered as:
A hand-off to another agent is not a hand-over to a person, so it does not send conversation.escalated. Which agent an event names. The call-level events — call.completed, call.analysed and call.problem — stay with the agent the call started with, which is also the one the call is billed and summarised under. Events the agents themselves raise mid-call — lead.captured, conversation.escalated — name the agent that ran the tool in data.agentId and carry data.callId, so on a handed call you can join them to the call report and to handedTo. Webhooks belong to an agent. An event raised by the agent a call was handed to goes to that agent’s webhooks, not to the agent that took the call first. If you only subscribe the first agent, you will get call.completed and call.analysed for the whole call but miss any lead.captured or conversation.escalated the second agent raises. Add the same endpoint to every agent a call can be handed to.

Delivery

Each event is a POST of JSON:
with these headers on every attempt (an email destination gets the ticket email only, without them):

Retries

Anything other than a 2xx answer — an error, a timeout after 10 seconds, an address we cannot reach — is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours: six attempts over about 8½ hours. Every attempt sends the same bytes, so sentAt is when the event happened, not when the attempt left. Payloads are kept for 7 days, so anything that still did not arrive can be sent again within that week. Re-sending a delivery starts its retry schedule afresh. Retries and re-sends go to the endpoint’s current address — change the URL and pending retries follow it. A Slack or helpdesk destination that accepted a post but answered after the 10-second timeout can receive it twice, since neither can dedupe on the event id.

What your receiver should do

  1. Verify x-thinnest-signature-v2 (or x-thinnest-signature) with a constant-time compare.
  2. Check freshness on x-thinnest-delivered-at — reject one more than five minutes from your clock. Not on sentAt: a retry’s sentAt is hours old by design. The delivery time is covered by the v2 signature, so it cannot be moved without breaking it.
  3. Dedupe on x-thinnest-event-id. A retry after a timeout can reach you after you already handled the first attempt.
  4. Answer 2xx quickly, then do the work.

Switched off

An endpoint is switched off — enabled: false, delivery.lastError says why — after five events in a row that each failed every attempt. A failed attempt that a retry then delivers does not count. Events that happen while it is off are recorded as not sent; fix the endpoint, PATCH { "enabled": true }, and re-send them — nothing within the last 7 days is lost.

Event payloads

Every delivery is the same envelope. What changes is data, shown here in full for each event. Ids in data are the same prefixed ids the API returns and takes: conv_… for a conversation (pass it to List Conversation Messages), cmp_… for a campaign, and out_…, sch_… or the call reference for a call (pass it to Get Call). conversationId and campaignId always carry their conv_ and cmp_ prefix, so a value from a webhook can be passed straight to the API. If your receiver stored these ids as bare identifiers before, store the prefixed value from now on. An endpoint created with includeCustomers: true also gets data.workspaceId (org_…), the customer the event is about.

lead.captured

The agent collected contact details. name, email and phone are each null when not given; note is what the customer wants, written for whoever follows up. agentId is the agent that captured it and callId the call it was on (the id Get Call takes; null off a call).

conversation.escalated

The agent handed the conversation to a person. reason is the agent’s own words. agentId is the agent that handed over and callId the call it was on (null off a call).

conversation.resolved

A teammate marked the conversation done. Sent once, when the status actually changes.

call.completed

The moment a call ends, before it has been read. outcome is completed, missed or failed; hangup is answered, no_answer, busy, rejected, unreachable, cancelled, voicemail, before_agent or failed; phone is null on a web call. campaignId is present only for a campaign’s call, and id, reference and metadata only for a call placed through the API.
quality is how the call went in numbers (see Call quality); it can be null when the call ended before its figures were ready. forwardedTo appears only on a call your number forwarded to a phone, and is that phone’s number; it is left out of every other call.

call.analysed

The call once it has been read: exactly the object Get Call returns. quality can be null (see Call quality), handedTo is empty unless the call was handed to another agent, and forwardedTo is null unless the call was forwarded to a phone.

campaign.finished

Everyone on a broadcast (kind: "whatsapp") or a calling campaign (kind: "voice") has been reached or given up on. skipped is people who withdrew consent before their turn.

contact.opted_out

During a call, somebody asked not to be called again and their number went on the do-not-call list. callId is the call’s id; source is always "call"; reason is what the caller said, as recorded (up to 160 characters, in the language they spoke), or null when the agent did not record their words.

call.problem

Something went wrong while a call was live. kind is no_answer_from_model, byok_failed, slow_reply or voice_failed; detail says what happened in a sentence; callId is the id Get Call takes. Sent at most once per kind per call.

Workspace events

contact.erased and byok.changed are about your whole workspace rather than one agent. They go to every endpoint in the workspace that subscribed to them, whichever agent you added it on, and once per address: if three agents post to the same URL, that URL still gets one delivery. If you are a developer, an endpoint that receives every customer’s events (includeCustomers) gets them for your customers too, with workspaceId. An endpoint subscribed to every event (["*"]) receives them as well. Neither event carries personal details or any part of a key.

contact.erased

A contact you deleted with Delete Contact is gone completely, recordings included. It is sent once: straight away when Delete Contact answered 204, or — when it answered recordingsPending because a recording could not be deleted at that moment — from the nightly retry, once the last of those recordings is gone. A recording that was still being written when you deleted the contact can take up to three nights to be confirmed gone. contactId is the id you deleted.

byok.changed

Your own keys changed. change is one of: kind (stt, llm or tts) and provider name the key for the four key changes, and are null for the others. byokEnabled and scope say where your workspace stands after the change. Choosing a different model for a key is not sent. Saved keys are not re-checked on a schedule: a model key that stops working on a call shows up as call.problem (byok_failed).

Re-send what failed

One delivery, now — the answer says whether it arrived:
Or everything that has not succeeded since a time, at most 7 days back — queued and sent within a minute:
The same body, event id and sentAt as the first attempt, with x-thinnest-attempt one higher. The endpoint must be switched on. The console’s Re-send failed button on the endpoint does the same for the last 7 days. A build key may do this.

Test an endpoint

Sends one sample lead.captured — marked "test": true, with obviously fake details — the same way a real event goes: signed JSON, Slack’s own format for a Slack URL, or a ticket email for an email address. A failed test counts towards the five failures that switch an endpoint off.

Delivery history

One row per event, however many attempts it took, newest first — the last 7 days (or the last 50, whichever is more). The answer to “why did leads stop reaching our CRM on Tuesday?”. status and error are from the latest attempt; 0 means we could not reach it. nextRetryAt is when the next automatic attempt is due — null once it arrived or ran out of retries. The payloads themselves are not returned: they carry your customers’ details.