Make Your First Call
Connect two phone numbers with Exotel in under 5 minutes. This guide walks you through making an API request that calls one phone first, then connects it to another.
How It Works
- You make a POST request to Exotel's API
- Exotel calls the From number first (e.g., your agent)
- When they pick up, Exotel dials the To number (e.g., the customer)
- Both parties are connected through your ExoPhone (virtual number)
This is the most common pattern with which developers integrate Programmable Voice APIs. It is commonly referred to as Click to Call. You can integrate this into your CRM, app, or website. When a customer receives a call, they see your ExoPhone number, not the agent's number.
Prerequisites
- An Exotel account (Sign up here)
- API credentials (API Key, API Token, Account SID) — see Authentication
- An ExoPhone (virtual phone number) — purchase from Dashboard or via ExoPhones API
Step 1: Set Up Your Credentials
Store your credentials as environment variables:
- macOS / Linux
- Windows
export EXOTEL_API_KEY="your_api_key"
export EXOTEL_API_TOKEN="your_api_token"
export EXOTEL_ACCOUNT_SID="your_account_sid"
export EXOTEL_SUBDOMAIN="api.exotel.com"
set EXOTEL_API_KEY=your_api_key
set EXOTEL_API_TOKEN=your_api_token
set EXOTEL_ACCOUNT_SID=your_account_sid
set EXOTEL_SUBDOMAIN=api.exotel.com
Use api.exotel.com (Singapore) for most accounts, or api.in.exotel.com (Mumbai) for India-based accounts.
Step 2: Make Your First Call
- cURL
- Python
- Node.js
- PHP
- Go
curl -u "$EXOTEL_API_KEY:$EXOTEL_API_TOKEN" -X POST \
"https://$EXOTEL_SUBDOMAIN/v1/Accounts/$EXOTEL_ACCOUNT_SID/Calls/connect" \
-d "From=+919876543210" \
-d "To=+919123456789" \
-d "CallerId=YOUR_EXOPHONE" \
-d "Record=true" \
-d "StatusCallback=https://your-server.com/call-status" \
-d "StatusCallbackContentType=application/json"
# make_call.py
import os
import requests
api_key = os.environ['EXOTEL_API_KEY']
api_token = os.environ['EXOTEL_API_TOKEN']
account_sid = os.environ['EXOTEL_ACCOUNT_SID']
subdomain = os.environ.get('EXOTEL_SUBDOMAIN', 'api.exotel.com')
url = f'https://{subdomain}/v1/Accounts/{account_sid}/Calls/connect'
data = {
'From': '+919876543210', # Agent's number (called first)
'To': '+919123456789', # Customer's number (called second)
'CallerId': 'YOUR_EXOPHONE', # Your virtual phone number
'Record': 'true', # Enable call recording
'StatusCallback': 'https://your-server.com/call-status',
'StatusCallbackContentType': 'application/json',
}
response = requests.post(url, data=data, auth=(api_key, api_token))
if response.ok:
result = response.json()
print('Call initiated!')
print('Call SID:', result['Call']['Sid'])
print('Status:', result['Call']['Status'])
else:
print('Failed:', response.text)
Run with:
pip install requests
python make_call.py
// make-call.js — Requires Node.js 18+
const apiKey = process.env.EXOTEL_API_KEY;
const apiToken = process.env.EXOTEL_API_TOKEN;
const accountSid = process.env.EXOTEL_ACCOUNT_SID;
const subdomain = process.env.EXOTEL_SUBDOMAIN || 'api.exotel.com';
async function makeCall() {
const url = `https://${subdomain}/v1/Accounts/${accountSid}/Calls/connect`;
const params = new URLSearchParams({
From: '+919876543210', // Agent's number (called first)
To: '+919123456789', // Customer's number (called second)
CallerId: 'YOUR_EXOPHONE', // Your virtual phone number
Record: 'true', // Enable call recording
StatusCallback: 'https://your-server.com/call-status',
StatusCallbackEvents: 'terminal,answered',
StatusCallbackContentType: 'application/json',
});
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': 'Basic ' + Buffer.from(`${apiKey}:${apiToken}`).toString('base64'),
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params,
});
const data = await response.json();
if (response.ok) {
console.log('Call initiated!');
console.log('Call SID:', data.Call.Sid);
console.log('Status:', data.Call.Status);
} else {
console.error('Failed:', data);
}
} catch (error) {
console.error('Error:', error.message);
}
}
makeCall();
Run with:
node make-call.js
<?php
$apiKey = getenv('EXOTEL_API_KEY');
$apiToken = getenv('EXOTEL_API_TOKEN');
$accountSid = getenv('EXOTEL_ACCOUNT_SID');
$subdomain = getenv('EXOTEL_SUBDOMAIN') ?: 'api.exotel.com';
$data = array(
'From' => '+919876543210',
'To' => '+919123456789',
'CallerId' => 'YOUR_EXOPHONE',
'Record' => 'true',
'StatusCallback' => 'https://your-server.com/call-status',
'StatusCallbackContentType' => 'application/json'
);
$ch = curl_init("https://{$subdomain}/v1/Accounts/{$accountSid}/Calls/connect");
curl_setopt($ch, CURLOPT_USERPWD, "{$apiKey}:{$apiToken}");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
?>
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
)
func main() {
apiKey := os.Getenv("EXOTEL_API_KEY")
apiToken := os.Getenv("EXOTEL_API_TOKEN")
accountSid := os.Getenv("EXOTEL_ACCOUNT_SID")
subdomain := os.Getenv("EXOTEL_SUBDOMAIN")
if subdomain == "" {
subdomain = "api.exotel.com"
}
endpoint := fmt.Sprintf("https://%s/v1/Accounts/%s/Calls/connect", subdomain, accountSid)
data := url.Values{}
data.Set("From", "+919876543210")
data.Set("To", "+919123456789")
data.Set("CallerId", "YOUR_EXOPHONE")
data.Set("Record", "true")
data.Set("StatusCallback", "https://your-server.com/call-status")
data.Set("StatusCallbackContentType", "application/json")
req, _ := http.NewRequest("POST", endpoint, strings.NewReader(data.Encode()))
req.SetBasicAuth(apiKey, apiToken)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := ioutil.ReadAll(res.Body)
fmt.Println(string(body))
}
Step 3: Check the Response
A successful request returns:
{
"Call": {
"Sid": "c5797dcbaaeed7678c4062a4a3ed2f8a",
"DateCreated": "2026-02-25 10:48:33",
"AccountSid": "your_sid",
"To": "+919123456789",
"From": "+919876543210",
"PhoneNumberSid": "YOUR_EXOPHONE",
"Status": "in-progress",
"Direction": "outbound-api",
"RecordingUrl": null
}
}
| Field | What it means |
|---|---|
Sid | Your unique Call ID — save this to check status later |
Status | in-progress means the call is being dialed |
RecordingUrl | Populated after the call ends (if Record=true) |
The call goes through these states: queued → in-progress → completed (or failed / busy / no-answer). Use Call Details or StatusCallback for the final status.
Step 4: Check Call Details (Optional)
After the call ends, retrieve the final status and recording:
curl -u "$EXOTEL_API_KEY:$EXOTEL_API_TOKEN" \
"https://$EXOTEL_SUBDOMAIN/v1/Accounts/$EXOTEL_ACCOUNT_SID/Calls/YOUR_CALL_SID.json"
Step 5: Handle Status Callbacks (Optional)
If you set a StatusCallback URL, Exotel sends a webhook when the call completes:
{
"CallSid": "c5797dcbaaeed7678c4062a4a3ed2f8a",
"Status": "completed",
"From": "+919876543210",
"To": "+919123456789",
"Direction": "outbound-api",
"Duration": "45",
"RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/...",
"StartTime": "2026-02-25 10:48:33",
"EndTime": "2026-02-25 10:49:18"
}
See Voice Webhooks for the full payload reference.
Enable Call Recording
- Set
Record=truein your request - Recording URL appears in the response and webhook as
RecordingUrl - Use
RecordingChannels=dualfor separate agent/customer audio tracks - Formats available:
mp3(default) ormp3-hqfor higher quality
What's Next?
- Connect Two Numbers — Full API Reference — All 19 parameters explained
- Connect to Flow — Build IVR Systems — Route calls to IVR flows
- Call Details — Check Call Status — Retrieve call details and recordings
- Voice Webhooks — Real-time call status updates
- Applets — Build Call Flows — IVR menu, voicemail, transfers