API reference · Send outreach
Messaging
Four routes send one message to one recipient per request. Use them for event-driven sends: an order ships, a form is submitted, an appointment is booked. To send to a whole list, use campaigns. For email, see email. To answer a contact who wrote back, see Conversations and replies.
| Route | Sends |
|---|---|
POST /rvm |
Ringless voicemail |
POST /sms |
SMS, MMS when you add media, or RCS when you pass template_id |
POST /voice-broadcast |
A Press-1 call: one message for a person, another for a machine |
POST /ai-broadcast |
An outbound call handled by one of your published AI agents |
Base URL: https://api-v2.dropcowboy.com.
Delivery expectations. A
202means the send was accepted, not delivered. Results arrive on webhooks:contact.rvm.statusfor voicemail, voice broadcast and AI calls, andcontact.sms.statusfor texts.
- Calling hours. Voice sends (ringless voicemail, voice broadcast, AI calls) outside the contact's allowed hours are held and retried for up to 3 days, then fail with reason
tcpa_expired(reason_code3999). Texts outside the window (default 8am to 9pm in the contact's time zone, with state overrides) fail withreason_code4011and are not retried, so resend them inside the window.- Contact frequency limit. Each team limits attempts to one phone number in a rolling window, by default 3 attempts in 3 days. Campaign sends and dialer calls count together, and a send over the limit fails with
reason_code4013. Read your team's limit atGET /register/public/account(delivery_limits.frequency).- Test numbers. Phone numbers you list as test numbers on the Dialing rules page skip the frequency limit, so you can send to yourself while you build. Calling hours still apply unless you turn that off on the same page.
Every
reason_codeis explained in Outcomes. Subscribe to results in Webhooks. Retries, delivery rules andcallback_urlare in Send lifecycle.
How a send works
These routes queue the request and answer 202 straight away. Your key and
secret, the recipient, consent, calling hours, your contact frequency limit
and your balance are checked after the 202, and the outcome arrives on
your callback_url and on a signed webhook. So 202 means queued, not
delivered. Send an Idempotency-Key header to make retries safe.
Send lifecycle covers the rest in one place: the responses before queueing, credentials, safe retries, the delivery rules and the results.
Choosing a recipient
Give one of:
to: a phone number in E.164, for example+13125550142. Aliasphone_number.contact_id: a contact UUID fromGET /contact/public/contacts. The number is picked from the contact withphone_selector.
If you send both, to is used and contact_id is ignored.
phone_selector |
Picks |
|---|---|
primary (default) |
The first of main, mobile, home, office, other phone |
any |
The first number the contact has |
main_phone, mobile_phone, home_phone, office_phone, other_phone |
Only that number. If it is empty, the send fails. |
These problems fail the send after the 202, with this reason on your
callback_url:
| Reason | Meaning |
|---|---|
Must be E.164 format |
to is not an E.164 number |
Invalid contact_id |
contact_id is not a UUID |
Invalid phone_selector |
Not one of the values above |
No contact |
No such contact on your team, or it was deleted |
Contact on DNC |
The contact is on your do-not-contact list |
No phone number for contact |
The selected number is empty |
Contact lookup failed |
A temporary error; retry |
Fields every route accepts
| Field | Purpose |
|---|---|
foreign_id |
Your reference, echoed on callback_url. Status webhooks do not carry it. |
callback_url |
Receives one unsigned POST with the outcome (see callback_url rules). Must be a publicly reachable http(s) URL. |
postal_code |
Recipient postal code; improves the time-zone guess for calling hours |
brand_id |
Registered brand to send under, when your account requires one. See GET /automation/public/brands. |
max_attempts |
Make the contact frequency limit stricter for this send: fewer attempts than your account allows |
max_attempt_window_ms |
Make it stricter with a longer window, in milliseconds. Over 30 days is capped at 30 days. |
Send a ringless voicemail
POST /rvm. Give the audio as exactly one of:
media_id: an uploaded or recorded file (GET /media/public/media);tts_bodyplusvoice_id: text to speech (GET /voice/public/voices);audio_url: a public mp3 or wav URL. BYOC accounts only; other accounts get "Not allowed audio_url".
| Field | Purpose |
|---|---|
phone_line_id |
Phone line whose numbers are the caller ID. Return calls and texts follow that line. Preferred. See GET /phone/public/lines. |
caller_id |
Your number in E.164, used when there is no phone_line_id. Unless you use BYOC, recipients see a number from the shared pool and return calls are forwarded to caller_id. |
mobile_only |
Skip numbers that are not mobile |
filter_spam_blockers |
Only bill for drops that were not blocked as spam |
privacy |
Caller ID privacy: off (default), signal or full |
curl -X POST https://api-v2.dropcowboy.com/rvm \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"phone_selector": "mobile_phone",
"phone_line_id": "e2b6f9a3-5c1d-4e8b-a4f7-9c3e1b5d7a28",
"voice_id": "7d2a9e4b-1c6f-4b3a-8e5d-2f9c7a1b4e63",
"tts_body": "Hi {{contact.first_name|there}}, your order is ready for pickup.",
"foreign_id": "order-1042"
}'
Ringless voicemail results can carry a proof_of_delivery_url that plays the
drop. It is rolling out and may not appear on your account yet. See
Proof of delivery.
Send a text
POST /sms. Give the content as body (plain text; aliases message,
sms_body) or template_id (an RCS template built in the dashboard; see
GET /template/public/templates?type=rcs). With a template the message goes
as RCS, and the template's fallback text is sent where RCS is not available.
If you send both, the template wins.
Send from a phone_line_id whose line has an approved texting campaign. That
line picks the sender number and the registered campaign. Without a phone
line the message goes out from the shared pool. A missing texting
registration fails with 6009 (Unregistered Brand); register your brand and
texting campaign in the dashboard first. A phone_line_id whose line has no
texting campaign fails with 3029 (Phone Line Has No SMS Campaign).
To send images or audio as MMS, add media_urls or media_ids; see
MMS.
{
"contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"phone_line_id": "e2b6f9a3-5c1d-4e8b-a4f7-9c3e1b5d7a28",
"template_id": "3a7d1f9c-5e2b-4c8a-9f6d-1b4e7a2c5d93"
}
MMS
POST /sms and POST /phone/public/sms/reply (Send Reply)
both accept media. Add it with either field, or both:
| Field | Takes |
|---|---|
media_urls |
https URLs of files you host |
media_ids |
media_id values from the Media API |
A send carries at most 10 files across the two fields. The text body
(body on POST /sms, sms_body on replies) becomes the caption and is
optional when media is present.
{
"contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"phone_line_id": "e2b6f9a3-5c1d-4e8b-a4f7-9c3e1b5d7a28",
"body": "Your order is ready. Show this at pickup.",
"media_urls": ["https://files.example.com/pickup-code.png"]
}
Upload, then send
The Media API stores audio only (MP3 or WAV), so media_ids can carry audio
only. Send images with media_urls.
- Create the media entry, either from a URL or with a signed upload, and complete it; see Create Media.
- Pass its
media_idinmedia_ids.
For media_urls, host the file at a public https URL. We fetch it once when
the message is sent.
Limits and types
| Limit | Value |
|---|---|
| Files per send | 10 |
| Size per file | 1 MiB |
| Size per send | 5 MiB |
| URL length | 2,048 characters |
| URL fetch | 10 seconds per file, 15 seconds in total on POST /sms; 5 and 8 seconds on replies |
Accepted types are JPEG, PNG, GIF, WAV and MP3. The type is read from the file's
bytes; the declared Content-Type and the extension are ignored. Anything else
fails as 3036 (Unsupported Media Type).
media_urls rules: https only, no redirects, and no user name or password in
the URL. Private, loopback and link-local hosts are refused.
A media_id must be yours, not deleted, and have a completed upload;
otherwise it fails as 3033 (Media Not Found).
Scanning and storage
We copy each file to our CDN, mms.dropcowboy.com, and scan it for malware.
Nothing is sent until the scan comes back clean; the carrier fetches the
file from that copy. POST /sms waits up to 45 seconds for a scan and replies
wait up to 12 seconds. When the wait runs out, the send fails as 3038
(Media Scan Pending), and you can retry. A retry of the same file on the same
UTC day reuses the stored copy and its finished scan, so it does not wait
again.
The copies are deleted after 3 days, and the links stop working then. If a
POST /sms MMS waits longer than that before it goes out, for example for an
opt-in reply, the carrier can no longer fetch the file. The original
media_id entry in the Media API is not affected.
Billing
An MMS is billed per file, not per text segment: each file goes out as its own carrier MMS carrying the caption. The caption adds no charge. Three files cost three MMS units whatever the caption length.
On a phone line whose texting campaign asks for opt-in first, a contact who has not opted in gets the opt-in request as a plain SMS, without the media. That message is billed by SMS segments of 153 characters. The MMS is billed per file when it is sent after the contact replies YES.
Caption and opt-out text
The caption is sent exactly as written, on POST /sms and on replies. No
opt-out text is added to an MMS, and an MMS with no caption goes out as media
only, with no text. If your texting campaign requires opt-out language, put it
in the caption. (A text-only POST /sms without opt-out language still gets
Reply STOP to opt-out appended.)
What else changes with media
- Any country. MMS has no destination limit of its own and is priced at the destination country's MMS rate. Countries your account blocks stay blocked.
- No templates. Media with
template_idfails as3032(Invalid Media). - Texts only. Media on
/rvm,/voice-broadcastor/ai-broadcastfails as3032. - An empty text fails.
POST /smswith no body, notemplate_idand no media fails as3020(Missing Parameters).
MMS results
On POST /sms, media is checked after the 202, like everything else, so
media problems arrive with the result on your callback_url and as a
contact.sms.status failure. An MMS that goes out reports
campaign_type: mms; a request refused before its media is stored reports
sms. The status webhook carries no media. status: success means the
message was sent to the carrier. It does not confirm the handset received it
or displayed the media.
reason_code |
Reason | Retry? |
|---|---|---|
3032 |
Invalid Media: bad media_urls or media_ids, more than 10 files, media with template_id, or media on a voice route |
After fixing |
3033 |
Media Not Found | After fixing |
3034 |
Media Fetch Failed: the URL did not answer 200 in time, or redirected |
Later |
3035 |
Media Too Large | After fixing |
3036 |
Unsupported Media Type | After fixing |
3037 |
Media Blocked by Malware Scan | No |
3038 |
Media Scan Pending | Later |
3039 |
Media Scan Failed | Later |
A failure to store the file reports 3999 (Other); retry later. Replies
answer synchronously with an HTTP status instead; see
Send Reply errors.
POST /sms has no readback. Its message_id is a queue receipt, the message
does not appear in Get SMS Record or the
thread, and the result comes only from the webhook and callback_url.
Replies are stored and readable, with their media under mms_media.
Inbound MMS arrives as contact.msg.received, with the files listed under
sms.media: id, type, size and scan status, never a link. Inbound files are
scanned after they arrive, so the event usually shows scan_status: pending.
Fetch the message with Get SMS Record a
little later for a download link. See
Webhooks.
Send a voice broadcast
POST /voice-broadcast rings the recipient and plays one message to a live
person and another to an answering machine. A key press can transfer the
call, confirm interest or opt out. It needs phone_line_id or caller_id.
| Field | Purpose |
|---|---|
tts_on_speech / media_on_speech |
Played when a person answers |
tts_on_beep / media_on_beep |
Played to an answering machine |
tts_on_transfer, tts_on_confirm, tts_on_opt_out (or the media_on_* versions) |
Played before a transfer, after the confirm key, after the opt-out key |
voice_id |
Voice for every tts_on_* field. Required when any is set. |
transfer_digit |
Key that transfers the call. Needs transfer_ivr_id (a phone line, preferred) or transfer_to (a number). |
confirm_digit |
Key that confirms interest |
opt_out_digit |
Key that adds the number to your do-not-contact list |
amd_enabled |
Detect answering machines and play the *_on_beep content after the greeting (default false) |
max_ring_seconds |
How long to ring (default 30) |
{
"to": "+13125550142",
"phone_line_id": "e2b6f9a3-5c1d-4e8b-a4f7-9c3e1b5d7a28",
"voice_id": "7d2a9e4b-1c6f-4b3a-8e5d-2f9c7a1b4e63",
"tts_on_speech": "Hi, this is Example Dental confirming your visit tomorrow at 10 AM. Press 1 to talk to us now.",
"tts_on_beep": "Hi, this is Example Dental. Please call us back to confirm your visit.",
"transfer_digit": 1,
"transfer_to": "+12125550100",
"amd_enabled": true
}
forwarding_number is an old alias of caller_id, not a transfer
destination. To transfer, use transfer_digit.
A transfer_ivr_id that is not a valid ID fails with 3030 (Invalid Transfer
IVR). A transfer_digit with neither transfer_ivr_id nor transfer_to
fails with 3031 (Transfer Destination Missing). Both fail before dialing.
Send an AI call
POST /ai-broadcast places a call handled by a published, active agent.
Build and publish the agent first; see AI agents.
| Field | Purpose |
|---|---|
agent_id |
The agent. See GET /agents/public/agents. |
caller_id |
Your number in E.164. Return calls are forwarded to it. |
voice_ids |
Override the agent's voice for this call |
voice_rotation |
sticky or round_robin, when you pass several voice_ids |
{
"to": "+13125550142",
"caller_id": "+12125550100",
"agent_id": "4c8e2a6f-3b9d-4e1a-9c7b-5d2f8a3e6b19",
"callback_url": "https://hooks.example.com/dropcowboy/outcome"
}
The send fails with "No Agent", "Agent Not Active" or "Agent Is Draft" if the agent cannot take calls.
Merge fields
With contact_id, {{ }} tokens in body and tts_body are filled from the
contact before sending.
| Token | Value |
|---|---|
{{contact.first_name}}, {{contact.last_name}}, {{contact.full_name}} |
Name |
{{contact.email}}, {{contact.city}}, {{contact.state}}, {{contact.postal_code}} |
Contact details |
{{contact.custom.<slug>}} |
A custom field, by its lowercase, underscored name |
{{user.team_name}} |
Your team name |
Add a default after | for when the field is empty:
Hi {{contact.first_name|there}}. With to only there is no contact to read,
so each token becomes its default, or empty.
Aliases kept for older integrations
| Field | Also accepted |
|---|---|
to |
phone_number |
body |
message, sms_body |
phone_line_id |
call_route_id, phone_ivr_id, sms_ivr_id (texts), voice_ivr_id (voice) |
media_id |
recording_id |
caller_id |
forwarding_number |