Skip to content

Make Outbound Phone Calls from an n8n Workflow

September 2, 2026 · 7 min read

You can call a customer from n8n without writing a line of code: add an HTTP Request node to your workflow, point it at POST https://volai.cz/v1/calls with an Authorization: Bearer vk_... header and a body of {to, agentId} - and your voice agent dials the number exactly when the workflow needs it to. Below is the full setup: mapping workflow data into the agent's prompt, getting the call result back into n8n, and calling without an agent at all, by bridging two numbers directly.

What you will need

  • An n8n instance - cloud or self-hosted, any recent version.
  • A volai account and an API key from the portal (Settings -> API & MCP), shaped like vk_.... Save it in n8n as a Header Auth credential (header Authorization, value Bearer vk_...) so you are not copying the key into every node.
  • Your own phone number with a voice agent attached - create one in the portal or via /docs/agent. No agent yet? You can still call by bridging two numbers directly (the from variant below).

1Add an HTTP Request node

Method POST, URL https://volai.cz/v1/calls, authentication via your saved credential, Body Content Type JSON. The body needs to (the customer's number) and agentId (the agent's id from the portal) - both required, and exactly ONE of agentId/from/systemPrompt, never more than one.

json
{
  "to": "={{ $json.phone }}",
  "agentId": "ag_kx91fa2b",
  "variables": {
    "customer_name": "={{ $json.name }}",
    "order_number": "={{ $json.order }}"
  },
  "ringingTimeoutSecs": 30
}

Values written as ={{...}} are n8n expressions - $json reads fields from the previous node in the workflow (say, a "new order" webhook or a spreadsheet row).

2Map workflow data into the agent's prompt

variables is a string-to-string object the agent receives as dynamic variables - reference them in its system prompt as {{customer_name}}. Limits: at most 20 keys, keys up to 64 characters, values up to 512 characters. The names attempt_id, caller_number, and called_number are reserved - volai fills them in itself, and matching values from your workflow would be discarded.

ringingTimeoutSecs (5 to 60 seconds, default 25) sets how long to let the phone ring before an unanswered call ends with endReason: "no_answer". A longer timeout raises the chance the customer picks up, but also holds an outbound slot longer.

3Read the response, and optionally wait for the result

A successful response comes back right away, but the call has only just started - status is initiated, not a final state:

json
{
  "id": "c_9d4e2b7f",
  "status": "initiated"
}

Want to wait for the outcome in the same workflow run instead of a separate webhook? Add a second node right after the HTTP Request node that calls GET /v1/calls/{id} with a waitSecs parameter (0 to 45 seconds) - the server responds once the call ends, or once the timeout is reached, whichever comes first:

bash
curl "https://volai.cz/v1/calls/c_9d4e2b7f?waitSecs=30" \
  -H "Authorization: Bearer vk_YOUR_KEY"

Getting the result back: a webhook into n8n

For a production workflow it is usually more practical not to wait inline, and instead have the result delivered separately: add a Webhook node in n8n (a new workflow works fine), copy its Production URL, and register it with volai once:

bash
curl -X PUT https://volai.cz/v1/webhook \
  -H "Authorization: Bearer vk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-instance.app.n8n.cloud/webhook/volai-calls",
    "events": ["call.completed", "call.failed", "call.no_answer"]
  }'

The response includes a secret (shaped like whsec_...) - save it, you will need it to verify the signature of every request that arrives. volai sends a Volai-Signature header shaped like t=<unix>,v1=<hex> - the full verification code in TypeScript and Python lives at /docs/webhooky. It does not carry over to an n8n Code node unchanged: the HMAC is computed over the RAW request body, so you need to turn on the Raw Body option on the Webhook node (without it n8n hands you already-parsed JSON and the signature never matches), and import crypto in the Code node with require, not import. Do not process the body before you verify the signature.

What arrives once the call finishes:

json
{
  "event": "call.completed",
  "ts": 1756111640000,
  "data": {
    "id": "c_9d4e2b7f",
    "direction": "out",
    "from": "+420601234567",
    "to": "+420777123456",
    "status": "completed",
    "durationSecs": 90,
    "priceHal": 513,
    "answeredBy": "human",
    "endReason": "completed",
    "transcript": [
      { "role": "agent", "message": "Hi, calling about order A-42." },
      { "role": "caller", "message": "Yes, I know, thanks for the reminder." }
    ],
    "summary": "The customer confirmed receiving order A-42.",
    "data": {
      "order_confirmed": true
    }
  }
}

The data key nested inside data is optional and holds exactly the fields you configured for the agent in its dataFields - the example above (order_confirmed) is illustrative only, yours will differ. A following n8n node can then branch on those fields, say to update a CRM record or reschedule the call.

How do you call without an agent?

When you do not need a voice agent, just a direct connection between two numbers (say, a salesperson and a customer), send from instead of agentId:

json
{
  "to": "+420777123456",
  "from": "+420601234567"
}

volai calls from first, and once someone picks up, dials to. Both legs are billed separately at 0.92 CZK per minute (no agent surcharge). from is the number where YOU pick up - typically your mobile - and it can be any Czech or Slovak number, it does not need to belong to your volai account. The real requirement is different: your account needs at least one volai number of its own, because that is the number the call is billed through and the one both sides see on the display (without it, volai returns bridge_needs_number). Just do not put a volai number that is routed to an agent or to SIP into from - Odorik would connect that first leg to the agent instead of to you, and the call would never actually ring your phone.

What does a call like this cost?

An outbound call through an agent is billed as two rates added together: the outbound minute (0.92 CZK) plus the agent surcharge ON TOP of it (2.50 CZK), both from the price list as of September 2, 2026. A 90-second call comes out to 5.13 CZK:

ItemRate90s call
Outbound minute0.92 CZK/min1.38 CZK
Agent surcharge2.50 CZK/min3.75 CZK
Total5.13 CZK

Billing runs per second, not per whole minute - a shorter call costs proportionally less. The full price list (numbers, agent, SMS) is at /cenik.

Retrying safely (Idempotency-Key)

n8n can automatically retry a node on failure (Retry On Fail) - without a safeguard, that would mean two real phone calls to the same customer. Add an Idempotency-Key header with a value that stays stable for one workflow run (n8n exposes this as {{ $execution.id }}, or use your own order id) - with the same key within 24 hours you get back the response from the first attempt, and the call is not placed twice:

bash
curl -X POST https://volai.cz/v1/calls \
  -H "Authorization: Bearer vk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: reminder-order-A-42" \
  -d '{"to": "+420777123456", "agentId": "ag_kx91fa2b"}'

What do you do when the API returns an error?

This table covers the agentId and from variants - the states specific to a trial call without your own number ( systemPrompt) are deliberately left out, that is a different flow.

StatusCodeWhat it means
400invalid_numberThe to field is not a valid phone number.
400validationagentId/from is missing, or both are set; or variables exceed their limits, or ringingTimeoutSecs is outside 5 to 60.
400on_dncThe number is on your do-not-call list (/v1/dnc).
400destination_auto_blockedThis destination was auto-blocked for 30 days after repeated failures.
400cannot_call_own_numberLoop protection - you cannot call your own volai number.
404agent_not_foundagentId does not exist or does not belong to your account.
404agent_no_numberThe agent has no phone number assigned.
404bridge_needs_numberFor the from variant: you need at least one volai number on your account.
409destination_busyYou already have another call running to that same number.
402insufficient_creditYour credit does not cover the minimum to start a call.
503capacity_busyAll outbound lines are busy right now, try again in a minute.

The general API limit

On top of the table above, one limit applies across the whole API: at most 60 requests per minute per API key (HTTP 429, rate_limited). Keep that in mind in a workflow that dials dozens of numbers back to back - the same limit applies to POST /v1/messages and every other endpoint under the same key.

What is next

A full rundown of what you can do from n8n (SMS, numbers, the do-not-call list) lives in the n8n integration guide. The full REST reference is at /docs/api, and the complete list of webhook events with signature verification is at /docs/webhooky.

Try volai - first 50 CZK on us

Sign up in a minute and your account already has starter credit for calls, SMS and a number.

Try it for free