API reference / Guides
Send a ringless voicemail on a retail plan
This guide sends one ringless voicemail from a phone line on your Drop Cowboy® account, using an audio file you upload, and then shows you how to read the result. It takes about 10 minutes. 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:retailin Node.js,python -m dropcowboy_examples.recipes.send_rvm_retailin Python, ordotnet run -- retailin C#. Each one sends the voicemail and waits for the result.
Before you start
| You need | Details | Where to get it |
|---|---|---|
| A plan | A retail plan, meaning no carrier of your own is connected. On your own carrier, use the BYOC guide. | Quickstart |
| API key scopes | rvm:send, numbers:read to list lines, media:write to add audio |
Authentication |
| A phone line | Drop Cowboy picks the caller ID from the line's numbers, and retail accounts ignore caller_id. |
Step 1 |
| Audio | An MP3 or WAV file at a public URL, 50 MiB or smaller. For spoken text, use text to speech. | Step 2 |
| 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. Choose a phone line
A phone line decides which number the recipient sees and where return calls go. List yours:
List lines with curl
curl https://api-v2.dropcowboy.com/phone/public/lines \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": [
{
"ivr_id": "234ecab5-1811-4c7d-a7a9-8c9ad9ca2e08",
"type": "voice",
"name": "Main line",
"is_default": true
}
],
"meta": { "request_id": "ceeb5da8-09c2-47b5-98dd-f672bb92dc1f" }
}
(The response is trimmed.) A line's ivr_id is the phone_line_id you send.
Leave phone_line_id out to use your default line. With no default line the
send fails with 4010. If you send caller_id as well, it is ignored on a
retail plan. See Phone numbers.
2. Add the audio
Add the file once, then send it by media_id as often as you like. Drop
Cowboy downloads it while you wait, so it must be reachable and download
within 15 seconds.
Add audio with curl
curl -X POST https://api-v2.dropcowboy.com/media/public/media \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Content-Type: application/json" \
-d '{
"name": "October follow-up",
"type": "rvm",
"url": "https://files.example.com/october-follow-up.mp3",
"ext": ".mp3"
}'
{
"data": {
"media_id": "55b9e55e-23f1-4c16-8865-f4b6261ebeea",
"name": "October follow-up",
"type": "rvm",
"media_exists": true,
"approved_at": 1759312800000,
"api_allowed": true
},
"meta": { "request_id": "3028a01c-9f13-4158-a864-2daa70189301" }
}
If the file is on your own server rather than at a public URL, use a
signed upload: create the entry with
signed_upload: true, PUT the bytes with exactly the returned
content_type, then complete it. A 403 from the PUT means a different
Content-Type or an expired URL.
To send spoken text instead, replace media_id with tts_body and voice_id,
as in the text to speech guide.
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.
3. Send the voicemail
Each send needs three things beyond the audio and line:
Idempotency-Key: a UUID you make up for each new send, as a header. If a request times out or answers500or502, send it again with the same key and body, and the voicemail is not sent twice. A new key is a new send. The same key with a different body fails with3027.foreign_id: your own reference, up to 256 characters. Drop Cowboy only echoes it on the callback.callback_url: optional. One unsignedPOSTwith the result, tried once. Use it while you build. A wrong key or secret still gets202, and the3007failure shows up on the callback only.
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: 9d2f6b1e-4a7c-4e35-8b90-1c5a3d7f2e64" \
-H "Content-Type: application/json" \
-d '{
"to": "+13125550142",
"phone_line_id": "234ecab5-1811-4c7d-a7a9-8c9ad9ca2e08",
"media_id": "55b9e55e-23f1-4c16-8865-f4b6261ebeea",
"foreign_id": "7c1e5a93-2d4b-4f68-a0b9-3e6d8c1f5a27",
"callback_url": "https://your-server.example.com/dropcowboy/rvm-result"
}'
The ids are placeholders: use an ivr_id from step 1 and the media_id from
step 2. A media_id copied from this page fails with 3001. This body keeps
working if you later connect your own carrier; see
Test on retail, then go live on BYOC.
{ "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.
Send with Node.js
Node.js 18 or later. Set DC_KEY, DC_SECRET, DC_TO, DC_PHONE_LINE_ID,
DC_MEDIA_ID and DC_CALLBACK_URL. The program stops with a message when one
is unset.
const { randomUUID } = require('crypto');
for (const name of ['DC_KEY', 'DC_SECRET', 'DC_TO', 'DC_PHONE_LINE_ID', 'DC_MEDIA_ID', 'DC_CALLBACK_URL']) {
if (!process.env[name]) throw new Error('Set ' + name + ' first.');
}
const body = {
to: process.env.DC_TO,
phone_line_id: process.env.DC_PHONE_LINE_ID,
media_id: process.env.DC_MEDIA_ID,
foreign_id: randomUUID(),
callback_url: process.env.DC_CALLBACK_URL
};
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_PHONE_LINE_ID", "DC_MEDIA_ID", "DC_CALLBACK_URL"):
if not os.environ.get(name):
raise SystemExit(f"Set {name} first.")
body = {
"to": os.environ["DC_TO"],
"phone_line_id": os.environ["DC_PHONE_LINE_ID"],
"media_id": os.environ["DC_MEDIA_ID"],
"foreign_id": str(uuid.uuid4()),
"callback_url": os.environ["DC_CALLBACK_URL"],
}
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_PHONE_LINE_ID", "DC_MEDIA_ID", "DC_CALLBACK_URL" })
if (Env(name).Length == 0) throw new InvalidOperationException($"Set {name} first.");
var body = new JsonObject
{
["to"] = Env("DC_TO"),
["phone_line_id"] = Env("DC_PHONE_LINE_ID"),
["media_id"] = Env("DC_MEDIA_ID"),
["foreign_id"] = Guid.NewGuid().ToString(),
["callback_url"] = Env("DC_CALLBACK_URL")
};
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. A voicemail
outside calling hours is held and retried, and fails if it still cannot go
out after 3 days.
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.
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. |
4013 |
Too many attempts | Wait for the frequency window to pass. See Contact frequency limit. |
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.
If it does not work
| You see | Likely cause | Fix |
|---|---|---|
400 on the request |
The body is not a JSON object, or a header has characters that are not printable ASCII | Fix the request |
429 |
You are sending too fast | Back off, retry with the same key. See Rate limits. |
3007 on the callback |
Wrong key or secret, or a token without rvm:send |
Fix the credentials |
3013 |
phone_line_id is not valid or not yours |
Use an ivr_id from step 1 |
3001 |
The media_id isn't on your account, for example one copied from this page |
Send the media_id from step 2 |
3014 |
audio_url isn't enabled for your account, or no audio was given |
Send a media_id, or text to speech |
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 |
403 on the upload PUT |
The Content-Type differs from the returned one, or the URL expired |
See Troubleshooting |
202, then no result |
Your callback route answered 404 or another non-2xx |
Accept POST, answer 200. Check Settings > API Logs. |
4010 |
No phone_line_id and no default line |
Pass one, or set a default line |
3027 |
The Idempotency-Key was used with a different body |
Use a new key per new send |
4013 while testing |
You hit the frequency limit | Add test numbers (numbers you own) on the Dialing rules page. Calling hours still apply. |
More symptoms are in Troubleshooting.
Production checklist
- Generate an
Idempotency-Keyper send, and store it with your own record so a retry reuses it. - Store
foreign_idand match results to it. Do not parsemessage_id. - 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. - Handle
429with a pause and the sameIdempotency-Key.
Next steps
- Receive the result of a send
- Send a ringless voicemail with text to speech
- Send a ringless voicemail from your own caller ID (BYOC)
- Test on retail, then go live on BYOC
- Ringless voicemail and Outcomes
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.
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.