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

# Post-Call Webhooks

> Send call data to external services like Zapier, Make, or n8n when a call matches your configured trigger

## Overview

Post-call webhooks let you send call results to any external service when a call ends and matches your configured trigger. Use them to sync call data with your CRM, trigger automations in Zapier or Make, log outcomes to a spreadsheet, or notify your team.

## How It Works

1. You configure a webhook URL in your workflow settings
2. After each eligible call, Tala sends a `POST` request to that URL with the call data
3. Your external service processes the data and takes action

## Setting Up a Webhook

### Step 1: Open Workflow Settings

In the workflow editor, click the **Settings** icon to open the Configurations dialog.

### Step 2: Enable the Webhook

Scroll to the **Post-call Webhook** section and toggle it on.

### Step 3: Enter Your Webhook URL

Paste the URL from your external service. The URL must use HTTPS.

| Service | Where to find the URL                                                                           |
| ------- | ----------------------------------------------------------------------------------------------- |
| Zapier  | Create a Zap with "Webhooks by Zapier" trigger, copy the URL                                    |
| Make    | Create a scenario with "Webhooks" module, copy the URL                                          |
| n8n     | Create a workflow with "Webhook" trigger node, then copy a public HTTPS URL without custom port |

### Step 4: Choose a Trigger

| Trigger                  | Description                                                                            |
| ------------------------ | -------------------------------------------------------------------------------------- |
| **Every call**           | Fires after every call, regardless of outcome                                          |
| **Qualified calls only** | Fires only when the call results in a transfer (XFER disposition)                      |
| **By call outcome**      | Fires only for specific disposition codes you define (e.g., XFER, CALLBACK, QUALIFIED) |

### Step 5: Save

Click **Save**. The webhook is now active for all future calls that match this workflow's configured trigger.

## Webhook Payload

Each webhook sends a JSON `POST` request with the following structure:

```json theme={null}
{
  "event": "call.completed",
  "workflow_run_id": 12345,
  "disposition": "XFER",
  "call_duration_seconds": 142,
  "recording_url": "https://storage.example.com/signed-url...",
  "transcript_url": "https://storage.example.com/signed-url...",
  "gathered_context": {
    "mapped_call_disposition": "XFER",
    "full_name": "Jean Dupont",
    "appointment_date": "2026-03-20"
  },
  "initial_context": {
    "customer_phone_number": "+33612345678"
  },
  "caller": {
    "phone_number": "+33612345678",
    "direction": "inbound"
  },
  "call_overview": {
    "headline": "Appointment request",
    "summary": "The caller wants an appointment on Thursday morning.",
    "topic_label": "Appointment booking",
    "next_action_label": "Call back",
    "appointment_status": "requested"
  },
  "workflow": {
    "id": 69,
    "name": "Appointment Booking"
  },
  "timestamp": "2026-03-14T15:30:00.000000+00:00"
}
```

### Field Reference

| Field                   | Type           | Description                                                                         |
| ----------------------- | -------------- | ----------------------------------------------------------------------------------- |
| `event`                 | string         | Always `"call.completed"`                                                           |
| `workflow_run_id`       | integer        | Unique identifier for this call                                                     |
| `disposition`           | string         | Call outcome code (XFER, HANGUP, VOICEMAIL, etc.)                                   |
| `call_duration_seconds` | number or null | Duration of the call in seconds                                                     |
| `recording_url`         | string or null | Signed URL to download the call recording (expires in 1 hour)                       |
| `transcript_url`        | string or null | Signed URL to download the call transcript (expires in 1 hour)                      |
| `gathered_context`      | object         | Data collected during the call (varies by workflow)                                 |
| `initial_context`       | object         | Data passed to the workflow at call start                                           |
| `caller`                | object         | The other party's `phone_number` (E.164, `null` when withheld) and call `direction` |
| `call_overview`         | object or null | Short, ready-to-use summary of the call (see below)                                 |
| `workflow`              | object         | Workflow identifier (`id` and `name`)                                               |
| `timestamp`             | string         | ISO 8601 timestamp of when the webhook was sent                                     |

### Ready-to-use fields

`caller` and `call_overview` are the fields to map when you push calls into a
spreadsheet or a CRM. They stay at the same place for every call, so your
automation does not have to know which of the context objects carried the
number this time.

| Field                              | Type           | Description                                                                                                                                                                                                                                |
| ---------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `caller.phone_number`              | string or null | Number in E.164 format. When the caller withholds their ID, a number they gave during the conversation is used instead. `null` only when no usable number was obtained at all                                                              |
| `caller.direction`                 | string or null | `"inbound"` or `"outbound"` when known                                                                                                                                                                                                     |
| `call_overview.headline`           | string         | One-line reason for the call, never empty                                                                                                                                                                                                  |
| `call_overview.summary`            | string or null | Longer summary. It is produced in parallel with this notification, so it can be `null` on any call, whatever its length. It is a snapshot taken when we first send the call, so a `null` here stays `null` for that call, retries included |
| `call_overview.topic_label`        | string         | Call category                                                                                                                                                                                                                              |
| `call_overview.next_action_label`  | string         | Suggested follow-up                                                                                                                                                                                                                        |
| `call_overview.appointment_status` | string         | Appointment state when relevant                                                                                                                                                                                                            |

<Note>
  `recording_url` and `transcript_url` may be `null` if the call recording or transcript upload has not completed yet. This can happen for a few seconds after the call ends. These three fields are decided when we first send the call: if a link or the summary was not ready then, it stays empty for that call, retries included. Only links that already existed are refreshed on a later attempt. `call_overview.headline` is always available.
</Note>

<Note>
  The same call can reach you more than once, including after your endpoint answered normally. Every request carries an `Idempotency-Key` header and a unique `workflow_run_id`, both identical across attempts, so use either one to recognize a repeat. A few failures are terminal and stop the attempts (see Retry Behavior below).
</Note>

## Send your calls to a spreadsheet

A common setup is to log every call in a Google Sheet that your team works from.
You do not need any development for this: an automation tool such as Zapier,
Make or n8n sits between Tala and your spreadsheet, and holds the Google
authorization for you.

<Steps>
  <Step title="Create the automation">
    In Zapier, create a Zap starting with the **Webhooks by Zapier** trigger,
    **Catch Hook**. In Make, create a scenario starting with the **Webhooks**
    module, **Custom webhook**. Either one gives you a URL.
  </Step>

  <Step title="Point Tala at it">
    Paste that URL in your agent's settings, in the post-call webhook section,
    and choose the trigger you want. Save.
  </Step>

  <Step title="Send a test call">
    Your automation needs one real call to discover the available fields. How
    you capture it differs between tools: some listen only while you ask them
    to, others keep the last request they received. Check how yours captures a
    test request, then call your agent. Two things make this test call worth
    preparing:

    It has to match the trigger you selected. If you chose **Qualified calls
    only** or **By call outcome**, an ordinary test call is filtered out by
    Tala and nothing arrives. Either produce that outcome during the call, or
    switch to **Every call** while you set things up and put your trigger back
    afterwards.

    It should also go through everything you intend to log. A field your agent
    did not collect during the test is simply absent from the sample, so the
    automation has no way to offer it for mapping. If you add a column later,
    send another test call that fills it.
  </Step>

  <Step title="Prepare the sheet">
    Give your sheet a header row: the first line must name the columns.
    Automations read those names to offer you the destinations, so on a blank
    sheet there is nothing to map to.
  </Step>

  <Step title="Add the spreadsheet step">
    Add a **Google Sheets** action that appends a row: it is called **Create
    Spreadsheet Row** in Zapier and **Add a Row** in Make. Connect your Google
    account, pick the file and the tab, then drag each field into the matching
    column.
  </Step>

  <Step title="Turn it on">
    Publish the Zap in Zapier, or switch the scenario on in Make. Until you do,
    the sample works in the editor but real calls create nothing.
  </Step>
</Steps>

### Which fields to use

These four cover most spreadsheets:

| Column you probably have | Field to map                      |
| ------------------------ | --------------------------------- |
| Date and time            | `timestamp`                       |
| Phone number             | `caller.phone_number`             |
| Reason for the call      | `call_overview.headline`          |
| What to do next          | `call_overview.next_action_label` |

Anything your agent collected during the conversation (a name, an address, a
booking date) is in `gathered_context`, under the variable name you gave it in
the agent.

<Note>
  Use `call_overview.headline` for the reason column: it is always filled in.
  `call_overview.summary` is longer, but it is produced at the same time as we
  send you the call, so it can be missing on any call, whatever its length. What
  we send is fixed at that moment and never updated afterwards, so that cell stays
  empty for that call. Treat it as a bonus column, not as the one your team
  reads.
</Note>

Every call that matches your trigger adds one row, including when the same
person calls again: you keep the full history rather than overwriting the
previous call.

<Note>
  One exception worth knowing when you are still setting things up: if three
  matching calls in a row come back with one of the definitive errors below, Tala
  stops sending to that address and the following calls are skipped. Fixing your
  automation is not enough to restart it, you also have to reopen your agent's
  settings and save them again.
</Note>

### Avoiding a duplicate line

The same call can reach your automation twice, and it can happen even when
everything on your side worked. To be safe, keep one column for the call number,
`workflow_run_id`, which never changes between two sends of the same call. Then have your automation search that column before it adds a line,
and skip the call when the number is already there. Each tool wires this search
differently, so follow its own documentation.

If a duplicate line is not a problem for you, you can skip this and simply add
every call.

<Note>
  A few answers stop the attempts for good: **404**, **410**, **422**, or an
  address our security checks refuse. Everything else, including a timeout or a
  refused connection, is retried. And once your automation has accepted the
  call, we cannot see what happens inside it, so a problem occurring further down
  your scenario will not trigger a new send from us.
</Note>

## Retry Behavior

If a delivery does not go through, Tala keeps trying: the wait between attempts starts at 1 minute, grows each time, and is capped at 15 minutes, for up to 20 hours in total.

Timeouts, refused connections, **5xx errors** and the statuses **408**, **409**, **425** and **429** are all treated as temporary and retried.

Only **404**, **410** and **422**, or a URL our security checks refuse, stop the attempts, because sending the very same request again would not change anything. **404** and **410** usually mean the address is wrong or no longer exists, while **422** comes from an address that works but rejects what we sent, so check your automation's configuration in that case.

## Security

* Webhook URLs must use **HTTPS**
* Validation happens when you save the workflow and again when Tala sends the webhook
* URLs with embedded credentials or custom ports are rejected
* URLs pointing to private networks, localhost, internal IPs, or non-resolvable hosts are rejected
* The webhook URL itself acts as a shared secret (keep it confidential)
* Media URLs (recording/transcript) are signed and expire after 1 hour

## Limits

* One webhook URL per workflow
* 10 second timeout per request
* Retries for up to 20 hours on temporary failures

***

## Webhook Node

In addition to the workflow-level post-call webhook described above, you can place a **Webhook node** directly inside the workflow builder. The node gives you a second post-call integration path with a customizable payload and multiple outbound requests per workflow.

<Note>
  The Webhook node does not replace the workflow-level webhook. If both are configured, Tala can send both requests after the call.
</Note>

### How It Differs from the Post-Call Webhook

|               | Post-Call Webhook                       | Webhook Node                                                |
| ------------- | --------------------------------------- | ----------------------------------------------------------- |
| **Trigger**   | Fires automatically after the call ends | Runs in the post-call integration phase after the call ends |
| **Placement** | Configured in workflow settings         | Drag-and-drop inside the workflow builder                   |
| **Quantity**  | One per workflow                        | As many as you need                                         |
| **Payload**   | Fixed structure (see above)             | Fully customizable template                                 |

### Adding a Webhook Node

1. Open the workflow editor
2. Drag the **Webhook** node from the node palette onto the canvas
3. Connect it to the appropriate step in your workflow

### Configuration

| Field                | Description                                                                                             |
| -------------------- | ------------------------------------------------------------------------------------------------------- |
| **URL**              | A public HTTPS endpoint without embedded credentials or custom port                                     |
| **Method**           | HTTP method (`POST`, `PUT`, `PATCH`)                                                                    |
| **Headers**          | Custom headers to include in the request (key-value pairs)                                              |
| **Credential**       | Stored credential support is available at the platform level and may appear progressively in the editor |
| **Payload template** | JSON body with variable placeholders                                                                    |

### Available Variables

You can insert variables into custom header values and the JSON payload using the `{{ variable_name }}` syntax. The endpoint URL itself is static and validated separately.

| Variable             | Description                                      |
| -------------------- | ------------------------------------------------ |
| `workflow_run_id`    | Unique identifier for the current call           |
| `workflow_run_name`  | Human-readable name of the current call          |
| `workflow_id`        | Identifier of the workflow                       |
| `workflow_name`      | Workflow name                                    |
| `call_duration`      | Call duration in seconds when available          |
| `call_status`        | Workflow run status                              |
| `initial_context`    | Full object passed at call start                 |
| `gathered_context`   | Full object collected during the call            |
| `initial_context.*`  | Direct access to a key from the initial context  |
| `gathered_context.*` | Direct access to a key from the gathered context |
| `recording_url`      | Signed URL to the call recording when available  |
| `transcript_url`     | Signed URL to the call transcript when available |
| `cost_info`          | Usage and cost information when available        |

### Payload Example

```json theme={null}
{
  "call_id": "{{ workflow_run_id }}",
  "customer_name": "{{ gathered_context.full_name }}",
  "status": "{{ call_status }}",
  "duration_seconds": "{{ call_duration }}",
  "recording": "{{ recording_url }}"
}
```

### Retry Behavior

Webhook nodes use the same durable delivery as the post-call webhook, so the retry policy described above applies here too: temporary failures are retried for up to 20 hours, and only **404**, **410**, **422** or a refused URL stop the attempts. Three consecutive definitive errors also disable a node, and the following calls skip it. Saving the settings dialog does not bring a node back: fix the node itself and save the workflow. Other post-call integrations continue regardless of whether the webhook succeeds or fails.

### Security

* Webhook node URLs are validated with the same strict outbound checks as the classic webhook
* Only `POST`, `PUT`, and `PATCH` are supported
* Media URLs are signed and expire after a short period
* Credential-backed auth can be applied when that integration surface is available
