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 202 means the send was accepted, not delivered. Results arrive on webhooks: contact.rvm.status for voicemail, voice broadcast and AI calls, and contact.sms.status for 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_code 3999). Texts outside the window (default 8am to 9pm in the contact's time zone, with state overrides) fail with reason_code 4011 and 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_code 4013. Read your team's limit at GET /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_code is explained in Outcomes. Subscribe to results in Webhooks. Retries, delivery rules and callback_url are 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. Alias phone_number.
  • contact_id: a contact UUID from GET /contact/public/contacts. The number is picked from the contact with phone_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_body plus voice_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.

  1. Create the media entry, either from a URL or with a signed upload, and complete it; see Create Media.
  2. Pass its media_id in media_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_id fails as 3032 (Invalid Media).
  • Texts only. Media on /rvm, /voice-broadcast or /ai-broadcast fails as 3032.
  • An empty text fails. POST /sms with no body, no template_id and no media fails as 3020 (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

Code samples

The requests from this page, ready to copy. Set DC_KEY and DC_SECRET to your API key pair first.

Send a ringless voicemail
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"
  }'
Send an RCS text from a template
curl -X POST https://api-v2.dropcowboy.com/sms \
  -H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
    "phone_line_id": "e2b6f9a3-5c1d-4e8b-a4f7-9c3e1b5d7a28",
    "template_id": "3a7d1f9c-5e2b-4c8a-9f6d-1b4e7a2c5d93"
  }'
Send a voice broadcast
curl -X POST https://api-v2.dropcowboy.com/voice-broadcast \
  -H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "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
  }'
Send an AI call
curl -X POST https://api-v2.dropcowboy.com/ai-broadcast \
  -H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+13125550142",
    "caller_id": "+12125550100",
    "agent_id": "4c8e2a6f-3b9d-4e1a-9c7b-5d2f8a3e6b19",
    "callback_url": "https://hooks.example.com/dropcowboy/outcome"
  }'