Place a Call Endpoint
curl --request POST \
--url https://app.tuco.ai/api/voice/calls \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"from": "<string>",
"to": "<string>",
"agent": "<string>",
"leadId": "<string>"
}
'import requests
url = "https://app.tuco.ai/api/voice/calls"
payload = {
"from": "<string>",
"to": "<string>",
"agent": "<string>",
"leadId": "<string>"
}
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({from: '<string>', to: '<string>', agent: '<string>', leadId: '<string>'})
};
fetch('https://app.tuco.ai/api/voice/calls', 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://app.tuco.ai/api/voice/calls",
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([
'from' => '<string>',
'to' => '<string>',
'agent' => '<string>',
'leadId' => '<string>'
]),
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://app.tuco.ai/api/voice/calls"
payload := strings.NewReader("{\n \"from\": \"<string>\",\n \"to\": \"<string>\",\n \"agent\": \"<string>\",\n \"leadId\": \"<string>\"\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://app.tuco.ai/api/voice/calls")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"from\": \"<string>\",\n \"to\": \"<string>\",\n \"agent\": \"<string>\",\n \"leadId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/voice/calls")
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 \"from\": \"<string>\",\n \"to\": \"<string>\",\n \"agent\": \"<string>\",\n \"leadId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"callId": "<string>",
"status": "<string>"
}Voice / Calling
Place a Call Endpoint
Place an outbound click-to-call from your own system: Tuco dials your agent, dials the lead from your Tuco number, and bridges the two.
POST
/
api
/
voice
/
calls
Place a Call Endpoint
curl --request POST \
--url https://app.tuco.ai/api/voice/calls \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"from": "<string>",
"to": "<string>",
"agent": "<string>",
"leadId": "<string>"
}
'import requests
url = "https://app.tuco.ai/api/voice/calls"
payload = {
"from": "<string>",
"to": "<string>",
"agent": "<string>",
"leadId": "<string>"
}
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({from: '<string>', to: '<string>', agent: '<string>', leadId: '<string>'})
};
fetch('https://app.tuco.ai/api/voice/calls', 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://app.tuco.ai/api/voice/calls",
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([
'from' => '<string>',
'to' => '<string>',
'agent' => '<string>',
'leadId' => '<string>'
]),
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://app.tuco.ai/api/voice/calls"
payload := strings.NewReader("{\n \"from\": \"<string>\",\n \"to\": \"<string>\",\n \"agent\": \"<string>\",\n \"leadId\": \"<string>\"\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://app.tuco.ai/api/voice/calls")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"from\": \"<string>\",\n \"to\": \"<string>\",\n \"agent\": \"<string>\",\n \"leadId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.tuco.ai/api/voice/calls")
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 \"from\": \"<string>\",\n \"to\": \"<string>\",\n \"agent\": \"<string>\",\n \"leadId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"callId": "<string>",
"status": "<string>"
}This endpoint is in beta. The interface is stable enough to build on; we may add fields as it matures.
Overview
Place an outbound call programmatically with a single request. Tuco rings your agent first, then dials the lead from your Tuco number, and bridges the two legs — so the lead sees the same number they’ve been texting, and your rep’s personal number is never exposed.US and Canada (
+1) numbers only.What you’ll need
Three things before you make a request:1
Your API key
A workspace API key (starts with
tuco_). Get it in the Tuco dashboard under
Settings → Integrations → API Keys → Create API Key. Send it as a bearer
token: Authorization: Bearer tuco_.... This is the same key used for the
Messages API.2
A voice-verified calling line
The call goes out from a line in your workspace whose caller ID is
voice-verified — that’s the number the recipient sees. Pass it as
from,
using either:- the line’s
+1number — simplest, no lookup needed, or - the line ID — go to the Lines page in the dashboard; each line row
has a Copy Line ID button (the copy icon next to the line) — click it
to copy the ID. You can also get it from the
List Lines API (each line’s
_id).
3
Two phone numbers
The lead you want to reach (
to) and your rep’s phone (agent), both
in E.164 format (US/CA, e.g. +13025550123).How it works
POST /api/voice/calls
│
├─ 1. Tuco dials your AGENT → your rep's phone rings first
├─ 2. agent answers → Tuco dials the LEAD from your Tuco number
├─ 3. lead answers → Tuco bridges the two legs
└─ call is recorded, then billed on hangup
Making a request
- Method:
POST - URL:
https://app.tuco.ai/api/voice/calls - Headers:
Authorization: Bearer tuco_...andContent-Type: application/json - Body: JSON with
from,to, andagent
Request body
{
"from": "69f918a0f62015705ca41fbb",
"to": "+13025550123",
"agent": "+13025550456"
}
string
required
Your calling line — pass either the line ID or the line’s
+1 number.
Must be a line in your workspace with a voice-verified caller ID. This is the
number the lead sees.string
required
The number to call, E.164 US/CA (e.g.
+13025550123). Non-toll-free. This is
just a phone number — the contact does not need to exist in Tuco first; you
can call any US/CA number directly.string
required
Your rep’s phone number, E.164 US/CA. This leg is dialed first — your rep
answers, then the number is dialed and bridged in.
string
Optional, advanced. Only if you already have a lead record in Tuco and want the
call logged against it (shows in that lead’s Unibox thread). Not required to
place a call.
Example
curl -X POST https://app.tuco.ai/api/voice/calls \
-H "Authorization: Bearer tuco_your_key" \
-H "Content-Type: application/json" \
-d '{
"from": "<your line ID or +1 number>",
"to": "+13025550123",
"agent": "+13025550456"
}'
Response
202 Accepted — the call was accepted and is being placed. Ringing takes a
few seconds, so the response returns a call ID immediately and the call then
progresses asynchronously.
{
"callId": "6a4ed2d5dec1b2d01a6f7656",
"status": "requested"
}
string
The call record ID (MongoDB ObjectId).
string
Always
"requested" on a successful create.Call lifecycle
The call advances through these states on the call record:requested → ringing → answered → billed
↘ failed
| State | Meaning |
|---|---|
requested | Accepted; the agent leg is dialing |
ringing | A leg is ringing |
answered | Both legs connected and bridged |
billed | Call ended; durationSec, billedSec, and the recording URL are settled |
failed | The agent didn’t answer, or the call couldn’t be placed |
The
agent leg rings first — your rep must answer within ~30 seconds or the call
times out. If the agent’s phone goes to voicemail, the call ends without dialing
or bridging the lead.Getting the recording
Recordings are captured automatically. After a call ends, fetch the recording (and the call’s timing details) with the Get Call Recordings endpoint, using the lead’s number. To get the most recent call’s recording for a lead, passlatest=true:
curl "https://app.tuco.ai/api/voice/recordings?number=+13025550123&latest=true" \
-H "Authorization: Bearer tuco_your_key"
recordingUrl in the response is the download link. Drop latest=true to get
all calls to that number (newest first).
Recordings resolve a few seconds after the call ends. If you request right on
hangup and
recordingUrl comes back null, wait ~5-10 seconds and retry. See the
Get Call Recordings endpoint for all
parameters.Errors
All errors return{ "error": "...", "code": "..." }.
| HTTP | code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 400 | MISSING_FROM | from (calling line) not provided |
| 400 | MISSING_TO | to (lead number) not provided |
| 400 | MISSING_AGENT | agent (rep number) not provided |
| 400 | INVALID_TO | to is not a US/CA number, or is toll-free |
| 400 | INVALID_AGENT | agent is not a US/CA number, or is toll-free |
| 403 | VOICE_NOT_ENABLED | Voice calling is not enabled for this workspace |
| 404 | LINE_NOT_FOUND | from line does not exist in your workspace |
| 409 | LINE_NOT_VERIFIED | The line exists but its caller ID is not voice-verified |
| 429 | CONCURRENT_CAP | Too many live calls on this line |
| 429 | LINE_MONTHLY_CAP | The line hit its monthly minutes cap |
| 402 | MINUTES_EXHAUSTED | The workspace is out of voice minutes |
| 502 | ORIGINATE_FAILED | The carrier rejected the outbound dial |
| 500 | INTERNAL_ERROR | Unexpected server error |