curl --request POST \
--url https://api.erdo.ai/v1/voice/person-calls \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"canonical_lead_id": "<string>",
"from_number": "<string>",
"person_phone": "<string>",
"placed_by": {
"name": "<string>",
"ref": "<string>"
},
"to_number": "<string>",
"prompt_language": "<string>",
"recipient_name": "<string>"
}
'import requests
url = "https://api.erdo.ai/v1/voice/person-calls"
payload = {
"canonical_lead_id": "<string>",
"from_number": "<string>",
"person_phone": "<string>",
"placed_by": {
"name": "<string>",
"ref": "<string>"
},
"to_number": "<string>",
"prompt_language": "<string>",
"recipient_name": "<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({
canonical_lead_id: '<string>',
from_number: '<string>',
person_phone: '<string>',
placed_by: {name: '<string>', ref: '<string>'},
to_number: '<string>',
prompt_language: '<string>',
recipient_name: '<string>'
})
};
fetch('https://api.erdo.ai/v1/voice/person-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://api.erdo.ai/v1/voice/person-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([
'canonical_lead_id' => '<string>',
'from_number' => '<string>',
'person_phone' => '<string>',
'placed_by' => [
'name' => '<string>',
'ref' => '<string>'
],
'to_number' => '<string>',
'prompt_language' => '<string>',
'recipient_name' => '<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://api.erdo.ai/v1/voice/person-calls"
payload := strings.NewReader("{\n \"canonical_lead_id\": \"<string>\",\n \"from_number\": \"<string>\",\n \"person_phone\": \"<string>\",\n \"placed_by\": {\n \"name\": \"<string>\",\n \"ref\": \"<string>\"\n },\n \"to_number\": \"<string>\",\n \"prompt_language\": \"<string>\",\n \"recipient_name\": \"<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://api.erdo.ai/v1/voice/person-calls")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"canonical_lead_id\": \"<string>\",\n \"from_number\": \"<string>\",\n \"person_phone\": \"<string>\",\n \"placed_by\": {\n \"name\": \"<string>\",\n \"ref\": \"<string>\"\n },\n \"to_number\": \"<string>\",\n \"prompt_language\": \"<string>\",\n \"recipient_name\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.erdo.ai/v1/voice/person-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 \"canonical_lead_id\": \"<string>\",\n \"from_number\": \"<string>\",\n \"person_phone\": \"<string>\",\n \"placed_by\": {\n \"name\": \"<string>\",\n \"ref\": \"<string>\"\n },\n \"to_number\": \"<string>\",\n \"prompt_language\": \"<string>\",\n \"recipient_name\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"call_id": "<string>",
"from_number": "<string>",
"message": "<string>",
"person_country": "<string>",
"person_phone": "<string>",
"prompt_language": "<string>",
"reason": "<string>",
"refused": true,
"to_country": "<string>",
"to_number": "<string>"
}{
"code": "not_found",
"details": {},
"message": "<string>"
}StartPersonCallAPI connects a person to a lead: Erdo rings the person's own
phone from one of the organization’s voice agent numbers, and when they answer and press 1 it dials the lead and bridges the two. The lead sees the organization’s number.
It has no MCP tool and no CLI verb, and that is the point rather than an omission. Every other capability on this gateway completes on its own; this one rings a phone that a specific human being has to be holding, and answer, within about thirty seconds. An agent or a script that “placed a call” would have started something only a person can finish — so the only caller that makes sense is a product surface with a person looking at it, which reaches Erdo over REST.
The refusals are values, not prose: `refused` with a `reason` the caller branches on (the number is not this organization’s, the lead opted out, that person is already on a call, the provider would not dial the destination). A successful response echoes both numbers normalised to E.164 with their countries, because a number typed without a country code has just been read as US/Canada and the person placing the call is the only one who can catch that.
curl --request POST \
--url https://api.erdo.ai/v1/voice/person-calls \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"canonical_lead_id": "<string>",
"from_number": "<string>",
"person_phone": "<string>",
"placed_by": {
"name": "<string>",
"ref": "<string>"
},
"to_number": "<string>",
"prompt_language": "<string>",
"recipient_name": "<string>"
}
'import requests
url = "https://api.erdo.ai/v1/voice/person-calls"
payload = {
"canonical_lead_id": "<string>",
"from_number": "<string>",
"person_phone": "<string>",
"placed_by": {
"name": "<string>",
"ref": "<string>"
},
"to_number": "<string>",
"prompt_language": "<string>",
"recipient_name": "<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({
canonical_lead_id: '<string>',
from_number: '<string>',
person_phone: '<string>',
placed_by: {name: '<string>', ref: '<string>'},
to_number: '<string>',
prompt_language: '<string>',
recipient_name: '<string>'
})
};
fetch('https://api.erdo.ai/v1/voice/person-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://api.erdo.ai/v1/voice/person-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([
'canonical_lead_id' => '<string>',
'from_number' => '<string>',
'person_phone' => '<string>',
'placed_by' => [
'name' => '<string>',
'ref' => '<string>'
],
'to_number' => '<string>',
'prompt_language' => '<string>',
'recipient_name' => '<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://api.erdo.ai/v1/voice/person-calls"
payload := strings.NewReader("{\n \"canonical_lead_id\": \"<string>\",\n \"from_number\": \"<string>\",\n \"person_phone\": \"<string>\",\n \"placed_by\": {\n \"name\": \"<string>\",\n \"ref\": \"<string>\"\n },\n \"to_number\": \"<string>\",\n \"prompt_language\": \"<string>\",\n \"recipient_name\": \"<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://api.erdo.ai/v1/voice/person-calls")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"canonical_lead_id\": \"<string>\",\n \"from_number\": \"<string>\",\n \"person_phone\": \"<string>\",\n \"placed_by\": {\n \"name\": \"<string>\",\n \"ref\": \"<string>\"\n },\n \"to_number\": \"<string>\",\n \"prompt_language\": \"<string>\",\n \"recipient_name\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.erdo.ai/v1/voice/person-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 \"canonical_lead_id\": \"<string>\",\n \"from_number\": \"<string>\",\n \"person_phone\": \"<string>\",\n \"placed_by\": {\n \"name\": \"<string>\",\n \"ref\": \"<string>\"\n },\n \"to_number\": \"<string>\",\n \"prompt_language\": \"<string>\",\n \"recipient_name\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"call_id": "<string>",
"from_number": "<string>",
"message": "<string>",
"person_country": "<string>",
"person_phone": "<string>",
"prompt_language": "<string>",
"reason": "<string>",
"refused": true,
"to_country": "<string>",
"to_number": "<string>"
}{
"code": "not_found",
"details": {},
"message": "<string>"
}Authorizations
An Erdo API key (erdo_api_...) or scoped token (erdo_token_...).
Body
22-character reference form. Required: the lead's timeline finds calls by this id, and a call that could not appear on it would defeat the point.
number the lead will see. It must be one Erdo manages inside the organization's Twilio subaccount.
record — the provider already holds it for the duration of the call, and keeping a copy of an employee's mobile beside every lead is not something this feature needs.
terms. Ref is whatever stable identifier that surface has for them (Maurice sends its Clerk user id); Erdo never resolves it, it only keys the one-live- call-per-person rule on it and shows Name beside the call.
Show child attributes
Show child attributes
it is normalised to E.164 and echoed back.
default), "es" or "pt". Anything else is refused rather than silently spoken in English at somebody who does not speak it.
call Maria Lopez") and shown on the call record.
Response
Success response

