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.

  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).

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
  }
}

Code samples

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

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 Request (2)
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 Request (3)
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 Request (4)
curl -X GET "https://api-v2.dropcowboy.com/phone/public/ivrs" \
  -H "X-Key: your_api_key" \
  -H "X-Secret: your_api_secret"