Sign inGet Started

Create a voice call

Warning: Voice sequences are in preview. Steps, entry data, and the sequence definition format can change in ways that break existing sequences and API requests.

Use POST /v1/voice/calls to call a phone number and start a published sequence, or a sequence definition sent with the request, when the recipient answers. The request places a real call; normal calling charges apply.

CLI and MCP preview: These interfaces require a compatible client. Check that your MCP server exposes voice_calls_create, or run bird voice --help to list the commands in your CLI. Check for newer releases with bird update --check and see CLI installation. If the tool or command is unavailable, use the HTTP API or SDKs.

In the preview, the CLI and MCP prepare the same call for a person to review and run in the browser. The agent receives the recorded acceptance result after the person runs the request.

Before you start

Prepare these resources in the same workspace:

  • A sequence to run: an enabled, unarchived sequence with an active publication built in the dashboard, or a complete definition sent with the request.
  • An API key for HTTP requests, or an OAuth login for CLI and MCP, with both voice_management:write and voice:write permissions.
  • A calling number the workspace may present, such as an owned or verified caller ID, and an enabled destination country. Prefer a number in the recipient's country: some mobile networks decline a call that presents a foreign caller ID.
  • A recipient you control for the first call.

The sequence object takes exactly one of sequence.id or sequence.definition. Both also need entry_node_id, the Start call step where the call starts, and trigger_data, an object matching that entry's data schema. Use {} when the entry accepts empty data. The Create Call reference documents request limits and the optional ringing timeout.

Call a sequence built in the dashboard

Use sequence.id to run a sequence you built and published in the dashboard. Create Call always runs its active publication.

  1. Open the sequence in Sequences.
  2. Under How to run this sequence, select More details next to Create a call.
  3. If the sequence has several entries, choose the Call trigger to start.
  4. Copy the request. It contains the sequence ID and entry ID.

Save the request body as call.json, replacing the example numbers with your own:

Code example
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "id": "YOUR_SEQUENCE_ID",
    "entry_node_id": "YOUR_ENTRY_NODE_ID",
    "trigger_data": {}
  }
}

Fill trigger_data with the fields the entry declares, including nested objects and arrays. Create Call does not accept historical versions, drafts, or test mode. To test a saved draft, use the dashboard's Place a call controls.

Run an inline sequence

Use sequence.definition to send a complete sequence with the call. Bird runs it once and does not save it, so the call has no sequence ID. An inline definition suits flows your application generates per call.

An inline definition must pass the same checks as publishing a sequence. Its run can execute at most 16 commands, including gather prompts; a call that needs a 17th ends with the call_limit trace error. The node reference lists every step you can use, with its fields and outcomes. To start from a working flow, build and publish it in the dashboard, open the sequence, and select Copy JSON on Definition JSON under Published version.

This call.json asks the recipient to press 1 for opening hours or 2 for directions. Silence and any other key end the run with their own exit reasons:

Code example
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "entry_node_id": "start",
    "trigger_data": {},
    "definition": {
      "schema_version": 1,
      "expression_environment": "bird.cel.v1",
      "nodes": [
        {
          "id": "start",
          "type": "trigger.start_call",
          "type_version": 1,
          "config": {},
          "input": {},
          "connections": { "event": { "node_id": "menu", "port": "input" } }
        },
        {
          "id": "menu",
          "type": "voice.gather",
          "type_version": 1,
          "config": {},
          "input": {
            "prompts": [
              {
                "type": "say",
                "text": "Press 1 for opening hours, or 2 for directions.",
                "language": "en"
              }
            ],
            "max_digits": 1,
            "timeout_seconds": 5,
            "inter_digit_timeout_seconds": 2,
            "finish_on_key": "#",
            "private": false,
            "matches": { "hours": "1", "directions": "2" }
          },
          "connections": {
            "hours": { "node_id": "hours", "port": "input" },
            "directions": { "node_id": "directions", "port": "input" },
            "timeout": { "node_id": "no_input", "port": "input" },
            "fallback": { "node_id": "other_input", "port": "input" }
          }
        },
        {
          "id": "hours",
          "type": "voice.say",
          "type_version": 1,
          "config": {},
          "input": { "text": "We are open Monday through Friday, nine to five.", "language": "en" },
          "connections": { "next": { "node_id": "done", "port": "input" } }
        },
        {
          "id": "directions",
          "type": "voice.say",
          "type_version": 1,
          "config": {},
          "input": {
            "text": "Find directions on your appointment confirmation.",
            "language": "en"
          },
          "connections": { "next": { "node_id": "done", "port": "input" } }
        },
        {
          "id": "done",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "information_played" },
          "input": { "output": {} }
        },
        {
          "id": "no_input",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "no_input" },
          "input": { "output": {} }
        },
        {
          "id": "other_input",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "unrecognized_input" },
          "input": { "output": {} }
        }
      ]
    }
  }
}

Each node's connections maps an outcome to the next node. The menu's matches names its choices, and connections uses the same names alongside the built-in timeout and fallback outcomes.

Personalize an inline sequence

Declare the fields you send in the entry's data_schema, then read them as trigger.data.<field> in an $expr value. This call greets the recipient by name and confirms or flags their appointment:

Code example
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "entry_node_id": "start",
    "trigger_data": {
      "appointment_id": "apt_2048",
      "customer_name": "Sam",
      "appointment_time": "Tuesday at 10 AM"
    },
    "definition": {
      "schema_version": 1,
      "expression_environment": "bird.cel.v1",
      "nodes": [
        {
          "id": "start",
          "type": "trigger.start_call",
          "type_version": 1,
          "config": {
            "data_schema": {
              "type": "object",
              "additionalProperties": false,
              "required": ["appointment_id", "customer_name", "appointment_time"],
              "properties": {
                "appointment_id": { "type": "string", "minLength": 1 },
                "customer_name": { "type": "string", "minLength": 1 },
                "appointment_time": { "type": "string", "minLength": 1 }
              }
            }
          },
          "input": {},
          "connections": { "event": { "node_id": "confirm_menu", "port": "input" } }
        },
        {
          "id": "confirm_menu",
          "type": "voice.gather",
          "type_version": 1,
          "config": {},
          "input": {
            "prompts": [
              {
                "type": "say",
                "text": {
                  "$template": [
                    "Hello ",
                    { "$expr": "trigger.data.customer_name" },
                    ". Press 1 to confirm your appointment on ",
                    { "$expr": "trigger.data.appointment_time" },
                    ", or 2 if you need help."
                  ]
                },
                "language": "en"
              }
            ],
            "max_digits": 1,
            "timeout_seconds": 7,
            "inter_digit_timeout_seconds": 2,
            "finish_on_key": null,
            "private": false,
            "matches": { "confirm": "1", "help": "2" }
          },
          "connections": {
            "confirm": { "node_id": "confirmed", "port": "input" },
            "help": { "node_id": "help_requested", "port": "input" },
            "timeout": { "node_id": "no_response", "port": "input" },
            "fallback": { "node_id": "no_response", "port": "input" }
          }
        },
        {
          "id": "confirmed",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "appointment_confirmed" },
          "input": {
            "output": { "appointment_id": { "$expr": "trigger.data.appointment_id" } }
          }
        },
        {
          "id": "help_requested",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "succeeded", "reason": "appointment_help_requested" },
          "input": {
            "output": { "appointment_id": { "$expr": "trigger.data.appointment_id" } }
          }
        },
        {
          "id": "no_response",
          "type": "logic.exit",
          "type_version": 1,
          "config": { "status": "failed", "reason": "appointment_no_response" },
          "input": {
            "output": { "appointment_id": { "$expr": "trigger.data.appointment_id" } }
          }
        }
      ]
    }
  }
}

Each exit's status and reason separate a confirmation, a request for help, and no response. A trigger_data object that does not match data_schema fails validation, with its location under /sequence/trigger_data, and no call is placed.

Send the request

Choose one request method below for the call.json you saved.

Choose a fresh, unique idempotency key once for this intended call. Keep that key and the original input for retries or confirmation recovery. Choose another key when you intend to place a separate new call, after resolving any unknown outcome for the original call.

For the shell examples, set IDEMPOTENCY_KEY to your chosen value before sending. The examples stop if it is unset or empty; they do not generate another key when repeated.

For HTTP, set BIRD_API_KEY to your key and BIRD_API_URL to its regional base URL, such as https://eu1.platform.bird.com for an eu1 key, then send the request:

Code example
curl --request POST "$BIRD_API_URL/v1/voice/calls" \
  --header "Authorization: Bearer $BIRD_API_KEY" \
  --header 'Content-Type: application/json' \
  --header "Idempotency-Key: ${IDEMPOTENCY_KEY:?Set IDEMPOTENCY_KEY to the saved key for this intended call}" \
  --data-binary @call.json

For the CLI preview, sign in with both write permissions and prepare the call using the same file:

Code example
bird auth login --scope voice_management:write --scope voice:write
bird voice calls create --body-file call.json \
  --idempotency-key "${IDEMPOTENCY_KEY:?Set IDEMPOTENCY_KEY to the saved key for this intended call}"

The command prints a browser review link and waits for completion. Open the link, check the caller, recipient, sequence, and entry data, and run the request yourself. Preparing the request does not place the call. Add --dry-run to inspect the CLI request without creating a confirmation. If confirmation expires, is cancelled, or ends without a recorded result, the command exits 5; follow the retry guidance before making another request.

For the MCP preview, call voice_calls_create with the fields from call.json and a required idempotency_key. Replace <unique-key-for-this-request-reuse-for-retries> with your chosen key:

Code example
{
  "from": "+12025550100",
  "to": "+12025550101",
  "sequence": {
    "id": "YOUR_SEQUENCE_ID",
    "entry_node_id": "YOUR_ENTRY_NODE_ID",
    "trigger_data": {}
  },
  "idempotency_key": "<unique-key-for-this-request-reuse-for-retries>"
}

Follow the returned browser review link to run the request. On the dynamic MCP endpoint, select voice_calls_create through execute. OAuth consent grants workspace permissions; the browser review runs this specific call.

A 202 Accepted response confirms admission. The sequence begins after the recipient answers. Publishing a newer version after acceptance does not change this call's selected definition or entry data.

Read the acceptance result

Retain the response's id, initial_leg_id, and sequence.run_id for correlation. The top-level id identifies the call across its legs; initial_leg_id identifies its first leg.

The response is an immutable acceptance snapshot. Its started_at and ended_at are null, observation booleans are false, and parties is empty. These fields describe acceptance, so they do not become a live status report when you replay the request.

Open the sequence's Runs to inspect progress. An inline call belongs to no sequence, so follow its leg instead; if it fails before its leg registers, there is not yet a way to inspect it. Once a leg record exists, use GET /v1/voice/legs/{leg_id} with initial_leg_id, or inspect the call log. Reading legs requires voice:read permission, which voice:write includes.

A leg with status rejected was refused before it connected, so its sequence did not run. Without a rejection reason, the far end declined the call; see Rejected calls.

With the CLI, bird voice calls get YOUR_CALL_ID shows the call and its parties. bird voice legs trace YOUR_LEG_ID shows which steps ran, the digits collected unless the gather is private, and the exit's status and reason. The trace is ready shortly after the call ends; until then the command returns an error, so retry it. A rejected leg's sequence never ran, so it may have no trace.

Acceptance does not guarantee dialing, an answer, or a readable call record. A call that fails before registration may appear in run history without a leg record. An answered call also does not establish that the sequence completed its intended business task.

Handle errors and retries

For direct HTTP requests, idempotency is optional. Without a key, each request accepts a new call attempt. The SDKs generate a key for automatic retries. To protect retries across separate calls or direct HTTP requests, supply a key on the first attempt and reuse it with the exact original request bytes. See the idempotency guide.

An identical replay within three hours returns the original acceptance and Idempotency-Replay: true. Changed input with that key returns 409. Authorization and calling-number permission are checked again on replay. After the replay window, the key can admit a new call, so reconcile the original result before resubmitting.

For CLI and MCP, supply the idempotency key on the first attempt. Repeating the same request and key with the same OAuth grant returns the existing confirmation while its record is retained. Keep the original input and confirmation ID. Changing the key can prepare another billable call.

If the CLI command is interrupted, resume with the original input, key, and returned confirmation ID:

Code example
bird voice calls create --body-file call.json \
  --idempotency-key "${IDEMPOTENCY_KEY:?Set IDEMPOTENCY_KEY to the saved key for this intended call}" \
  --confirmation-id YOUR_CONFIRMATION_ID

For MCP, repeat the original tool arguments, including the same idempotency_key, together with the returned request state. The state identifies the confirmation and does not replace those arguments. Alternatively, repeat the same tool and arguments with the returned ID in bird/confirmation-id request metadata. With dynamic MCP, repeat the same execute request and put that metadata on it. Resuming reads the original confirmation and returns its recorded result when completed.

A Create Call confirmation expires after 90 minutes. A pending or expired confirmation without a result does not prove the call never ran. Inspect the original sequence run and any call-leg record before deciding to place another call; do not create a fresh key to resolve an unknown outcome. A completed confirmation with a 202 result proves acceptance, so continue with the acceptance checks to establish the call's outcome.

SymptomWhat to check
400 or 422Read the error's field details. Check number format, calling-number eligibility and ownership, sequence and entry IDs, explicit data object, and request limits.
401 or 403Check your API key or OAuth login, both required write permissions, and workspace access.
404Check the sequence belongs to the selected workspace and that call creation is available on the regional service.
409Check for a changed-body retry or an unavailable active production sequence. Publish or resume as needed before beginning a new call.
429Respect the response's retry guidance. If you supplied a key, reuse it for the same call attempt.
Accepted, then failedInspect the run and any leg record for destination, limits, balance, or runtime failures before deciding to place another call.

Next steps

Continue with the documentation, guides and examples for this topic.