Skip to main content
The call.incoming webhook is a blocking request. When an inbound telephony call or a webhook-mode widget session starts, ThunderPhone calls your server and waits up to 2 seconds for a configuration response. Use this event to dynamically choose an agent, rewrite a prompt, or inject tools per call.
In the endpoint-based delivery system, the equivalent event types are telephony.incoming (phone calls) and web.incoming (widget / mic). The legacy single-URL webhook uses call.incoming for both. Subscribe to whichever name applies to your integration style.
If your handler returns a non-2xx status or times out, ThunderPhone falls back to the agent already assigned to the phone number or publishable key.

Request payload

{
  "type": "call.incoming",
  "data": {
    "call_id":     987654321,
    "from_number": "+14155550199",
    "to_number":   "+15551234567"
  }
}
FieldTypeDescription
call_idintegerCall id — stable across all events for this call
from_numberstringE.164 caller number. "web" for widget/mic sessions
to_numberstringE.164 destination (one of your ThunderPhone numbers)

Response schema

Return a JSON object describing the agent configuration for this call. All unknown top-level keys are rejected — bring anything you need under tools[i].
{
  "prompt":  "You are a helpful booking assistant for Acme Restaurant.",
  "voice":   "en-US-James1",
  "product": "spark",
  "background_track": null,
  "tools":   []
}
FieldTypeRequiredDescription
promptstringyesSystem prompt driving the agent
voicestringyesVoice id from GET /v1/voices. voice_name is accepted as an alias
productstringnoDefaults to spark. Allowed: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack
thinking_levelstringnobase or extra. Derived from product for Storm tiers
background_trackstring | nullnoAmbient audio id or null
acknowledgement_prompt_modestringnoauto or manual (Storm-with-ack products)
acknowledgement_promptstringnoRequired if acknowledgement_prompt_mode="manual"
toolsarraynoInline function-tool schemas (see Function Tools)
Speak-order and max_hold_seconds are not accepted here — they are only configurable on the Agent itself. Any other top-level keys are rejected with 400.
Returning {} (empty object), null, or a non-2xx status uses the agent that was statically configured on the phone number or publishable key.

Example handler

import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
WEBHOOK_SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]

def verify(body: bytes, signature: str) -> bool:
    expected = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

@app.post("/thunderphone-webhook")
async def webhook(request: Request):
    body = await request.body()
    if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
        raise HTTPException(status_code=401)

    event = json.loads(body)
    if event["type"] in ("call.incoming", "telephony.incoming"):
        caller = event["data"]["from_number"]
        prompt = (
            "Greet the caller as a San Francisco local…"
            if caller.startswith("+1415")
            else "You are a friendly customer support agent…"
        )
        return {
            "prompt": prompt,
            "voice": "en-US-James1",
            "product": "spark",
        }
    return {}

Response with function tools

Attach tools so the AI can call your APIs mid-conversation:
{
  "prompt":  "You are a booking assistant. Use the available tools to help customers schedule appointments.",
  "voice":   "en-US-James1",
  "product": "spark",
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "search_appointments",
        "description": "Find available appointment slots",
        "parameters": {
          "type": "object",
          "properties": {
            "date": { "type": "string", "description": "YYYY-MM-DD" },
            "service": { "type": "string" }
          },
          "required": ["date"]
        }
      },
      "endpoint": {
        "url": "https://api.example.com/appointments/search",
        "method": "POST",
        "headers": {
          "X-Api-Key": "your-key"
        }
      }
    }
  ]
}
Endpoints use the same signing key your webhook uses. See Function Tools for the exact shape and the signed request format.

Product tier cheat sheet

ProductLatencyReasoningAcknowledgement
sparkLowestBasic
boltLowImproved
storm-baseMediumStrong
storm-base-with-ackMediumStrongAuto filler while thinking
storm-extraHigherDeep
storm-extra-with-ackHigherDeepAuto filler while thinking

call.complete

The non-blocking end-of-call event.

Function Tools

Full JSON schema for tools[] and the signed endpoint contract.

Webhook endpoints

Subscribe multiple URLs to telephony.incoming / web.incoming.