> ## Documentation Index
> Fetch the complete documentation index at: https://docs.topcalls.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive notifications when calls finish. Deliver transcripts, recordings, and analysis straight to your systems.

## What Are Webhooks?

Webhooks are HTTP callbacks that TopCalls sends to your server when a call finishes. They're the best way to integrate TopCalls with your existing systems.

<CardGroup cols={2}>
  <Card title="Real-Time Updates" icon="bolt">
    Get notified when calls complete or fail. No polling required.
  </Card>

  <Card title="Complete Data" icon="database">
    Receive full call details, transcripts, recordings, and analysis in a single webhook payload.
  </Card>

  <Card title="Reliable Delivery" icon="shield-check">
    Built-in retry logic ensures webhooks are delivered even if your server is temporarily unavailable.
  </Card>

  <Card title="Filterable" icon="filter">
    Subscribe to specific dispositions or conversions so your endpoint only receives the events it cares about.
  </Card>
</CardGroup>

## Setting Up Webhooks

There are two ways to receive webhooks.

### Per-Call Webhooks

Set `webhook_url` when creating a call. That URL receives one payload when the call completes, fails, or is cancelled:

```json theme={null}
{
  "phone_number": "+14155551234",
  "task": "Confirm appointment...",
  "webhook_url": "https://your-app.com/webhooks/call-complete",
  "webhook_call_status_filter": ["completed", "failed"]
}
```

`webhook_call_status_filter` is optional. When set, only calls whose final status matches a listed value fire the webhook; leave it out (or send an empty array) to receive every outcome. It applies only to the per-call `webhook_url`, not to account-level subscriptions.

### Account-Level Subscriptions

Subscribe a URL once and receive events for every matching call on your account, no matter how the call was created. Manage subscriptions through the API:

```bash theme={null}
curl -X POST https://api.topcalls.ai/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "call.completed",
    "url": "https://hooks.your-app.com/topcalls"
  }'
```

Subscribe to a specific disposition by adding its slug as a suffix. For example, if your campaign's post-call analysis labels a call `Booked Callback`, the event name is `call.completed.booked_callback`:

```json theme={null}
{
  "event": "call.completed.booked_callback",
  "url": "https://hooks.your-app.com/callbacks"
}
```

Narrow further with `filters`. Values match by equality, arrays match any element, strings with `%` match SQL-LIKE, and the special key `is_conversion` matches calls whose disposition is in the campaign's conversion outcomes:

```json theme={null}
{
  "event": "call.completed",
  "url": "https://hooks.your-app.com/conversions",
  "filters": {
    "is_conversion": true,
    "phone_number": "+1555%"
  }
}
```

Use `GET /v1/webhooks` to list subscriptions and `DELETE /v1/webhooks/{webhook_id}` to remove one. See the [API reference](/api-reference/introduction) for full schemas.

### Campaign Webhooks

Campaigns configure their webhook URL in the TopCalls dashboard. The campaign editor also lets you pick which call statuses fire the webhook (for example only answered calls, or only voicemail), which keeps noise out of your downstream tools.

## Webhook Payload

Here's a complete `call.completed` webhook payload:

```json theme={null}
{
  "call_id": "564d4fd4-03bc-400a-abe0-05540fbeff88",
  "phone_number": "+14155551234",
  "from_phone_number": "+18005551234",
  "status": "completed",
  "call_status": "completed",
  "duration": 2.5,
  "transcript": [
    {
      "id": 1,
      "user": "assistant",
      "text": "Hi, this is Rachel from TopView Dental...",
      "created_at": "2025-12-22T10:30:05Z"
    },
    {
      "id": 2,
      "user": "user",
      "text": "Yes, hi...",
      "created_at": "2025-12-22T10:30:08Z"
    }
  ],
  "recording_url": "https://api.topcalls.ai/recordings/.../2025/12/22/mp3",
  "call_summary": "Successfully confirmed appointment...",
  "error_message": null,
  "answered_by": "human",
  "campaign_id": "camp_abc123",
  "lead_id": "lead_xyz789",
  "created_at": "2025-12-22T10:30:00Z",
  "started_at": "2025-12-22T10:30:05Z",
  "end_at": "2025-12-22T10:32:35Z",
  "disposition": "Appointment Booked",
  "is_conversion": true,
  "analysis": {
    "call_outcome": "Appointment Booked",
    "main_objection": null
  },
  "metadata": {
    "patient_id": "pat_123",
    "source": "reminder_system"
  }
}
```

Each transcript entry's `user` field identifies the speaker: `"assistant"` for AI turns, `"user"` for caller turns. The field name is awkward but matches what the API actually sends.

`disposition` is the label assigned by post-call analysis, and `is_conversion` is true when that disposition is in the campaign's conversion outcomes list.

## Webhook Timing

### Successful Calls

The completion webhook fires shortly after the call ends, once the transcript and (if configured) the call summary are ready.

The `recording_url` may be missing from this payload. Recordings take a few seconds longer to finish encoding, so a background job resolves the recording and writes it to the call record just after the webhook is sent. If you need the recording and it was not in the webhook, read the call back with `GET /v1/calls/{call_id}` and use the `recording_url` from that response.

### Failed Calls

Webhooks are sent **immediately** when the call fails (no recording to wait for).

## Handling Webhooks

### Express.js Example

```javascript theme={null}
app.post('/webhooks/call-complete', async (req, res) => {
  // Always return 200 quickly
  res.status(200).json({ received: true });
  
  // Process asynchronously
  const call = req.body;
  
  // Update CRM
  await updateCRM(call);
  
  // Send notification
  await sendNotification(call);
  
  // Update analytics
  await updateAnalytics(call);
});
```

### Securing Your Endpoint

Webhook payloads are not signed today, so treat the URL itself as the credential:

* Serve the endpoint over HTTPS only.
* Put an unguessable token in the path or query string (for example `https://your-app.com/webhooks/tc_8f3k2m9x`) and reject requests without it.
* Before acting on a payload, you can confirm it against the API: fetch `GET /v1/calls/{call_id}` and compare.

## Webhook Retries

TopCalls automatically retries failed webhook deliveries:

* **Initial**: Immediate
* **Retry 1**: After 1 minute
* **Retry 2**: After 5 minutes
* **Retry 3**: After 15 minutes
* **Retry 4**: After 30 minutes
* **Retry 5**: After 1 hour

After 5 retries, the webhook is marked as failed. You can still retrieve call data via the API.

## Best Practices

### ✅ Do This

* **Return 200 quickly**: Acknowledge receipt within 1 second
* **Process asynchronously**: Don't block the webhook response
* **Idempotency**: Handle duplicate webhooks gracefully
* **Log everything**: Keep logs for debugging
* **Keep the URL secret**: Use an unguessable path token and HTTPS
* **Handle errors**: Don't let webhook processing crash your server

### ❌ Don't Do This

* **Long processing**: Don't process synchronously in the webhook handler
* **Ignore duplicates**: Webhooks may be delivered multiple times
* **Expose the URL**: Don't publish or log your webhook URL where others can see it
* **Block on external APIs**: Don't wait for slow external services

## Testing Webhooks

Use [ngrok](https://ngrok.com) or [webhook.site](https://webhook.site) to test locally:

```bash theme={null}
# Start ngrok tunnel
ngrok http 3000

# Use the ngrok URL
{
  "webhook_url": "https://abc123.ngrok.io/webhooks/call-complete"
}
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Webhooks & Events" icon="webhook" href="/concepts/webhooks">
    Learn about available webhook events and when they fire.
  </Card>

  <Card title="API Reference" icon="book" href="/api-reference/introduction">
    See complete webhook payload schemas in the API reference.
  </Card>
</CardGroup>
