# Post-call actions over the API (/docs/integrations/post-call-actions-api)

> Send every call's results to email, a webhook, Google Sheets, Slack or your CRM, and add, change or remove those deliveries from your own code.

When a call ends, your agent runs its **post-call actions**. Each action sends
the call's results somewhere: an email report, a webhook to your server, a new
row in a Google Sheet, a Slack message or a CRM record.

With the API you manage these actions **one at a time**. Add a webhook, and the
agent's other actions stay exactly as they were, including the ones set up in
the dashboard.

## What your agent can send after a call [#what-your-agent-can-send-after-a-call]

  - [Email](/docs/dashboard-guides/post-call#email-delivery) — A call report in any inbox.
  - [Webhook](#what-a-webhook-receives) — The full call data, POSTed to your server.
  - [Google Sheets](/docs/integrations/google-sheets) — One new row per call.
  - [Slack](/docs/integrations/slack) — A message in the channel you pick.
  - [Salesforce](/docs/integrations/salesforce) — A Lead, Contact, Case or other record.
  - [HubSpot](/docs/integrations/hubspot) — A contact, company, deal or ticket.
  - [Zoho Bigin](#action-types) — A call log, and the contact created or updated.
  - [Omni CRM](/docs/dashboard-guides/omni-crm) — The caller and the call, in your built-in CRM.
  - [WhatsApp](/docs/dashboard-guides/post-call#whatsapp-cloud-post-call) — An approved template sent to the caller.
  - [Workflow](/docs/workflows) — One of your workflows, started with the call's data.

## How it works [#how-it-works]

## Before you start [#before-you-start]

- **Your API key** goes in the `Authorization: Bearer` header.
- **For Google Sheets, Slack, Salesforce, HubSpot or Zoho Bigin**, connect the
  account once in the dashboard under **Integrations**. Signing in to Google or
  Slack has to happen in a browser, so the API can't do that part.
- **Resellers** can manage a client's agent by adding `user_id`. See
  [Acting as clients](/docs/reseller/your-clients/acting-as-clients).

  To send just one call's results somewhere else, without changing the agent,
  use [per-call post-call actions](/docs/per-call-post-call-actions) instead.

## Step by step [#step-by-step]

### See the agent's actions [#see-the-agents-actions]

API reference: [List post-call actions](/docs/api-reference/agents/listPostCallActions)

```bash
curl "https://backend.omnidim.io/api/v1/agents/158910/post-call-actions" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

You get every action with its `id` and `type`:

```json
{
  "actions": [
    {
      "id": 63535,
      "type": "email",
      "recipients": ["demo@example.com"],
      "include": ["summary"],
      "extracted_variables": [],
      "trigger_call_statuses": ["completed", "voicemail_detected"]
    }
  ],
  "applies_after_publish": false
}
```

### Add a webhook [#add-a-webhook]

API reference: [Add a post-call action](/docs/api-reference/agents/addPostCallAction)

```bash
curl -X POST "https://backend.omnidim.io/api/v1/agents/158910/post-call-actions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2d9e-0b7a-4c1e-9a55-3e2f8b1d7c40" \
  -d '{
    "type": "webhook",
    "url": "https://crm.acme-corp.io/omnidim-callback",
    "include": ["summary", "extracted_variables"],
    "extracted_variables": [
      { "key": "outcome", "prompt": "Did the caller book an appointment?" }
    ],
    "trigger_call_statuses": ["completed", "failed"]
  }'
```

You get the new action back, with its `id`:

```json
{
  "action": {
    "id": 63536,
    "type": "webhook",
    "url": "https://crm.acme-corp.io/omnidim-callback",
    "include": ["summary", "extracted_variables"],
    "extracted_variables": [
      { "key": "outcome", "prompt": "Did the caller book an appointment?" }
    ],
    "trigger_call_statuses": ["completed", "failed"]
  },
  "applies_after_publish": false
}
```

- The `url` must be a real, public address. Placeholder hosts like
  `example.com`, `localhost` and private network addresses are refused.
- The `Idempotency-Key` is any unique string you choose, up to 255 characters.
  If your request times out and you send it again with the same key and body,
  you get the same action back instead of a second copy.

### Check that it works [#check-that-it-works]

Before you point the webhook at your real server, try it with a free test
inbox:

1. Open [webhook.site](https://webhook.site) and copy your unique URL.
2. Add a webhook action with that URL, as in the step above.
3. Make a short test call to the agent and hang up.
4. Watch webhook.site. The call data arrives shortly after the call ends,
   once the summary and variables are ready.

The data you see is described in
[What a webhook receives](#what-a-webhook-receives). When it looks right,
change the action's `url` to your server.

### Add Google Sheets, Slack or a CRM [#add-google-sheets-slack-or-a-crm]

API reference: [List integrations](/docs/api-reference/integrations/listIntegrations), [Add a post-call action](/docs/api-reference/agents/addPostCallAction)

First, find the id of the account you connected:

```bash
curl "https://backend.omnidim.io/api/v1/integrations" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Pick the entry whose `integration_type` is `google_sheets` (or `slack`,
`salesforce`, `hubspot`, `zoho_bigin`). Pass its `id` as `integration_id`:

```bash
curl -X POST "https://backend.omnidim.io/api/v1/agents/158910/post-call-actions" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "google_sheets",
    "integration_id": 812,
    "spreadsheet_id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
    "worksheet_name": "Calls",
    "include": ["summary", "extracted_variables"],
    "extracted_variables": [
      { "key": "customer_name", "prompt": "The caller'\''s full name." }
    ]
  }'
```

- The `spreadsheet_id` is the part of the sheet's link after `/d/`.
- Each call adds **one new row**. The row includes the call's details, such
  as `call_id`, `call_date`, `phone_number`, `to_number`, `from_number`,
  `call_direction`, `call_status`, the call duration, `recording_url` and
  `bot_name`. Every variable extracted on the call gets its own column, from
  all of the agent's actions, not only this one. `summary`, `sentiment` and
  the full conversation are added when you `include` them.
- `call_date` is in the agent's timezone, not UTC. See
  [Agent timezone](/docs/dashboard-guides/update-timezone).
- Bulk call campaigns add `bulk_call_name`, `retry_count` and one
  `metadata_<key>` column for each key in the contact's `metadata`.
- Columns are created from the first call, and new ones are added when later
  calls bring new fields.

### Change an action [#change-an-action]

API reference: [Update a post-call action](/docs/api-reference/agents/updatePostCallAction)

Send only what changes. Everything else stays the same.

```bash
curl -X PATCH "https://backend.omnidim.io/api/v1/agents/158910/post-call-actions/63536" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://crm.acme-corp.io/omnidim-callback-v2" }'
```

An action can't change its `type`. To switch, remove it and add a new one.

### Remove an action [#remove-an-action]

API reference: [Delete a post-call action](/docs/api-reference/agents/deletePostCallAction)

```bash
curl -X DELETE "https://backend.omnidim.io/api/v1/agents/158910/post-call-actions/63536" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

The agent's other actions stay as they are.

## What a webhook receives [#what-a-webhook-receives]

After each call, a webhook action sends one `POST` to your `url`, with a JSON
body like this:

```json
{
  "call_id": 7651752,
  "call_sid": "CA4b1f9e2d7c...",
  "call_request_id": 91422,
  "bot_id": 158910,
  "bot_name": "Front desk",
  "call_direction": "outbound",
  "from_number": "+15551230000",
  "to_number": "+15551234567",
  "phone_number": "+15551230000",
  "call_date": "2026-10-16 14:02:11",
  "start_time": "2026-10-16 14:02:11",
  "end_time": "2026-10-16 14:04:37",
  "call_duration": 146,
  "call_status": "completed",
  "hangup_source": "user",
  "hangup_reason": "The caller hung up.",
  "user_email": "demo@example.com",
  "recording_url": "https://backend.omnidim.io/...",
  "recording_available_at": "2026-10-16 14:06:07",
  "low_interaction": false,
  "metadata": { "lead_id": "L-1042" },
  "call_report": {
    "summary": "The caller booked a cleaning for Friday at 10 am.",
    "sentiment": "Positive",
    "extracted_variables": { "outcome": "booked" },
    "full_conversation": "...",
    "recording_url": "https://backend.omnidim.io/...",
    "interactions": [
      {
        "sequence": 1,
        "user_query": "Hi, I want to book a cleaning.",
        "bot_response": "Sure. What day works for you?",
        "time": "2026-10-16 14:02:15"
      }
    ]
  }
}
```

### Call fields [#call-fields]

| Field | What it is |
|---|---|
| `call_id` | The call's id in OmniDimension. |
| `call_sid` | The call's id at the phone carrier. |
| `call_request_id` | The id of the request that started the call. |
| `bot_id`, `bot_name` | The agent that took the call. |
| `call_direction` | `inbound` or `outbound`. |
| `from_number`, `to_number` | Who called whom. `phone_number` repeats `from_number`. |
| `call_date`, `start_time`, `end_time` | When the call ran, in UTC, as `YYYY-MM-DD HH:MM:SS`. |
| `call_duration` | Length in seconds. Always equals `end_time` minus `start_time`. |
| `call_status` | `completed`, `no-answer`, `busy`, `failed`, `voicemail_detected` or `canceled`. |
| `hangup_source` | Who ended the call: `user`, `bot`, `timeout`, `voicemail`, `transfer`, `system`, `carrier` or `screening`. |
| `hangup_reason` | Why the call ended, in words. Most useful on failed calls. |
| `user_email` | The email of the account that owns the agent. |
| `recording_url` | A link to the call recording, when there is one. |
| `recording_available_at` | When the recording is ready to download. |
| `low_interaction` | `true` when the call had 4 turns or fewer, for example a quick hang-up. |

Every call field above is always in the body, but some can be empty. On a
call with no recording or no hang-up details, such as most `no-answer` and
`busy` calls:

- `recording_url`, `hangup_source` and `hangup_reason` are `false`.
- `recording_available_at` is `null`.

Treat both `false` and `null` as "no value".

### Fields added only in certain cases [#fields-added-only-in-certain-cases]

These keys are left out of the body entirely unless their condition is true.

| Field | Added when | Value |
|---|---|---|
| `bulk_call_name` | The call was placed by a [bulk call](/docs/bulk-calls) campaign. | The campaign's name. |
| `retry_count` | The call was placed by a bulk call campaign. | `0` on the first attempt, `1` on the first retry, and so on. |
| `extra_data` | The call was placed by a bulk call campaign. | The contact's own fields from your list, or `null` if it has none. |
| `metadata` | Bulk call: always, with the contact's `metadata` or `null`. Any other call: only if you sent `metadata` when you started it with [dispatch](/docs/api-reference/calls/dispatchCall) or a [web call session](/docs/api-reference/sessions/createSession). | Exactly what you sent. |
| `transferred` | The agent started a transfer during the call. Never sent as `false`. | `true`. It means a transfer was started, not that it connected. |
| `transferred_to_number` | A transfer was started and it had exactly one destination. | That number. |

### The call report [#the-call-report]

`call_report` is always in the body. Its keys depend on `include` and on what
the call produced:

| Key | Added when |
|---|---|
| `interactions` | Always. One entry per turn of the conversation, in order. Empty when nobody spoke. |
| `recording_url` | The call has a recording. You don't need `include` for it. |
| `summary` | `include` has `summary`, and the call produced a summary. |
| `sentiment` | `include` has `sentiment`, and the call produced one. |
| `extracted_variables` | `include` has `extracted_variables`, and at least one value was extracted. One key per variable you defined. |
| `full_conversation` | `include` has `fullConversation`, and the call has a transcript. |

A call nobody answered has no conversation, so it has no `summary`,
`sentiment`, `extracted_variables` or `full_conversation`, even if you asked
for them. Check that a key exists before you read it.

  Need a different shape, such as your CRM's field names? In the dashboard, a
  webhook can use a custom payload template. See
  [Post-call actions](/docs/dashboard-guides/post-call#webhook).

## Delivery and retries [#delivery-and-retries]

- Your server should answer with any `2xx` status. Do the heavy work after
  you answer, because a request is given up after **30 seconds**.
- If your server is down, times out, or answers `408`, `425`, `429`, `500`,
  `502`, `503` or `504`, the webhook is sent again, **up to 4 more times**, with
  growing waits between tries.
- Any other answer, such as `400` or `404`, is final. It is not retried, so fix
  the endpoint and the next call will deliver.
- If your server fails several times in a row, deliveries to it pause for a
  short while, then resume on their own. They are kept and sent, not dropped,
  for up to about 20 minutes.
- Retries can send the same call twice. Use `call_id` to ignore a call you
  already processed.

## Action types [#action-types]

| `type` | What it does | You must send |
|---|---|---|
| `email` | Emails the call report | `recipients` |
| `webhook` | POSTs the call data to your URL | `url` |
| `omni_crm` | Logs the call in Omni CRM | Nothing extra |
| `cloud_whatsapp` | Sends a WhatsApp template | `phone_number_id`. Also send a `template_id` or `templates`, or nothing is sent. |
| `workflow` | Runs one of your [workflows](/docs/workflows) | `workflow_id`. The workflow must be published, or the action is skipped. |
| `google_sheets` | Adds a row to a sheet | `integration_id`, `spreadsheet_id` |
| `slack` | Posts in a Slack channel | `integration_id`, plus `channel_id` or `channel_name` |
| `salesforce` | Creates a Salesforce record | `integration_id` (creates a Lead unless you set `object_name`) |
| `hubspot` | Creates a HubSpot record | `integration_id`, `object_name` |
| `zoho_bigin` | Logs the call in Zoho Bigin | `integration_id` |

A `workflow` action starts the workflow's published version with the call's
details and transcript, plus the summary, sentiment and extracted variables
when the call produced them. To create and publish a workflow from your code, see
[Build with the API](/docs/workflows/build-with-the-api). Its
[List workflows](/docs/api-reference/workflows/listWorkflows) call gives you
the `workflow_id`.

  A `zoho_bigin` action fills a Bigin field only when an extracted variable's
  key matches that field's name. Any other value goes into the call log's
  description. To choose the value for each Bigin field yourself, add a
  `workflow` action that uses the
  [Zoho Bigin node](/docs/workflows/nodes/crm#zoho-bigin).

Every type also accepts three options:

- **`include`**: what to send. Pick from `summary`, `extracted_variables`,
  `fullConversation` and `sentiment`. Zoho Bigin, workflow and WhatsApp
  always receive extracted variables. See
  [How extracted variables work](#how-extracted-variables-work).
- **`extracted_variables`**: details for the agent to pull out of the
  conversation, each a `key` and a `prompt`. Salesforce and HubSpot send a
  fixed set of fields, so they take none.
- **`trigger_call_statuses`**: which call outcomes run the action. If you leave
  it out, the action runs on `completed` and `voicemail_detected`.

Every field is listed in the
[Add a post-call action](/docs/api-reference/agents/addPostCallAction) reference.

## When a change goes live [#when-a-change-goes-live]

**Most agents:** right away. The next call uses it, and every response says
`"applies_after_publish": false`.

**Agents you publish in the dashboard:** calls use the published version. The
response says `"applies_after_publish": true`, and your change goes live the
next time you publish. See
[Agent versioning and publishing](/docs/dashboard-guides/agent-versioning-and-publishing).

## Finding your action again [#finding-your-action-again]

An action's `id` can change. Saving post-call changes in the dashboard, or
restoring an older version, gives every action a new id.

So don't keep the id. When you need your action again, list the actions and
match it by what it points at:

- a webhook by its `url`
- an email by its `recipients`
- Sheets, Slack or a CRM by its `integration_id`

Your actions are safe from the dashboard, too. If someone has the agent open in
the dashboard while you add an action, their next save keeps yours. Their own
post-call change in that save is not applied: the dashboard shows the current
list and asks them to make it again.

## How extracted variables work [#how-extracted-variables-work]

An extracted variable is a detail the agent pulls out of the conversation,
like `customer_name` or `appointment_date`. Each one is a `key` (the name
you get back) and a `prompt` (what the agent should look for). An action can
have as many as you need.

**One extraction per call.** After the call, the agent reads the conversation
once and fills in the variables of **all** the agent's actions together.
Adding more actions doesn't add more work per call.

**Each action gets its own keys.** The results are then split by action:

| Action | Gets |
|---|---|
| Email, webhook, Slack | Only when `include` has `extracted_variables`. Then: the keys defined on that action, or every key extracted on the call if the action defines none. |
| Zoho Bigin, workflow, WhatsApp | Always. When `include` has `extracted_variables` and the action defines keys, only those keys. Otherwise, every key extracted on the call. A WhatsApp template fills its placeholders from these keys. |
| Google Sheets | Every key extracted on the call, from all actions, each in its own column. |
| Salesforce, HubSpot | A fixed set of fields for the chosen object. You don't define variables for them. |

**Same key, same prompt.** One key has one prompt per agent, because it is
extracted only once. If your webhook asks for `outcome` and a Slack action
asks for `outcome` too, both prompts must match. A new action that reuses a
key with a different prompt is refused with a `400`, and nothing changes.
Give it a new key instead, such as `slack_outcome`.

**Different keys, different prompts.** Keys only need to differ from each
other. You can give the webhook `budget` and the email `follow_up_date`, each
with its own prompt, and each action receives its own.

## Errors [#errors]

| Status | What it means |
|---|---|
| `400` | Something in the action is wrong, like a missing field or another workspace's `integration_id`. Nothing is saved. |
| `401` | The API key is missing or wrong. |
| `403` | The key's user isn't allowed to change agents. |
| `404` | The agent or action wasn't found. List the actions to find its current id. |
| `409` | The `Idempotency-Key` was already used with a different request. Use a new key for a new request. |

Every error comes back as `{ "error": "...", "error_description": "..." }`,
and the description tells you what to fix.

## Related [#related]

  }
    title="Post-call actions in the dashboard"
    href="/docs/dashboard-guides/post-call"
    description="Set up the same deliveries in the agent editor, and see the webhook payload."
  />
  }
    title="Per-call post-call actions"
    href="/docs/per-call-post-call-actions"
    description="Send one call's results somewhere else, chosen when you dial."
  />
  }
    title="List integrations"
    href="/docs/api-reference/integrations/listIntegrations"
    description="Find the id of an account you connected in the dashboard."
  />
  }
    title="Acting as clients"
    href="/docs/reseller/your-clients/acting-as-clients"
    description="Manage your clients' agents with your reseller key."
  />