# Webhooks

Use webhooks when your application needs to react to terminal call task results without polling.

Phone values in examples are placeholders. Real webhook payloads echo the phone numbers from your call task.

## Terminal events

CALL-E sends terminal webhook events:

- `call.completed`
- `call.failed`
- `call.result_validation_failed`

`call.result_validation_failed` is sent when a completed call task had an internal structured-result validation failure for the task or a recipient. The payload does not expose validation details; invalid or unsupported structured results are returned as `null`. Failed or canceled terminal call tasks use `call.failed`.

Each event has an `id`, `type`, `created_at`, and a `data` object containing the terminal call task fields.

Recipient-level structured results appear inside the `recipients` array. They do not create separate recipient webhook events.

Attempt transcripts appear as `recipients[].attempts[].transcript_turns`. Each turn includes `offset_seconds`, `speaker`, and `text`; the array is empty when no transcript is available.

Example payload:

```json
{
  "id": "evt_123",
  "type": "call.completed",
  "created_at": "2026-06-08T18:30:00Z",
  "data": {
    "id": "call_123",
    "object": "call_task",
    "status": "completed",
    "task": "Call each recipient and ask whether they can attend Friday lunch in San Francisco.",
    "recipients": [
      {
        "id": "rcp_001",
        "phones": ["<RECIPIENT_1_E164_PHONE>"],
        "region": "US",
        "locale": "en-US",
        "status": "completed",
        "structured_result": {
          "can_attend": "yes"
        },
        "summary": "The recipient can attend Friday lunch.",
        "attempts": [
          {
            "id": "att_001",
            "phone": "<RECIPIENT_1_E164_PHONE>",
            "status": "completed",
            "started_at": "2026-06-08T18:21:00Z",
            "completed_at": "2026-06-08T18:29:00Z",
            "summary": null,
            "transcript_turns": [
              {
                "offset_seconds": 0,
                "speaker": "bot",
                "text": "Can you attend Friday lunch in San Francisco?"
              },
              {
                "offset_seconds": 8,
                "speaker": "user",
                "text": "Yes, I can attend."
              }
            ],
            "provider_call_id": "provider_001",
            "failure_code": null,
            "failure_message": null
          }
        ]
      },
      {
        "id": "rcp_002",
        "phones": ["<RECIPIENT_2_E164_PHONE>"],
        "region": "US",
        "locale": "en-US",
        "status": "completed",
        "structured_result": null,
        "summary": "The recipient did not provide a usable answer.",
        "attempts": [
          {
            "id": "att_002",
            "phone": "<RECIPIENT_2_E164_PHONE>",
            "status": "completed",
            "started_at": "2026-06-08T18:22:00Z",
            "completed_at": "2026-06-08T18:30:00Z",
            "summary": null,
            "transcript_turns": [],
            "provider_call_id": "provider_002",
            "failure_code": null,
            "failure_message": null
          }
        ]
      }
    ],
    "structured_result": {
      "attending_count": 1
    },
    "summary": "One recipient can attend Friday lunch.",
    "task_completed": true,
    "completion_confidence": {
      "score": 0.86,
      "label": "high"
    },
    "evidence": [
      "One recipient confirmed they can attend.",
      "The second recipient did not provide a usable answer."
    ],
    "metadata": {
      "workflow_run_id": "wf_123"
    },
    "failure_code": null,
    "failure_message": null,
    "created_at": "2026-06-08T18:20:00Z",
    "completed_at": "2026-06-08T18:30:00Z"
  }
}
```

## Signature verification

Verify the signature against the raw request body before parsing JSON. The SDK helpers return the parsed event only after the timestamp and signature are valid.

TypeScript:

```ts
const event = client.webhooks.unwrap({
  rawBody,
  headers,
  secret: process.env.CALLE_WEBHOOK_SECRET!,
});

if (event.type === "call.completed") {
  console.log(event.data.id, event.data.recipients);
}
```

Python:

```python
event = client.webhooks.unwrap(
    raw_body=raw_body,
    headers=headers,
    secret=os.environ["CALLE_WEBHOOK_SECRET"],
)

if event["type"] == "call.completed":
    print(event["data"]["id"], event["data"]["recipients"])
```

## Example servers

The SDK repositories include minimal webhook servers built with runtime standard libraries:

- [TypeScript webhook server](https://github.com/CALLE-AI/server-sdk-typescript/blob/main/examples/webhook-server.ts)
- [Python webhook server](https://github.com/CALLE-AI/server-sdk-python/blob/main/examples/webhook_server.py)

Both examples listen on `POST /calle/webhook`, preserve the raw request body for signature verification, and return `200` only after `client.webhooks.unwrap` succeeds.

## Idempotent handling

Webhook delivery is at least once. Store the webhook event `id` before processing side effects so duplicate deliveries are ignored safely.

TypeScript:

```ts
if (await eventStore.has(event.id)) {
  return new Response("duplicate", { status: 200 });
}

await eventStore.insert(event.id);
await handleCallEvent(event);
```

Python:

```python
if event_store.has(event["id"]):
    return {"ok": True, "duplicate": True}

event_store.insert(event["id"])
handle_call_event(event)
```
