API reference · More
SMS Records and MMS
Send picture and audio messages (MMS), read individual SMS records and a contact's thread, reply to a contact, and list IVR configurations.
To send a new text, use POST /sms; see Messaging. It answers
202 and reports the result later. Its only immediate errors are 400
(empty or non-JSON body), 401 (missing credentials), 429, 500 and 502,
and it accepts Idempotency-Key for safe retries. Request problems such as
3029 (Phone Line Has No SMS Campaign) arrive with the result; see
Outcomes.
MMS
POST /sms and POST /phone/public/sms/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).
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.
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.
Get SMS Record
GET /phone/public/sms/:sms_id
Retrieve a single stored message: inbound texts and replies sent through this API or the inbox.
Required scope: contacts:read
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
sms_id |
string | Yes | The SMS record ID |
Example Request
curl -X GET "https://api-v2.dropcowboy.com/phone/public/sms/8945b5d3-d4b9-435e-ab6d-a21bb5b9b628" \
-H "X-Key: your_api_key" \
-H "X-Secret: your_api_secret"
Example Response
Fields that were never set on the message are omitted.
{
"data": {
"sms_id": "8945b5d3-d4b9-435e-ab6d-a21bb5b9b628",
"sms_type": "inbound",
"team_id": "e4b1c7d2-8a3f-4c6e-9b5d-1f2a3c4d5e6f",
"contact_id": "7b764153-bb16-4ece-80dd-95556699c2ea",
"from": "+12125550120",
"to": "+14155550120",
"sms_body": "Here is the photo of the damage.",
"created_at": 1757932200000,
"mms_media": [
{
"media_id": "a9c3e8f1-4b2d-4a7c-8e9b-1c2d3e4f5a6b",
"content_type": "image/jpeg",
"size": 184220,
"scan_status": "clean",
"url": "https://files.example.com/a9c3e8f1-4b2d-4a7c-8e9b-1c2d3e4f5a6b.jpg?X-Amz-Signature=..."
}
]
},
"meta": {
"request_id": "c7238eeb-141a-492c-84e8-d71321d19b99"
}
}
sms_type is inbound or outbound. created_at is epoch milliseconds.
mms_media lists the message's files. url appears only when
scan_status is clean. For inbound media it is a signed link that works for
24 hours from the request. For media you sent with a reply, it is the
mms.dropcowboy.com copy, which works for 3 days after sending.
An unknown sms_id, or one that belongs to another account, answers 404
with type ending in not-found.
Get SMS Thread
GET /phone/public/sms/thread/:contact_id
Retrieve the stored messages recorded against a contact, inbound and outbound,
newest first. A contact_id with no messages, or one that is not on your
account, returns an empty smss.
Reading the thread marks that contact's unseen messages as seen, with seen_by
set to null. No other contact's messages are touched. Messages in the response
still carry the seen_at they had before this read.
Required scope: contacts:read
Path Parameters
| Name | Type | Required | Description |
|---|---|---|---|
contact_id |
string | Yes | The contact ID |
Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
skip |
integer | No | Number of messages to skip, 0 or more (default: 0) |
limit |
integer | No | Number of messages to return, 1 or more (default: 50) |
A skip or limit that is not a whole number in range answers 400 with
type ending in invalid-parameters.
Example Request
curl -X GET "https://api-v2.dropcowboy.com/phone/public/sms/thread/7b764153-bb16-4ece-80dd-95556699c2ea?limit=10" \
-H "X-Key: your_api_key" \
-H "X-Secret: your_api_secret"
Example Response
Each entry in smss has the same fields as Get SMS Record.
{
"data": {
"smss": [
{
"sms_id": "bb8a4f80-2cd2-4048-9dca-b75cab74b974",
"sms_type": "inbound",
"contact_id": "7b764153-bb16-4ece-80dd-95556699c2ea",
"from": "+12125550120",
"to": "+14155550120",
"sms_body": "Thanks! Can you send more info?",
"created_at": 1757933100000
},
{
"sms_id": "8945b5d3-d4b9-435e-ab6d-a21bb5b9b628",
"sms_type": "outbound",
"contact_id": "7b764153-bb16-4ece-80dd-95556699c2ea",
"from": "+14155550120",
"to": "+12125550120",
"sms_body": "Hi Jane, just following up on our conversation. Reply STOP to opt-out",
"created_at": 1757932200000
}
]
},
"meta": {
"request_id": "ee87822c-45bb-43bb-b437-89cf458c111e"
}
}
Send Reply
POST /phone/public/sms/reply
Send a text or MMS to one number from a number on your account. Unlike POST /sms, this route sends before it answers, and returns the stored message.
Required scope: sms:send
Request Body
| Name | Type | Required | Description |
|---|---|---|---|
phone_number |
string | Yes | Recipient number in E.164 format. If no contact has this number, one is created |
caller_id |
string | Yes | Sender number. Must be an SMS-enabled number on your account |
sms_body |
string | Unless media is sent | The text, or the caption for media |
media_urls |
string[] | No | https URLs to send as MMS; see MMS |
media_ids |
string[] | No | Media API ids to send as MMS; see MMS |
contact_id |
string | No | Contact to record the message against |
The route does not accept body or message as aliases for sms_body.
Example Request
curl -X POST "https://api-v2.dropcowboy.com/phone/public/sms/reply" \
-H "X-Key: your_api_key" \
-H "X-Secret: your_api_secret" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+12125550120",
"caller_id": "+14155550120",
"sms_body": "Here is the floor plan you asked about.",
"media_urls": ["https://files.example.com/floor-plan.png"]
}'
Example Response
Abbreviated: the stored row has more fields than shown.
{
"data": {
"sms_id": "f3fe47ad-c169-4f00-8993-68025a9195a1",
"team_id": "e4b1c7d2-8a3f-4c6e-9b5d-1f2a3c4d5e6f",
"contact_id": "7b764153-bb16-4ece-80dd-95556699c2ea",
"sms_type": "outbound",
"from": "+14155550120",
"to": "+12125550120",
"sms_body": "Here is the floor plan you asked about.",
"parts": 1,
"media_urls": [
"https://mms.dropcowboy.com/e4b1c7d2-8a3f-4c6e-9b5d-1f2a3c4d5e6f/0b7c9e2d-4f1a-5d3b-9e6c-2a8f4b1d7c35.png"
],
"mms_media": [
{
"media_id": "0b7c9e2d-4f1a-5d3b-9e6c-2a8f4b1d7c35",
"ext": "png",
"content_type": "image/png",
"size": 48213,
"scan_status": "clean",
"url": "https://mms.dropcowboy.com/e4b1c7d2-8a3f-4c6e-9b5d-1f2a3c4d5e6f/0b7c9e2d-4f1a-5d3b-9e6c-2a8f4b1d7c35.png"
}
]
},
"meta": {
"request_id": "78af89a4-2fcb-4956-bd6f-5053d50869d5"
}
}
The stored row also carries delivered: true and reason: "delivered".
Those record that the carrier accepted the message; they do not confirm the
handset received it. The CDN links in media_urls and mms_media stop working
3 days after sending.
A successful reply also fires contact.msg.sent.
Errors
| Status | type ends with |
Cause |
|---|---|---|
400 |
invalid-parameters |
Bad media_urls or media_ids, more than 10 files, or a media_id that is not found |
400 |
server-error |
Missing phone_number, caller_id, or both sms_body and media; do-not-contact number; no usable sender |
402 |
server-error |
Account billing is past due |
413 |
payload-too-large |
A file is over 1 MiB, or the files total over 5 MiB |
415 |
unsupported-media-type |
A file is not JPEG, PNG, GIF, WAV or MP3 |
422 |
media-fetch-failed |
A URL did not answer 200 in time, or redirected |
422 |
media-blocked |
The malware scan flagged a file. Do not retry |
502 |
server-error |
The malware scan failed. Retry later |
503 |
media-scan-pending |
The scan did not finish in time. Retry after the Retry-After seconds (15) |
500 |
server-error |
Storing a file failed. Retry later |
List IVRs
GET /phone/public/ivrs
Returns a list of IVR (Interactive Voice Response) configurations on your account. IVRs define automated call flows and can be assigned to phone numbers for inbound routing.
Required scope: numbers:read
Example Request
curl -X GET "https://api-v2.dropcowboy.com/phone/public/ivrs" \
-H "X-Key: your_api_key" \
-H "X-Secret: your_api_secret"
Example Response
{
"data": [
{
"id": "2f6aee2e-63eb-4463-bd79-4fda2c33fe68",
"name": "Main Menu",
"description": "Primary inbound call routing",
"status": "active",
"created_at": "2025-08-01T14:00:00Z",
"updated_at": "2025-09-10T09:15:00Z"
},
{
"id": "ba67602a-3abf-4cac-90f7-59d723f76753",
"name": "After Hours",
"description": "Voicemail routing for outside business hours",
"status": "active",
"created_at": "2025-08-15T11:30:00Z",
"updated_at": "2025-08-15T11:30:00Z"
}
],
"meta": {
"request_id": "1c40b016-9891-4724-a07c-61da5648ad34",
"total": 2
}
}