Calls
Create phone call
Place an outbound phone call with an AI voice agent from one of your phone numbers. Pass dynamic variables to personalize each call.
POST
/
v2
/
call
Create phone call
curl --request POST \
--url https://api.verbeo.ai/v2/call \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to_number": "<string>",
"from_number": "<string>",
"agent_id": "<string>",
"dynamic_variables": {}
}
'import requests
url = "https://api.verbeo.ai/v2/call"
payload = {
"to_number": "<string>",
"from_number": "<string>",
"agent_id": "<string>",
"dynamic_variables": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
to_number: '<string>',
from_number: '<string>',
agent_id: '<string>',
dynamic_variables: {}
})
};
fetch('https://api.verbeo.ai/v2/call', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.verbeo.ai/v2/call",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'to_number' => '<string>',
'from_number' => '<string>',
'agent_id' => '<string>',
'dynamic_variables' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.verbeo.ai/v2/call"
payload := strings.NewReader("{\n \"to_number\": \"<string>\",\n \"from_number\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"dynamic_variables\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.verbeo.ai/v2/call")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to_number\": \"<string>\",\n \"from_number\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"dynamic_variables\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.verbeo.ai/v2/call")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"to_number\": \"<string>\",\n \"from_number\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"dynamic_variables\": {}\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"status": "<string>",
"to_number": "<string>",
"from_number": "<string>",
"agent_id": "<string>",
"agent_name": "<string>"
}Places one outbound call. You name the number to call and one of your workspace’s phone numbers to call from; Verbeo dials the destination and connects your assistant. Dynamic variables you pass are filled into the assistant’s system prompt for this call, so you can personalize each call without creating separate assistants.
The workspace comes from your API key, so the body names none.
This is version 2 of the endpoint, and the one to build on. It answers the standard envelope every other resource uses. Version 1 (
POST /calls) is unchanged and keeps working for integrations built on it.Endpoint
POST https://api.verbeo.ai/v2/call
Body
string
required
The number to call, in E.164 format:
+14155550123. Spaces, dashes and brackets are accepted and removed. Always send the + and the country code. A number without the + passes validation and is dialed exactly as sent, but Verbeo’s destination rules read it as if it began with +: a national number such as 808 555 0123 is then read as a +808 number and refused with 403, and whatever is not refused is left to your carrier to read.string
required
The number to call from, exactly as it appears in your workspace (
phone_number on the phone number object). It must be an active number of your workspace.string
UUID of the assistant that handles the call. Optional — leave it out and the call uses the outbound assistant set on
from_number. If that number has none either, the request is rejected.object
Values for the
{{variables}} in the assistant’s system prompt, by name. Every value must be a string. A name that starts with two underscores (__) is reserved: it is ignored, whatever it holds.{
"customer_name": "Jane Smith",
"order_id": "ORD-98765"
}
The response returns immediately, as soon as the call is created — it does not wait for the destination to answer, and a
201 does not mean the phone rang. A call that then fails to connect (no answer, busy, a carrier error) is reported by its call_ended webhook. Track the outcome with the call_started, call_ended and call_analyzed webhooks: their call_id is the id this endpoint returns.Request
curl -X POST "https://api.verbeo.ai/v2/call" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to_number": "+14155550123",
"from_number": "+14155550100",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"dynamic_variables": {
"customer_name": "Jane Smith",
"order_id": "ORD-98765"
}
}'
curl -X POST "https://api.verbeo.ai/v2/call" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to_number": "+14155550123",
"from_number": "+14155550100"
}'
const res = await fetch("https://api.verbeo.ai/v2/call", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VERBEO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
to_number: "+14155550123",
from_number: "+14155550100",
agent_id: "550e8400-e29b-41d4-a716-446655440000",
dynamic_variables: {
customer_name: "Jane Smith",
order_id: "ORD-98765",
},
}),
});
const body = await res.json();
if (!res.ok) throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`);
const call = body.data;
import os, requests
res = requests.post(
"https://api.verbeo.ai/v2/call",
headers={"Authorization": f"Bearer {os.environ['VERBEO_API_KEY']}"},
json={
"to_number": "+14155550123",
"from_number": "+14155550100",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"dynamic_variables": {
"customer_name": "Jane Smith",
"order_id": "ORD-98765",
},
},
)
body = res.json()
if not res.ok:
error = body.get("error", {})
raise RuntimeError(f"{res.status_code} {error.get('code')}: {error.get('message')}")
call = body["data"]
Response
201 Created as soon as the call is created.
{
"success": true,
"data": {
"id": "b7a3c1e4-94f2-4c9d-a8e0-5d9f6c2a1b34",
"status": "queued",
"to_number": "+14155550123",
"from_number": "+14155550100",
"agent_id": "550e8400-e29b-41d4-a716-446655440000",
"agent_name": "Customer Support"
}
}
string
required
UUID of the call. It is the
call_id of the call’s webhook events.string
required
The status the call starts in:
queued — it has been created and is being placed.string
required
The number dialed, without the separators you may have sent.
string
required
The number the call is placed from.
string
required
UUID of the assistant on the call — the one you named, or the number’s outbound assistant.
string
required
That assistant’s name.
Errors
Every error uses the standard error envelope. Branch onerror.code.
| Code | Status | When |
|---|---|---|
VALIDATION_FAILED | 422 | A missing or malformed field — the message names it, e.g. to_number: must be a phone number in E.164 format, e.g. +14155550123. Also: from_number is not an active number of your workspace; agent_id is not an assistant of your workspace; no agent_id was sent and the number has no outbound assistant; or, for a number imported by its Twilio credentials, Twilio refused the call — the message carries its reason. |
PAYMENT_REQUIRED | 402 | Your balance is below what is needed to start a call. Add funds on the Billing page. |
FORBIDDEN | 403 | A public (vbr_pub_…) key; billing is paused on the workspace; or calls to this destination are not permitted for the workspace. |
TOO_MANY_REQUESTS | 429 | The concurrent-call limit is reached, or the day’s spending limit is reached. The concurrent-call limit is your workspace’s, counted across all of its calls; for a number imported by its Twilio credentials it is counted on that number’s calls alone. For such a number, also: the number is dialing at its calls-per-second limit, or Twilio is rate-limiting your account. |
NOT_FOUND | 404 | The number or the assistant was deleted while the call was being placed. Also a body that names a workspace_id other than your key’s. |
BAD_GATEWAY | 502 | The request to place the call failed on Verbeo’s or the carrier’s side, and the call may or may not have been created. |
INTERNAL_ERROR | 500 | Unexpected failure before the call was created. |
SERVICE_UNAVAILABLE | 503 | Outbound calling is paused on Verbeo’s side. Nothing is wrong with your account. |
UNAUTHORIZED | 401 | Invalid, inactive or expired key. A request with no Authorization: Bearer vbr_… header is a 401 too, but outside the envelope: check the status first. |
Retrying
| Code | Retry? | Notes |
|---|---|---|
TOO_MANY_REQUESTS | Yes, with a limit | Wait for a call to finish, or a moment for the dial rate, then send the same request. The daily spending limit does not clear until the next UTC day or until it is raised, so cap your retries. |
SERVICE_UNAVAILABLE | Yes | Retry later; calls resume on their own. |
BAD_GATEWAY | Check first | A call may already exist. Look for it in the call history, or for its call_started webhook if the assistant has a webhook URL, before you send the request again, or you may call the same person twice. |
INTERNAL_ERROR | Yes | No call was created. Use exponential backoff. |
PAYMENT_REQUIRED | After topping up | The same request goes through once the balance allows it. |
VALIDATION_FAILED | No | Fix the request, then retry. |
To personalize calls at scale, put placeholders like
{{customer_name}} in your assistant’s system prompt and pass the values through dynamic_variables on each call.Coming from version 1
Rename every field. Version 2 reads only its own names and ignores version 1’s without an error: a body that still sends
agentId places the call with the number’s outbound assistant instead, and one that still sends dynamicVariables places it with no variables.Version 1 — POST /calls | Version 2 — POST /v2/call | |
|---|---|---|
| Body fields | toNumber, fromNumber, agentId, dynamicVariables | to_number, from_number, agent_id, dynamic_variables |
| Success | 201 with the fields at the top level: { "success": true, "callId": "…", … } | 201 with the call under data: { "success": true, "data": { "id": "…", … } } |
| The call’s id | callId | data.id |
| Errors | { "error": "…", "details": "…" } | { "success": false, "error": { "code": "…", "message": "…" } } |
| A bad field | 400 | 422 VALIDATION_FAILED |
| A number or assistant that is not yours | 404 | 422 VALIDATION_FAILED, naming the field |
| The concurrent-call limit, for a number imported by its Twilio credentials | Counted across the workspace’s calls | Counted on that number’s calls |
Was this page helpful?
⌘I
Create phone call
curl --request POST \
--url https://api.verbeo.ai/v2/call \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"to_number": "<string>",
"from_number": "<string>",
"agent_id": "<string>",
"dynamic_variables": {}
}
'import requests
url = "https://api.verbeo.ai/v2/call"
payload = {
"to_number": "<string>",
"from_number": "<string>",
"agent_id": "<string>",
"dynamic_variables": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
to_number: '<string>',
from_number: '<string>',
agent_id: '<string>',
dynamic_variables: {}
})
};
fetch('https://api.verbeo.ai/v2/call', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.verbeo.ai/v2/call",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'to_number' => '<string>',
'from_number' => '<string>',
'agent_id' => '<string>',
'dynamic_variables' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.verbeo.ai/v2/call"
payload := strings.NewReader("{\n \"to_number\": \"<string>\",\n \"from_number\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"dynamic_variables\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.verbeo.ai/v2/call")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"to_number\": \"<string>\",\n \"from_number\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"dynamic_variables\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.verbeo.ai/v2/call")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"to_number\": \"<string>\",\n \"from_number\": \"<string>\",\n \"agent_id\": \"<string>\",\n \"dynamic_variables\": {}\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"status": "<string>",
"to_number": "<string>",
"from_number": "<string>",
"agent_id": "<string>",
"agent_name": "<string>"
}