Threll.ai
Webhooks

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.

Webhooks

Request headers

Every webhook delivery carries the following headers:

HeaderDescription
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
Webhook event · sync

call.worker_request

Pre-call hook fired right before a call starts. Your endpoint returns the worker configuration to use for the call.

When it fires

Fires immediately before the outbound call starts (or on inbound entry) so scheduled calls re-resolve their configuration when they actually run.

Expected response

Return JSON with the worker configuration overrides to merge onto the call before the voice session starts.

Response fields

FieldTypeRequiredDescription
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.

Example response

call.worker_request response
{
  "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"
    }
  ]
}

Payload fields

FieldTypeRequiredDescription
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.

Example payload

call.worker_request
{
  "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"
    }
  }
}
Webhook event · sync

call.tool_call

Mid-call external tool dispatch. Threll forwards a tool call from the worker to your endpoint, which returns the tool result.

When it fires

Fires every time the worker invokes an externally-registered tool (provider="external") during an active call. The voice session pauses on the response.

Expected response

Return JSON with the tool result that should be presented back to the worker.

Response fields

FieldTypeRequiredDescription
result any The tool result presented back to the worker. Any JSON value.

Example response

call.tool_call response
{
  "result": {
    "accountNumber": "12345",
    "name": "Acme Corp",
    "plan": "enterprise",
    "balanceDue": 0
  }
}

Payload fields

FieldTypeRequiredDescription
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.

Example payload

call.tool_call
{
  "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"
    }
  }
}
Webhook event · async

call.transcript

A transcript turn (final or interim depending on the worker config) recorded during the call.

When it fires

Streamed as transcript turns are committed. Fire-and-forget delivery; no response expected.

Payload fields

FieldTypeRequiredDescription
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.

Example payload

call.transcript
{
  "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"
  }
}
Webhook event · async

call.status_update

Lifecycle status change for the call (ringing, in_progress, etc.).

When it fires

Fires whenever the call status changes during its lifecycle. Fire-and-forget.

Payload fields

FieldTypeRequiredDescription
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.

Example payload

call.status_update
{
  "type": "call.status_update",
  "data": {
    "callId": "5f1a3e7b-2c8d-49a6-b0e4-7d9f3c5a1b2e",
    "status": "in_progress",
    "providerStatus": "ringing",
    "ts": "2026-06-08T10:00:15Z"
  }
}
Webhook event · async

call.ended

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).

When it fires

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.

Payload fields

FieldTypeRequiredDescription
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.

Example payload

call.ended
{
  "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"
  }
}
Security

Signature verification

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.

Verify in bash
echo -n "$RAW_REQUEST_BODY" | \
  openssl dgst -sha256 -hmac "whsec_..." | \
  awk '{print $2}'
Delivery

Delivery & retries

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.

  • Sync deliveries time out after a few seconds. Return JSON quickly; on error or timeout, the call proceeds with defaults (unless the subscription is marked required).
  • Async deliveries are retried with exponential backoff on non-2xx responses.
  • Every delivery has a unique X-Threll-Event-Id; treat handlers as idempotent.
Delivery

Testing

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.