API reference / Guides
Send a ringless voicemail with text to speech
By the end of this guide you'll have sent a ringless voicemail that speaks text you wrote, in a voice you picked, with no audio file to record or upload. It takes about ten minutes.
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:ttsin Node.js,python -m dropcowboy_examples.recipes.send_rvm_ttsin Python, ordotnet run -- ttsin C#. Each one sends the voicemail and waits for the result.
Before you start
| You need | Details |
|---|---|
| An account | Either kind works. A standard account sends from a phone line. A bring-your-own-carrier (BYOC) account can send from a number you are entitled to use as your caller ID, with caller_id. See Bring your own carrier. |
| An API key | In DC_KEY and DC_SECRET, with scopes media:read (list voices), rvm:send, voice:send (only for the preview) and numbers:read (only to list phone lines). See Authentication. |
| A sender | A phone line on your account, or on BYOC your carrier's number in E.164. |
| A recipient | Someone who agreed to hear from you. For a test, list a number you own as a test number. Calling hours still apply. See Test numbers. |
| A results URL | A public HTTPS address for callback_url. |
1. Pick a voice
A voice is the speaker for your text. The list holds your own cloned and
designed voices plus the platform catalog. Only a voice whose status is
ready can speak.
curl "https://api-v2.dropcowboy.com/voice/public/voices?limit=20" \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": {
"voices": [
{
"voice_id": "7d2a9e4b-1c6f-4b3a-8e5d-2f9c7a1b4e63",
"team_id": null,
"name": "Jordan",
"type": null,
"gender": "female",
"status": "ready",
"pro_voice": true,
"created_at": 1774041600000,
"failed_at": null
}
],
"total": 0
},
"meta": { "request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24" }
}
Keep the voice_id of a ready voice. Catalog voices have a null team_id,
and total counts only your own voices. Add include_urls=true to get a sample
url for each voice to listen to. To make a voice of your own, see
Voices, speech and transcription.
2. Write the text
Put the words in tts_body. Keep these limits in mind:
- It can be up to 1,200 characters, counted after merge fields are filled in. A
longer text fails with
3021. - Write numbers and abbreviations the way you want them said. Read the text aloud once, or preview it in step 3.
- Merge fields such as
{{contact.first_name|there}}fill in only when you send to acontact_id. A send totohas no contact to merge. Give every token a default after the|. See Merge fields.
A short message that works on its own:
Hi, this is Example Dental. Your appointment is tomorrow at ten. Call us back
if you need to change it.
Say who you are at the start, and give a way to reach you.
3. Preview the audio (optional)
A preview turns your text into a file you can play, so you can check the voice and the wording before anything goes to a recipient. It's separate from the send.
curl https://api-v2.dropcowboy.com/voice/public/tts/synthesize \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Content-Type: application/json" \
-d '{
"voice_id": "7d2a9e4b-1c6f-4b3a-8e5d-2f9c7a1b4e63",
"text": "Hi, this is Example Dental. Your appointment is tomorrow at ten."
}'
{
"data": {
"audio_url": "https://media.example.com/tts/1b7e3c9a.mp3",
"expires_at": 1774045200000,
"tts_characters": 64,
"content_type": "audio/mpeg"
},
"meta": { "request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24" }
}
Open audio_url and listen. It works for about an hour, until expires_at. A
preview is billed per character, and tts_characters reports the count, so
preview a script once, not in a loop. Don't send the preview's audio_url as
your audio. Send the text itself, as in step 5.
4. Choose the sender
On a standard account, the voicemail goes out from a phone line, and the
recipient sees one of its numbers. List your lines and keep the ivr_id of the
one to use. Its value is the phone_line_id.
curl "https://api-v2.dropcowboy.com/phone/public/lines?type=voice" \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
The answer is { "data": [ ... ] }, one object per line, each with an
ivr_id, a name and is_default. If you leave phone_line_id out, the send
uses your default line. With no default line, the send fails with 4010.
On a BYOC account, use your carrier's number as caller_id instead, in E.164.
It shows as the caller ID exactly as given. Use only a number you are entitled
to use as your caller ID. Always identify your business location truthfully
when asked by recipients. If you send a phone_line_id too, the line wins and
caller_id is ignored. To have the platform choose among
several of your numbers, see
Choose the caller ID automatically with phone lines (BYOC).
5. Send
Send tts_body and voice_id together, and no other audio field. Don't add
media_id or audio_url. To send a recorded file instead, add it with a
signed upload and send its media_id.
On a standard account:
curl -X POST https://api-v2.dropcowboy.com/rvm \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Idempotency-Key: 9b4e7c1a-2d8f-4a63-b5e0-3f7a1c9d6e28" \
-H "Content-Type: application/json" \
-d '{
"to": "+17735550150",
"phone_line_id": "234ecab5-1811-4c7d-a7a9-8c9ad9ca2e08",
"voice_id": "7d2a9e4b-1c6f-4b3a-8e5d-2f9c7a1b4e63",
"tts_body": "Hi, this is Example Dental. Your appointment is tomorrow at ten.",
"foreign_id": "3b8f6d12-a4c7-4e90-9d15-7c2e5a1b8f43",
"callback_url": "https://your-server.example.com/dropcowboy/rvm-result"
}'
The ids are placeholders: use the ivr_id from step 4 and the voice_id from
step 1. This body keeps working after you connect your own carrier, once your
numbers are on the line; see
Test on retail, then go live on BYOC.
On a BYOC account, replace phone_line_id with the number to show:
curl -X POST https://api-v2.dropcowboy.com/rvm \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Idempotency-Key: 4c8a1e6d-7b2f-4d93-a5e1-2f6b9c3d7a40" \
-H "Content-Type: application/json" \
-d '{
"to": "+17735550150",
"caller_id": "+13125550142",
"voice_id": "7d2a9e4b-1c6f-4b3a-8e5d-2f9c7a1b4e63",
"tts_body": "Hi, this is Example Dental. Your appointment is tomorrow at ten.",
"callback_url": "https://your-server.example.com/dropcowboy/rvm-result"
}'
Either one answers:
{ "status": "queued", "message_id": "8e4a2c6f-9d1b-4f7e-b3a5-6c9e1f4d2b78" }
| Field | What to know |
|---|---|
tts_body, voice_id |
Send both, or neither. tts_body without voice_id fails with 3002. voice_id without tts_body fails with 3015. |
contact_id |
Use it instead of to to fill merge fields from the contact. If you send both, to is used. |
foreign_id, callback_url |
Your reference, and where the result is posted. The post echoes foreign_id. |
Idempotency-Key |
A new UUID for each send. Reuse it only to retry the same send. See Retry safely. |
A 202 means the send was queued. Problems with the text or the voice show up
afterward, in the result, so read it.
6. Read the result
The outcome arrives on your callback_url and as a signed contact.rvm.status
webhook, with a reason_code. A reason_code of 0 means the voicemail was
left in the contact's mailbox. Every other code is in Outcomes.
To subscribe and verify signatures, see
Receive the results of your sends.
Run the whole flow in code
Each script lists your voices, uses DC_VOICE_ID or else the first ready
voice, and sends. Set these first:
| Variable | Value |
|---|---|
DC_KEY, DC_SECRET |
Your API key |
DC_TO |
The recipient, in E.164 |
DC_TEXT |
The text to speak |
DC_CALLBACK_URL |
Your results URL |
DC_VOICE_ID |
Optional. A ready voice. |
DC_PHONE_LINE_ID |
Optional. Standard accounts: the line to send from. |
DC_CALLER_ID |
Optional. BYOC accounts: a number you are entitled to use as your caller ID, in E.164. |
Node.js
Node.js 18 or later. Save it as send.mjs and run node send.mjs.
import { randomUUID } from 'node:crypto';
async function dc(method, path, body, extraHeaders) {
const res = await fetch('https://api-v2.dropcowboy.com' + path, {
method,
headers: Object.assign({
'x-key': process.env.DC_KEY,
'x-secret': process.env.DC_SECRET,
'Content-Type': 'application/json'
}, extraHeaders),
body: body ? JSON.stringify(body) : undefined
});
const json = await res.json();
if (!res.ok) throw new Error(res.status + ' ' + (json.detail || json.title));
return json;
}
if (!process.env.DC_CALLBACK_URL) throw new Error('Set DC_CALLBACK_URL first.');
let voiceId = process.env.DC_VOICE_ID;
if (!voiceId) {
const list = await dc('GET', '/voice/public/voices?limit=100');
const voice = list.data.voices.find((v) => v.status === 'ready');
if (!voice) throw new Error('No voice is ready.');
voiceId = voice.voice_id;
console.log('Voice', voice.name);
}
const body = {
to: process.env.DC_TO,
voice_id: voiceId,
tts_body: process.env.DC_TEXT,
callback_url: process.env.DC_CALLBACK_URL
};
if (process.env.DC_PHONE_LINE_ID) body.phone_line_id = process.env.DC_PHONE_LINE_ID;
if (process.env.DC_CALLER_ID) body.caller_id = process.env.DC_CALLER_ID;
const queued = await dc('POST', '/rvm', body, { 'Idempotency-Key': randomUUID() });
console.log('Queued', queued.message_id);
Python
Python 3.9 or later, with requests.
import os
import uuid
import requests
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, "https://api-v2.dropcowboy.com" + 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')}")
return data
if not os.environ.get("DC_CALLBACK_URL"):
raise RuntimeError("Set DC_CALLBACK_URL first.")
voice_id = os.environ.get("DC_VOICE_ID")
if not voice_id:
voices = dc("GET", "/voice/public/voices?limit=100")["data"]["voices"]
ready = [v for v in voices if v["status"] == "ready"]
if not ready:
raise RuntimeError("No voice is ready.")
voice_id = ready[0]["voice_id"]
print("Voice", ready[0]["name"])
body = {
"to": os.environ["DC_TO"],
"voice_id": voice_id,
"tts_body": os.environ["DC_TEXT"],
"callback_url": os.environ["DC_CALLBACK_URL"],
}
if os.environ.get("DC_PHONE_LINE_ID"):
body["phone_line_id"] = os.environ["DC_PHONE_LINE_ID"]
if os.environ.get("DC_CALLER_ID"):
body["caller_id"] = os.environ["DC_CALLER_ID"]
queued = dc("POST", "/rvm", body, {"Idempotency-Key": str(uuid.uuid4())})
print("Queued", queued["message_id"])
C#
.NET 8 or later. Create a console project and replace Program.cs.
using System.Text;
using System.Text.Json.Nodes;
string Env(string name) => Environment.GetEnvironmentVariable(name) ?? "";
var http = new HttpClient { BaseAddress = new Uri("https://api-v2.dropcowboy.com") };
http.DefaultRequestHeaders.Add("x-key", Env("DC_KEY"));
http.DefaultRequestHeaders.Add("x-secret", Env("DC_SECRET"));
async Task<JsonNode> Dc(HttpMethod method, string path, JsonNode? body = null, string? key = null)
{
using var request = new HttpRequestMessage(method, path);
if (body != null) request.Content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
if (key != null) request.Headers.Add("Idempotency-Key", key);
using var response = await http.SendAsync(request);
var json = JsonNode.Parse(await response.Content.ReadAsStringAsync())!;
if (!response.IsSuccessStatusCode) throw new Exception($"{(int)response.StatusCode} {json["detail"] ?? json["title"]}");
return json;
}
if (Env("DC_CALLBACK_URL") == "") throw new InvalidOperationException("Set DC_CALLBACK_URL first.");
var voiceId = Env("DC_VOICE_ID");
if (voiceId == "")
{
var voices = (await Dc(HttpMethod.Get, "/voice/public/voices?limit=100"))["data"]!["voices"]!.AsArray();
var voice = voices.FirstOrDefault(v => (string?)v!["status"] == "ready");
if (voice == null) throw new Exception("No voice is ready.");
voiceId = (string)voice["voice_id"]!;
Console.WriteLine("Voice " + voice["name"]);
}
var body = new JsonObject
{
["to"] = Env("DC_TO"),
["voice_id"] = voiceId,
["tts_body"] = Env("DC_TEXT"),
["callback_url"] = Env("DC_CALLBACK_URL")
};
if (Env("DC_PHONE_LINE_ID") != "") body["phone_line_id"] = Env("DC_PHONE_LINE_ID");
if (Env("DC_CALLER_ID") != "") body["caller_id"] = Env("DC_CALLER_ID");
var queued = await Dc(HttpMethod.Post, "/rvm", body, Guid.NewGuid().ToString());
Console.WriteLine("Queued " + queued["message_id"]);
Tips
- Preview once, then send. A preview is billed per character.
- Keep the text short and spoken. Long text is harder to follow on a voicemail.
- Keep the same
voice_idacross a campaign so recipients hear one speaker. - Send a
foreign_idso the result matches your data.
If it does not work
| Symptom | Likely cause | What to do |
|---|---|---|
Result 3002 |
tts_body was sent without voice_id, or the voice_id isn't valid. |
Send both. Take the voice_id from step 1. |
Result 3015 |
voice_id was sent without tts_body. |
Send both, or use media_id and no voice_id. |
Result 3021 |
tts_body is over 1,200 characters after merge fields are filled in. |
Shorten the text, or shorten the defaults. |
Result 3000 |
Your account has no funds. | Add funds. See Payment required. |
Send fails with 4010 |
No phone_line_id, and no default phone line. |
Send a phone_line_id, or set a default line. |
Result 3040 |
Your account is in testing mode, and the number isn't one of your test numbers. | Add it under test numbers on the Dialing rules page. |
202, then no result |
Your callback route answered 404 or another non-2xx, which isn't retried. |
Accept POST and answer 200. Check Settings > API Logs. |
Preview answers 409 |
The voice is processing or failed. |
Wait until its status is ready, or pick another voice. |
Preview answers 402 |
No funds, or the voice is pending_payment. |
Add funds, or complete checkout for the voice in the dashboard. |
Preview answers 404 |
The voice doesn't exist, was deleted, or isn't yours. | Take a voice_id from step 1. |
Preview answers 400 |
voice_id or text is missing. |
Send both. |
Result 4001 or 4002 |
Their mailbox can't take a message. | Retrying won't help. See Act on the result. |
Result 0, but nothing seen |
Carrier handling can take up to 24 hours, and some carriers filter voicemails as spam. | Wait before you retry. |
Every code is in Outcomes.
Production checklist
- Every recipient agreed to hear from you. See Consent.
- You've listened to the voice and the wording in a preview.
- Every send has a new
Idempotency-Key, and a retry reuses it. - Merge fields have defaults, and the text stays under 1,200 characters.
- You act on results from the webhook, not on the
202.
Next steps
- Receive the results of your sends
- Send a ringless voicemail from a phone line
- Send a ringless voicemail from your own carrier
- Choose the caller ID automatically with phone lines (BYOC)
- Test on retail, then go live on BYOC
- Voices, speech and transcription
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.