API reference / Guides
Send a ringless voicemail from your own caller ID (BYOC)
This guide sends one ringless voicemail over a carrier you already have, from a number you hold there, using an audio file you host. It takes about 10 minutes once your carrier is connected. At the end you have a voicemail queued to a number you own and a way to match its result to your own record.
Send only to people who agreed to hear from you. See Consent. Test with numbers you own.
A
202means accepted, not delivered. The result of each voicemail arrives on thecontact.rvm.statuswebhook.
Run it from GitHub. This guide is a working recipe in the Drop Cowboy® examples:
npm run send:byocin Node.js,python -m dropcowboy_examples.recipes.send_rvm_byocin Python, ordotnet run -- byocin C#. Each one sends the voicemail and waits for the result.
Before you start
| You need | Details | Where to get it |
|---|---|---|
| A plan | A BYOC account with a carrier connected. Step 1 checks it. | Bring your own carrier |
| API key scopes | rvm:send, and balance:read for the connection check |
Authentication |
| A caller ID | A number you hold at your carrier and are entitled to use as your caller ID, in E.164 form. Your carrier has to accept it. It does not need to exist in Drop Cowboy. | Your carrier's portal |
| Audio | An MP3 or WAV file, up to 50 MB, at a public URL that stays up until the send finishes. Sending it as audio_url must be enabled by support; otherwise send a media_id. |
Your own hosting, or Media |
| A recipient | A number in E.164 form whose owner agreed to hear from you. Test with your own number. | Consent |
| Somewhere for results | A public HTTPS URL for a callback or webhook | Receive results |
1. Check the carrier connection
Check with curl
curl https://api-v2.dropcowboy.com/integration/public/byoc \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": {
"connected": true,
"providers": [
{
"provider": "twilio",
"integration_type": "twilio",
"integration_id": "2f8c4a6e-9b3d-4e1f-a7c5-6d2b9e4f1a38",
"enabled": true,
"default": true,
"pool_id": "6e1f3a5c-9b2d-4c8e-a7f1-3d5b9c2e4a61"
}
],
"pool_id": "6e1f3a5c-9b2d-4c8e-a7f1-3d5b9c2e4a61"
},
"meta": { "request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24" }
}
You need connected: true and a provider with enabled: true. If not,
connect first. Connecting can move an account off a retail plan and release
rented numbers, so read
Connect your carrier before
you call it. A send has no route field, because the route is part of the
connection.
2. Host the audio
Drop Cowboy downloads the file from audio_url when the voicemail is sent,
not when you call POST /rvm. A voicemail outside calling hours is held and
retried for up to 3 days, so keep the file at that address until the result
arrives.
audio_url is available on BYOC plans only and must be enabled by support.
Contact support to enable it. If you're testing on a retail account before
connecting your carrier, support can enable it for testing; while testing,
voice sends go only to your test numbers. Otherwise, upload your file and send
media_id, or use text to speech. A signed upload
takes a file from your own server: create the entry, PUT the bytes with
exactly the returned content_type, then complete it.
3. Send the voicemail
The fields that matter:
| Field | Meaning |
|---|---|
caller_id |
A number at your carrier that you are entitled to use as your caller ID. Shown exactly as given. |
audio_url |
Public MP3 or WAV URL. Or send media_id, or tts_body with voice_id. |
byoc.sti_orig_id |
Optional. The origination ID your carrier gave you for STIR/SHAKEN signing, as a UUID. Anything else fails with 3017. |
byoc.sti_attestation |
Optional. The level your carrier assigned for this number: A, B or C. Don't send a higher level than your carrier gave you. Anything else fails with 3018. |
Idempotency-Key header |
A UUID you make up for each new send. If a request times out or answers 500 or 502, retry with the same key and body and the voicemail is not sent twice. A new key is a new send. |
foreign_id |
Your own reference, up to 256 characters, echoed on the callback only. |
callback_url |
Optional. One unsigned POST with the result, tried once. A wrong key still gets 202, and 3007 shows up here only. |
To show this exact caller_id, don't send phone_line_id. When both are
present the phone line wins and caller_id is ignored. If you tested on retail
with phone_line_id and no caller_id, that body still works once your
numbers are on the line; see
Test on retail, then go live on BYOC. To have Drop Cowboy pick the caller ID for each
recipient from several of your numbers, put them on a phone line. See
Choose the caller ID automatically with phone lines (BYOC).
Send with curl
curl -X POST https://api-v2.dropcowboy.com/rvm \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Idempotency-Key: 4a8e1c7d-2f5b-4d93-9e60-7b3c1a5f8d24" \
-H "Content-Type: application/json" \
-d '{
"to": "+13125550142",
"caller_id": "+13125550100",
"audio_url": "https://your-server.example.com/audio/october-follow-up.mp3",
"byoc": {
"sti_orig_id": "c4a7e1d2-9b3f-4a68-8d05-2e7f6b1a9c34",
"sti_attestation": "A"
},
"foreign_id": "7c1e5a93-2d4b-4f68-a0b9-3e6d8c1f5a27",
"callback_url": "https://your-server.example.com/dropcowboy/rvm-result"
}'
The values are placeholders. Use your own caller ID, audio address and
sti_orig_id, and the attestation level your carrier gave you. The value A
here is an example.
{ "status": "queued", "message_id": "8e4a2c6f-9d1b-4f7e-b3a5-6c9e1f4d2b78" }
The answer is flat, without a data envelope. message_id is a receipt and
does not appear on later results. Leave byoc out if you do not sign calls.
Send with Node.js
Node.js 18 or later. Set DC_KEY, DC_SECRET, DC_TO, DC_CALLER_ID,
DC_AUDIO_URL and DC_CALLBACK_URL. The program stops with a message when one
is unset. DC_STI_ORIG_ID and DC_STI_ATTESTATION are optional.
const { randomUUID } = require('crypto');
for (const name of ['DC_KEY', 'DC_SECRET', 'DC_TO', 'DC_CALLER_ID', 'DC_AUDIO_URL', 'DC_CALLBACK_URL']) {
if (!process.env[name]) throw new Error('Set ' + name + ' first.');
}
const body = {
to: process.env.DC_TO,
caller_id: process.env.DC_CALLER_ID,
audio_url: process.env.DC_AUDIO_URL,
foreign_id: randomUUID(),
callback_url: process.env.DC_CALLBACK_URL
};
const byoc = {};
if (process.env.DC_STI_ORIG_ID) byoc.sti_orig_id = process.env.DC_STI_ORIG_ID;
if (process.env.DC_STI_ATTESTATION) byoc.sti_attestation = process.env.DC_STI_ATTESTATION;
if (Object.keys(byoc).length > 0) body.byoc = byoc;
async function main() {
const res = await fetch('https://api-v2.dropcowboy.com/rvm', {
method: 'POST',
headers: {
'x-key': process.env.DC_KEY,
'x-secret': process.env.DC_SECRET,
'Content-Type': 'application/json',
'Idempotency-Key': randomUUID()
},
body: JSON.stringify(body)
});
const json = await res.json();
if (res.status !== 202) {
console.error('Not accepted:', res.status, JSON.stringify(json));
process.exit(1);
}
console.log('Queued', json.message_id, 'foreign_id', body.foreign_id);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Send with Python
Python 3.9 or later with pip install requests. Same environment variables.
import os
import uuid
import requests
for name in ("DC_KEY", "DC_SECRET", "DC_TO", "DC_CALLER_ID", "DC_AUDIO_URL", "DC_CALLBACK_URL"):
if not os.environ.get(name):
raise SystemExit(f"Set {name} first.")
body = {
"to": os.environ["DC_TO"],
"caller_id": os.environ["DC_CALLER_ID"],
"audio_url": os.environ["DC_AUDIO_URL"],
"foreign_id": str(uuid.uuid4()),
"callback_url": os.environ["DC_CALLBACK_URL"],
}
byoc = {}
if os.environ.get("DC_STI_ORIG_ID"):
byoc["sti_orig_id"] = os.environ["DC_STI_ORIG_ID"]
if os.environ.get("DC_STI_ATTESTATION"):
byoc["sti_attestation"] = os.environ["DC_STI_ATTESTATION"]
if byoc:
body["byoc"] = byoc
res = requests.post(
"https://api-v2.dropcowboy.com/rvm",
headers={
"x-key": os.environ["DC_KEY"],
"x-secret": os.environ["DC_SECRET"],
"Idempotency-Key": str(uuid.uuid4()),
},
json=body,
timeout=30,
)
if res.status_code != 202:
raise SystemExit(f"Not accepted: {res.status_code} {res.text}")
print("Queued", res.json()["message_id"], "foreign_id", body["foreign_id"])
Send with C#
.NET 8. Create a project with dotnet new console, replace Program.cs, and
set the same environment variables.
using System.Net.Http.Json;
using System.Text.Json.Nodes;
foreach (var name in new[] { "DC_KEY", "DC_SECRET", "DC_TO", "DC_CALLER_ID", "DC_AUDIO_URL", "DC_CALLBACK_URL" })
if (Env(name).Length == 0) throw new InvalidOperationException($"Set {name} first.");
var body = new JsonObject
{
["to"] = Env("DC_TO"),
["caller_id"] = Env("DC_CALLER_ID"),
["audio_url"] = Env("DC_AUDIO_URL"),
["foreign_id"] = Guid.NewGuid().ToString(),
["callback_url"] = Env("DC_CALLBACK_URL")
};
var byoc = new JsonObject();
if (Env("DC_STI_ORIG_ID").Length > 0) byoc["sti_orig_id"] = Env("DC_STI_ORIG_ID");
if (Env("DC_STI_ATTESTATION").Length > 0) byoc["sti_attestation"] = Env("DC_STI_ATTESTATION");
if (byoc.Count > 0) body["byoc"] = byoc;
using var http = new HttpClient { BaseAddress = new Uri("https://api-v2.dropcowboy.com"), Timeout = TimeSpan.FromSeconds(30) };
using var request = new HttpRequestMessage(HttpMethod.Post, "/rvm") { Content = JsonContent.Create(body) };
request.Headers.Add("x-key", Env("DC_KEY"));
request.Headers.Add("x-secret", Env("DC_SECRET"));
request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
using var response = await http.SendAsync(request);
var text = await response.Content.ReadAsStringAsync();
if ((int)response.StatusCode != 202)
{
Console.Error.WriteLine($"Not accepted: {(int)response.StatusCode} {text}");
return 1;
}
Console.WriteLine($"Queued {JsonNode.Parse(text)?["message_id"]} foreign_id {body["foreign_id"]}");
return 0;
static string Env(string name) => Environment.GetEnvironmentVariable(name) ?? "";
Read the result
A 202 means Drop Cowboy received the request. Calling hours, the contact
frequency limit and your balance are checked afterwards, and so is consent when
your account requires it. You remain responsible for having consent.
The result arrives on the callback_url and on the contact.rvm.status
webhook. Receive the result of a send builds the
receiver. Match a callback to your record on foreign_id. Status webhooks do
not carry it, so match those on to, or on contact_id. If you signed the
call with a certificate you uploaded, the callback and the status webhook also
carry sti_identity, sti_attestation, sti_orig_id and sip_call_id. See
Results of your sends.
reason_code |
Meaning | What to do |
|---|---|---|
0 |
Voicemail left | Record it. See Proof of delivery. |
4001, 4002 |
Their voicemail is not set up, or is full | Another voicemail will not help. |
4005, 4006 |
No answer, or busy | Try again later, inside calling hours. |
4016, 4017, 6005, 6011 |
Do-not-contact, known litigator, opted out, or no consent | Mark the contact and stop. |
3000 to 3999 |
A problem with your request or account | Fix the setup. |
Every code is in Outcomes. A person who calls back the number on their voicemail reaches your carrier first. To take those calls in Drop Cowboy, see Use your own numbers on BYOC.
If it does not work
| You see | Likely cause | Fix |
|---|---|---|
connected: false in step 1 |
No carrier connected | Connect one. See Bring your own carrier. |
3014 |
audio_url isn't enabled for your account, or no audio was given |
Contact support to enable it, or send a media_id |
3040 |
Your account is in testing mode and the number isn't a test number | Add it under test numbers on the Dialing rules page, or finish connecting your carrier |
3001 |
The audio couldn't be downloaded or isn't MP3 or WAV, or the media_id isn't on your account |
Check the URL, or send your own media_id |
202, then no result |
Your callback route answered 404 or another non-2xx |
Accept POST, answer 200. Check Settings > API Logs. |
3016 |
The send has byoc, but BYOC is not on for the account |
Remove byoc, or connect a carrier |
3017 or 3018 |
sti_orig_id is not a UUID, or sti_attestation is not A, B or C |
Fix the value |
3007 on the callback |
Wrong key or secret, or a token without rvm:send |
Fix the credentials |
4010 |
No caller_id and no phone line |
Pass caller_id |
3027 |
The Idempotency-Key was used with a different body |
Use a new key per new send |
3019 |
callback_url is not a URL |
Pass a full https:// address |
More symptoms are in Troubleshooting.
Production checklist
- Generate an
Idempotency-Keyper send and store it with your own record so a retry reuses it. - Keep the audio file at its URL until the result arrives, because it is fetched when the voicemail goes out.
- Read results from the signed webhook, verify the raw bytes, and
deduplicate on
event_id. - Keep
DC_SECRETand webhook signing secrets in a secrets manager. - Record each recipient's consent before the send, and stop on
4016,4017,6005and6011. - Use only a caller ID you are entitled to use, and check that your carrier accepts it. Always identify your business location truthfully when asked by recipients.
Next steps
- Receive the result of a send
- Choose the caller ID automatically with phone lines (BYOC)
- Send a ringless voicemail with text to speech
- Test on retail, then go live on BYOC
- Bring your own carrier and Use your own numbers on BYOC
Ringless voicemail technology delivers messages directly to voicemail inboxes. Delivery success depends on carrier compatibility, device type, and recipient settings. While designed for voicemail delivery, technical factors may affect performance. Drop Cowboy does not guarantee delivery rates or specific outcomes.
BYOC customers connect their own carrier accounts (Twilio, Bandwidth, etc.) to the Drop Cowboy platform. Customers are responsible for their carrier relationship, billing, and compliance with carrier terms of service. Drop Cowboy does not mark up or bill for carrier services. Message delivery and carrier connectivity depend on the customer's carrier account status and settings.
While Drop Cowboy provides tools to support compliance efforts, customers remain solely responsible for obtaining proper consent, maintaining opt-out lists, and complying with all federal and state telemarketing regulations. Consult with your legal counsel to ensure your specific use case and consent mechanisms comply with applicable laws.
This information is for educational purposes only and does not constitute legal advice. Regulations vary by jurisdiction and use case. Always consult with qualified legal counsel to ensure your specific practices comply with applicable federal and state laws.