API reference / Outreach
Use your own numbers on BYOC
A bring-your-own-carrier (BYOC) account sends through your carrier, from numbers you hold there. There are four ways to put those numbers to work. This page shows each one, with code that ends in a ringless voicemail.
Connect the carrier first. See Bring your own carrier.
Pick a way in
| You want to | Do this | Receive calls back? | Text from it? |
|---|---|---|---|
| Send now, with nothing set up | Pass a caller ID | No | No |
| Use numbers already on your carrier account | Sync them from your carrier in the dashboard | Yes, after you point the trunk | With a registered texting campaign |
| Use a list of numbers you copied from your carrier | Load the list with the API | Yes, after you point the trunk | With a registered texting campaign |
| Get new numbers from your carrier | Buy them through Drop Cowboy® | Yes | With a registered texting campaign |
All four need the carrier connected. The first needs nothing else.
Before you start
- Create an API key with the scopes you use:
rvm:sendto send,numbers:writeto load or buy numbers,numbers:readto list them. See Authentication. - The examples read your key from
DC_KEYandDC_SECRET. - Every voicemail on BYOC needs audio. Give a public
audio_url(mp3 or wav, up to 50 MB), amedia_id, ortts_bodywithvoice_id. - Use numbers you hold and are allowed to call from. Your carrier decides whether it accepts a number as a caller ID.
Send from a caller ID
Nothing is loaded into Drop Cowboy. You name your carrier's number on the send and it goes out as given, through the route made when you connected the carrier. There is no route, trunk or host field on a send.
curl -X POST https://api-v2.dropcowboy.com/rvm \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Idempotency-Key: 3b7e9d1a-6c4f-4a82-b5d0-8f1e2c7a9b46" \
-H "Content-Type: application/json" \
-d '{
"to": "+13125550142",
"caller_id": "+17735550188",
"audio_url": "https://cdn.example.com/voicemails/appointment-reminder.mp3"
}'
The answer is 202 with a message_id. The result arrives on the status
webhook and your callback_url. See
Where results arrive.
Don't send phone_line_id with caller_id. The line wins, and a number that
isn't in Drop Cowboy isn't on a line.
Calls back to this number go to your carrier, not to Drop Cowboy. To have them reach a phone line, load the number as in the next sections.
Sync numbers from your carrier
Use this when the numbers are already on your carrier account. This is a dashboard step. The API has no route that lists your carrier's account.
- Open Connect your carrier in the left menu and click your carrier.
- Under Next: add your phone numbers, click Set Up Phone Line & Add Numbers.
- On the line's Numbers tab, click Add Phone Number, then Sync from Carrier.
- Click Load Carrier Numbers. Tick the numbers, or tick Import all numbers on account, then click Sync All Numbers.
The numbers join a phone line and stay with your carrier. List your numbers to
read each one's line in voice_ivr_id, or list your lines and take each line's
ivr_id. That value is the phone_line_id you send with.
curl https://api-v2.dropcowboy.com/phone/public/numbers \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
Then send from the line. The call uses the line's caller ID, and calls back reach the line.
curl -X POST https://api-v2.dropcowboy.com/rvm \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Idempotency-Key: 5c8f2a7e-1d4b-4e96-a3c0-7b2d9e6f1a58" \
-H "Content-Type: application/json" \
-d '{
"to": "+13125550142",
"phone_line_id": "234ecab5-1811-4c7d-a7a9-8c9ad9ca2e08",
"audio_url": "https://cdn.example.com/voicemails/appointment-reminder.mp3"
}'
You can also keep sending with caller_id after a number is synced. Leave out
phone_line_id when you do.
Load a list of numbers
Use this when you have the numbers as a list, for example copied from your
carrier's console, and want them on a phone line without the dashboard.
POST /phone/public/numbers/import loads them. It needs numbers:write, a
BYOC account and a connected carrier. Drop Cowboy doesn't check with your
carrier that you own them, so load only numbers you hold.
| Field | Type | Required | Description |
|---|---|---|---|
phone_numbers |
array of strings | Yes | E.164 numbers, up to 5,000. One bad value fails the request and is named in the message. |
phone_line_id |
string | No | The line the numbers go on. Default: your default voice line. |
overwrite_routing |
boolean | No | true moves numbers already on your account onto this line. Default false: they stay where they are. |
The route answers 202 with a long_job_id. Poll
GET /phone/public/numbers/import/{job_id} until status is completed or
failed. A number another account holds isn't taken. It's counted in
conflicts, and the rest still load.
This script loads two numbers, waits for the import, then sends from the first.
Node.js (18 or later):
const BASE = 'https://api-v2.dropcowboy.com';
const AUTH = { 'x-key': process.env.DC_KEY, 'x-secret': process.env.DC_SECRET };
async function dc(method, path, body, extraHeaders) {
const headers = Object.assign({ 'Content-Type': 'application/json' }, AUTH, extraHeaders);
const res = await fetch(BASE + path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined
});
const json = await res.json();
if (!res.ok) {
throw new Error(res.status + ' ' + (json.detail || json.title) + ' ' + JSON.stringify(json.details || {}));
}
return json;
}
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function loadNumbers(phoneNumbers) {
const accepted = await dc('POST', '/phone/public/numbers/import', { phone_numbers: phoneNumbers });
const jobId = accepted.data.long_job_id;
for (let attempt = 0; attempt < 60; attempt++) {
const job = (await dc('GET', '/phone/public/numbers/import/' + jobId)).data;
if (job.status === 'completed') return job.result;
if (job.status === 'failed') throw new Error('Import failed: ' + job.error);
await wait(2000);
}
throw new Error('Import is still running. Check job ' + jobId + ' later.');
}
async function sendVoicemail(to, callerId, audioUrl) {
return dc('POST', '/rvm', { to, caller_id: callerId, audio_url: audioUrl }, {
'Idempotency-Key': crypto.randomUUID()
});
}
(async () => {
const numbers = ['+13125550142', '+13125550143'];
const result = await loadNumbers(numbers);
console.log('Loaded', result.added, 'new,', result.updated, 'updated,', result.conflicts, 'held by another account');
const queued = await sendVoicemail(
'+13125550150',
numbers[0],
'https://cdn.example.com/voicemails/appointment-reminder.mp3'
);
console.log('Queued', queued.message_id);
})();
Python (3.9 or later, with requests):
import os
import time
import uuid
import requests
BASE = "https://api-v2.dropcowboy.com"
AUTH = {"x-key": os.environ["DC_KEY"], "x-secret": os.environ["DC_SECRET"]}
def dc(method, path, body=None, extra_headers=None):
headers = dict(AUTH)
headers.update(extra_headers or {})
res = requests.request(method, BASE + path, json=body, headers=headers, timeout=30)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data.get('detail') or data.get('title')} {data.get('details', {})}")
return data
def load_numbers(phone_numbers):
accepted = dc("POST", "/phone/public/numbers/import", {"phone_numbers": phone_numbers})
job_id = accepted["data"]["long_job_id"]
for _ in range(60):
job = dc("GET", f"/phone/public/numbers/import/{job_id}")["data"]
if job["status"] == "completed":
return job["result"]
if job["status"] == "failed":
raise RuntimeError(f"Import failed: {job['error']}")
time.sleep(2)
raise RuntimeError(f"Import is still running. Check job {job_id} later.")
def send_voicemail(to, caller_id, audio_url):
return dc(
"POST",
"/rvm",
{"to": to, "caller_id": caller_id, "audio_url": audio_url},
{"Idempotency-Key": str(uuid.uuid4())},
)
numbers = ["+13125550142", "+13125550143"]
result = load_numbers(numbers)
print("Loaded", result["added"], "new,", result["updated"], "updated,", result["conflicts"], "held by another account")
queued = send_voicemail(
"+13125550150",
numbers[0],
"https://cdn.example.com/voicemails/appointment-reminder.mp3",
)
print("Queued", queued["message_id"])
To send from the line instead of one number, replace caller_id with
phone_line_id in the send body.
The import doesn't change anything at your carrier. To receive calls on these numbers, point the trunk at us.
Buy numbers from your carrier
Use this to get new numbers. They're bought from your connected carrier, so your carrier account holds them. In the API it takes three calls: search, rent, send.
- In the dashboard, open the phone line's Numbers tab, click Add Phone Number, then Buy a New Number, and search as usual.
- With the API,
POST /phone/public/numbers/availablesearches your carrier's live inventory, andPOST /phone/public/numbers/rentbuys the numbers you pick. See Search for numbers to rent.
Search works for Twilio, Commio (ThinQ) and Sinch carriers. For any other
carrier, buy the number at your carrier and
load it. The search needs numbers:read and the rent
needs numbers:write. Each fails with 400 and a code when no carrier is
connected: byoc_not_configured for the search and pool_required for the
rent.
curl -X POST https://api-v2.dropcowboy.com/phone/public/numbers/available \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Content-Type: application/json" \
-d '{ "country_iso": "US", "pattern": "312", "type": "local", "limit": 5 }'
The answer is a list in data. Each result has a phone_number. Pass the ones
you want to the rent route:
curl -X POST https://api-v2.dropcowboy.com/phone/public/numbers/rent \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Content-Type: application/json" \
-d '{
"numbers": ["+13125550143"],
"voice_ivr_id": "234ecab5-1811-4c7d-a7a9-8c9ad9ca2e08"
}'
voice_ivr_id is optional. Without it the numbers go on your default phone
line. Each new number fires the number.provisioned webhook.
This Node.js code finds a number, buys it and sends from it. It uses the dc()
and sendVoicemail() helpers from Load a list of numbers:
async function buyAndSend(areaCode, lineId, to, audioUrl) {
const found = (await dc('POST', '/phone/public/numbers/available', {
pattern: areaCode,
limit: 5
})).data;
if (found.length === 0) throw new Error('No numbers in ' + areaCode);
const chosen = found[0].phone_number;
await dc('POST', '/phone/public/numbers/rent', { numbers: [chosen], voice_ivr_id: lineId });
return sendVoicemail(to, chosen, audioUrl);
}
The same in Python, with dc() and send_voicemail() from the same section:
def buy_and_send(area_code, line_id, to, audio_url):
found = dc("POST", "/phone/public/numbers/available", {"pattern": area_code, "limit": 5})["data"]
if not found:
raise RuntimeError(f"No numbers in {area_code}")
chosen = found[0]["phone_number"]
dc("POST", "/phone/public/numbers/rent", {"numbers": [chosen], "voice_ivr_id": line_id})
return send_voicemail(to, chosen, audio_url)
Receive calls on your numbers
Sending needs no inventory. Receiving does. When someone calls back the number on their voicemail, the call reaches your carrier first. Drop Cowboy only sees it when both of these are done:
- The number is on a phone line. Sync it, load it or buy it as above. The line's rules decide what happens: forward, queue, an AI agent and so on.
- Your carrier's trunk points at us. Send inbound calls for the number to
sip.dropcowboy.comon port 5060. Connecting Twilio, Telnyx, SignalWire, Plivo, Bandwidth, Vonage, Sinch, Flowroute or Commio (ThinQ) sets this up for you. Acustomtrunk is set up by hand in your carrier's portal.
Texting from a number also needs a registered texting campaign on its line. See Texts.
Who can choose a caller ID
| Account | caller_id on a send |
Phone line |
|---|---|---|
BYOC (force_byoc) |
Used as given, with no number loaded | Optional |
| Any other account | Ignored. The send uses the phone line you name, or your default line. | Required, or a default line |
Other places a caller ID is read follow the same rule. A campaign's
caller_id and a dialer call's caller_id must be a number on your account
unless the account is BYOC. A text reply's caller_id must be an SMS-enabled
number on your account.
Common problems
| Result | What it means | Try this next |
|---|---|---|
403 with byoc_required |
The account isn't on a BYOC plan. | Connect a carrier. See the plan change. |
400 with pool_required or byoc_not_configured |
No carrier is connected. | Connect one, then load, search or buy numbers. |
400 with invalid_phone_numbers |
A value isn't E.164. | Send + and the country code. The message names the refused values. |
400 with phone_line_required |
No phone_line_id and no default voice line. |
Pass phone_line_id, or set a default line. |
conflicts above 0 on the import |
Another account holds those numbers. | They weren't loaded. Check the numbers. |
Send fails with 3014 |
audio_url isn't allowed on this account. |
Connect a carrier, or send a media_id. |
Send fails with 4010 |
No phone_line_id and no default line. |
Pass phone_line_id or caller_id (BYOC), or set a default line. |
403 with caller_id_not_owned or CALLER_ID_NOT_OWNED |
A non-BYOC account named a number it doesn't hold. | Use a number on your account, or connect a carrier. |
| Nothing answers when people call back | The trunk isn't pointed at us, or the number isn't on a phone line. | Do both under Receive calls on your numbers. |
Every code is in Outcomes. For more symptoms, see Troubleshooting.