Webhooks Reference
Threll delivers events to your HTTPS endpoint as JSON. Async events are fire-and-forget; sync events expect a JSON response that influences the call.
Pick an event or topic from the sidebar.
Threll delivers events to your HTTPS endpoint as JSON. Async events are fire-and-forget; sync events expect a JSON response that influences the call.
Pick an event or topic from the sidebar.
Every webhook delivery carries the following headers:
| Header | Description |
|---|---|
X-Threll-Event | Event type, e.g. call.transcript |
X-Threll-Event-Id | Unique id for this delivery attempt |
X-Threll-Timestamp | ISO 8601 timestamp when the event was generated |
X-Threll-Delivery | "sync" or "async" |
X-Threll-Account-Id | Your account ID |
X-Threll-Signature | Hex-encoded HMAC-SHA256 of the raw body, computed with the subscription signing secret |
Content-Type | application/json |
Pre-call hook fired right before a call starts. Your endpoint returns the worker configuration to use for the call.
Fires immediately before the outbound call starts (or on inbound entry) so scheduled calls re-resolve their configuration when they actually run.
Return JSON with the worker configuration overrides to merge onto the call before the voice session starts.
| Field | Type | Required | Description |
|---|---|---|---|
config | object | Partial worker configuration overrides (voice, llm, routing) merged onto the call before the session starts. | |
instructions | string | Full prompt for the worker. When provided, replaces the instructions generated from the worker call script. | |
tools | object[] | External tools the worker can call during this call. Invocations are delivered as signed call.tool_call webhooks to each tool deliveryUrl. | |
metadata | object | Opaque key/value bag. Threll persists it on the call and echoes it back on every subsequent call.* event so you can correlate events with your own records. |
{
"instructions": "You are calling Acme Corp to follow up on their demo request. Be concise and friendly.",
"config": {
"voiceId": "nova",
"llmTemperature": 0.4
},
"tools": [
{
"name": "lookup_account",
"description": "Look up an account by its number.",
"inputSchema": {
"type": "object",
"properties": {
"accountNumber": {
"type": "string"
}
},
"required": [
"accountNumber"
]
},
"deliveryUrl": "https://example.com/threll/tools",
"signingSecret": "whsec_your_tool_signing_secret"
}
]
} | Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | |
data | object | yes | |
data.callId | string | yes | Unique identifier for the phone call |
data.accountId | string | yes | Your account ID |
data.workerId | string | yes | Worker that will handle the call |
data.direction | "inbound" | "outbound" | yes | Call direction |
data.transport | string | Call transport (e.g. twilio, sip_trunk, webrtc) | |
data.customer | object | Remote party identity if known at request time | |
data.customer.number | string | Remote phone number in E.164 format | |
data.customer.name | stringnull | Customer name if known | |
data.customer.personId | stringnull | Threll person record identifier if matched | |
data.customer.language | stringnull | BCP-47 language tag | |
data.metadata | object | Opaque metadata supplied when the call was created (e.g. via web-calls), echoed here so you can correlate this pre-call hook. Absent if none was set. |
{
"type": "call.worker_request",
"data": {
"callId": "5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e",
"accountId": "9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b",
"workerId": "2b7d9a4c-6e3f-48a1-9c5d-8e0b4f2a1c8d",
"direction": "outbound",
"customer": {
"number": "+15555550100",
"name": "Acme Corp"
}
}
} Mid-call external tool dispatch. Threll forwards a tool call from the worker to your endpoint, which returns the tool result.
Fires every time the worker invokes an externally-registered tool (provider="external") during an active call. The voice session pauses on the response.
Return JSON with the tool result that should be presented back to the worker.
| Field | Type | Required | Description |
|---|---|---|---|
result | any | The tool result presented back to the worker. Any JSON value. |
{
"result": {
"accountNumber": "12345",
"name": "Acme Corp",
"plan": "enterprise",
"balanceDue": 0
}
} | Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | |
data | object | yes | |
data.callId | string | yes | Phone call identifier |
data.accountId | string | yes | Your account ID |
data.workerId | string | yes | Worker that invoked the tool |
data.toolCallId | string | yes | Unique id for this tool call; use to correlate logs |
data.name | string | yes | Name of the externally-registered tool |
data.arguments | object | yes | JSON arguments the worker passed in |
data.metadata | object | Opaque metadata you returned from call.worker_request, echoed back for correlation. Absent if you did not set any. |
{
"type": "call.tool_call",
"data": {
"callId": "5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e",
"accountId": "9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b",
"workerId": "2b7d9a4c-6e3f-48a1-9c5d-8e0b4f2a1c8d",
"toolCallId": "c8d4e9f2-1a3b-4c7d-8e0f-9a2b3c4d5e6f",
"name": "lookup_account",
"arguments": {
"accountNumber": "12345"
}
}
} A transcript turn (final or interim depending on the worker config) recorded during the call.
Streamed as transcript turns are committed. Fire-and-forget delivery; no response expected.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | |
data | object | yes | |
data.callId | string | yes | Phone call identifier |
data.turnIndex | integer | yes | Monotonic turn index within the call |
data.role | "user" | "agent" | yes | Who produced this turn |
data.text | string | yes | Turn text |
data.isFinal | boolean | yes | true when this turn is final; false for partial/interim |
data.confidence | numbernull | Transcription confidence (0-1). Null for engines that do not report it (e.g. Gemini). | |
data.source | stringnull | Engine that produced the text (e.g. gemini, whisper-nb). | |
data.ts | string | yes | ISO 8601 timestamp |
data.metadata | object | Opaque metadata you returned from call.worker_request, echoed back for correlation. Absent if you did not set any. |
{
"type": "call.transcript",
"data": {
"callId": "5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e",
"turnIndex": 0,
"role": "user",
"text": "Hi, I have a question about your enterprise plan.",
"isFinal": true,
"confidence": 0.92,
"source": "whisper-nb",
"ts": "2026-06-08T10:00:15Z"
}
} Lifecycle status change for the call (ringing, in_progress, etc.).
Fires whenever the call status changes during its lifecycle. Fire-and-forget.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | |
data | object | yes | |
data.callId | string | yes | Phone call identifier |
data.status | string | yes | Threll-normalized call status |
data.providerStatus | stringnull | Underlying provider status (Twilio/Jambonz/etc.) when available | |
data.ts | string | yes | ISO 8601 timestamp of the status change |
data.metadata | object | Opaque metadata you returned from call.worker_request, echoed back for correlation. Absent if you did not set any. |
{
"type": "call.status_update",
"data": {
"callId": "5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e",
"status": "in_progress",
"providerStatus": "ringing",
"ts": "2026-06-08T10:00:15Z"
}
} Final event for the call, delivered exactly once. Once you receive this, no further events for the call will be delivered. When status is "failed", failedReason carries a machine-readable cause (see the payload schema for the full list).
Fires once a dialed call reaches a terminal status: completed, voicemail, no_answer, busy, canceled or failed. For a call that was answered and recorded, it fires after post-processing, so transcript and nextAction are populated. For a call that never connected — voicemail, no answer, a failed dial — there is nothing to analyse, so those fields are omitted and only the call metadata is present. A scheduled call that is canceled before dialing starts does not emit this event; track those via its status instead.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | yes | |
data | object | yes | |
data.callId | string | yes | Phone call identifier |
data.status | string | yes | Final call status |
data.failedReason | any | Machine-readable reason the call failed, present when status is "failed". Absent for calls that failed at the carrier without a known reason. One of: missing_phone_numbers — The call had no from or to number set, so it could never be placed. worker_request_required_failed — A required call.worker_request hook failed or aborted, so the call was not started. voice_session_unavailable — The Threll voice session could not be created for an outbound carrier call (voice server misconfigured or unavailable). voice_session_creation_failed — The Threll voice session could not be created for a web (browser) call. twilio_call_failed — Placing the outbound call via Twilio failed before the carrier call was created (missing integration, auth, or Twilio API error). jambonz_call_failed — Placing the outbound call via the SIP/Jambonz provider failed before the carrier call was created. webrtc_client_never_connected — A scheduled web call expired because the browser client never connected. cancelled_by_account_admin — A scheduled call was cancelled by an account admin before it ran. | |
data.accountId | string | yes | Your account ID |
data.workerId | string | yes | Worker that handled the call |
data.direction | "inbound" | "outbound" | Call direction | |
data.fromNumber | stringnull | Caller number in E.164, when available | |
data.toNumber | stringnull | Callee number in E.164, when available | |
data.duration | numbernull | Call duration in seconds | |
data.scheduledAt | stringnull | ISO 8601 | |
data.startedAt | stringnull | ISO 8601 | |
data.endedAt | stringnull | ISO 8601 | |
data.transcript | any | Full transcript turns, when available | |
data.recordingAvailable | boolean | yes | true when a recording exists. Fetch a temporary download link from GET /v1/accounts/{accountId}/phone-calls/{callId}/audio/url. |
data.nextAction | object | The next action resolved for the call, when one was configured. | |
data.nextAction.key | string | yes | Machine key of the resolved next action |
data.nextAction.name | string | yes | Human-readable next-action name |
data.nextAction.data | object | Structured data captured for the next action | |
data.personId | stringnull | Threll person record identifier, when matched | |
data.metadata | object | Opaque metadata you returned from call.worker_request, echoed back for correlation. Absent if you did not set any. |
{
"type": "call.ended",
"data": {
"callId": "5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e",
"status": "completed",
"failedReason": null,
"accountId": "9c3e8f02-7a14-4b62-bc59-1d8e5fa3027b",
"workerId": "2b7d9a4c-6e3f-48a1-9c5d-8e0b4f2a1c8d",
"direction": "outbound",
"fromNumber": "+15555550111",
"toNumber": "+15555550100",
"duration": 96,
"startedAt": "2026-06-08T10:00:00Z",
"endedAt": "2026-06-08T10:01:36Z",
"recordingAvailable": true,
"personId": "b2c3d4e5-6f7a-48b9-0c1d-2e3f4a5b6c7d"
}
}
Every delivery is signed with the subscription's signing secret using
HMAC-SHA256 over the raw request body. Compare the
hex digest to the X-Threll-Signature header using a
constant-time comparison.
echo -n "$RAW_REQUEST_BODY" | \
openssl dgst -sha256 -hmac "whsec_..." | \
awk '{print $2}' Async events are delivered fire-and-forget via the event bus; sync events are delivered inline and the response is fed back into the call.
X-Threll-Event-Id; treat handlers as idempotent.
Use the subscription Test action in Settings → Developer
(or POST /v1/accounts/{accountId}/webhook-subscriptions/{id}/test)
to dispatch a synthetic event matching the subscription's event type.