API reference / Outreach
Conversations and replies
When a contact writes back, you read the conversation and reply. Replies are
sent before the route answers, and the response is the stored message. That's
different from POST /sms, which queues a new outbound text and
reports the result later.
- Texts from contacts arrive on the
contact.msg.receivedwebhook. A STOP reply also sendscontact.msg.opt_out. See Inbound messages. - Website chat visitors are answered with Reply to a web chat.
Routes
| Method | Route | Scope | What it does |
|---|---|---|---|
GET |
/phone/public/sms/{sms_id} |
contacts:read |
Get one stored text |
GET |
/phone/public/sms/thread/{contact_id} |
contacts:read |
Get a contact's text thread |
POST |
/phone/public/sms/reply |
sms:send |
Send a text or MMS reply |
POST |
/chat/public/reply |
chat:write |
Reply to a web chat |
GET |
/chat/public/sites |
chat:read |
List your chat sites |
Get SMS record
GET /phone/public/sms/{sms_id} returns one stored message: an inbound text,
or a reply sent from the inbox or this API. Texts sent with POST /sms aren't
stored here.
curl https://api-v2.dropcowboy.com/phone/public/sms/8945b5d3-d4b9-435e-ab6d-a21bb5b9b628 \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": {
"sms_id": "8945b5d3-d4b9-435e-ab6d-a21bb5b9b628",
"sms_type": "inbound",
"team_id": "3f6c2a1e-8b4d-4c7a-9e2f-5a1b3c4d6e7f",
"contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"from": "+13125550142",
"to": "+12125550100",
"sms_body": "Here is the photo of the damage.",
"created_at": 1774041600000,
"mms_media": [
{
"media_id": "a9c3e8f1-4b2d-4a7c-8e9b-1c2d3e4f5a6b",
"ext": "jpg",
"content_type": "image/jpeg",
"size": 184220,
"scan_status": "clean",
"url": "https://files.example.com/a9c3e8f1-4b2d-4a7c-8e9b-1c2d3e4f5a6b.jpg?X-Amz-Expires=86400"
}
]
},
"meta": { "request_id": "c7238eeb-141a-492c-84e8-d71321d19b99" }
}
sms_type is inbound or outbound, and created_at is epoch
milliseconds. Fields that were never set are left out.
Each file in mms_media has a url once its scan_status is clean. For an
inbound file, the url is a signed link that works for 24 hours, so fetch the
record again for a fresh one. For a file you sent in a reply, it's the
mms.dropcowboy.com copy, which works for 3 days after sending.
An sms_id that doesn't exist, or belongs to another account, answers 404.
Get SMS thread
GET /phone/public/sms/thread/{contact_id} returns the stored messages for a
contact, inbound and outbound, newest first.
| Field | Type | Required | Description |
|---|---|---|---|
skip |
integer | No | How many to skip, 0 or more. Default 0. |
limit |
integer | No | How many to return, 1 or more. Default 50. |
curl "https://api-v2.dropcowboy.com/phone/public/sms/thread/5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d?limit=10" \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": {
"smss": [
{
"sms_id": "bb8a4f80-2cd2-4048-9dca-b75cab74b974",
"sms_type": "inbound",
"contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"from": "+13125550142",
"to": "+12125550100",
"sms_body": "Thanks! Can you send more info?",
"created_at": 1774042500000
}
]
},
"meta": { "request_id": "ee87822c-45bb-43bb-b437-89cf458c111e" }
}
Each message has the same fields as Get SMS record. A
contact with no messages, or one on another account, returns an empty smss.
A skip or limit that isn't a whole number in range answers 400.
Reading the thread marks the contact's unseen messages as seen, with
seen_by set to null. The response still shows each message's seen_at
from before this read.
Send reply
POST /phone/public/sms/reply sends one text or MMS from a number on your
account, then answers with the stored message.
| Field | Type | Required | Description |
|---|---|---|---|
phone_number |
string | Yes | Recipient in E.164. If no contact has this number, one is created. |
caller_id |
string | Yes | The sender. Must be a texting-enabled number on your account. |
sms_body |
string | Unless media is sent | The text, or the caption for media. Up to 1,600 characters. |
media_urls |
string[] | No | https URLs to send as MMS. The rules are in MMS. |
media_ids |
string[] | No | Media API ids to send as MMS |
contact_id |
string | No | The contact to record the message against |
curl -X POST https://api-v2.dropcowboy.com/phone/public/sms/reply \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+13125550142",
"caller_id": "+12125550100",
"sms_body": "Here is the floor plan you asked about.",
"media_urls": ["https://files.example.com/floor-plan.png"]
}'
{
"data": {
"sms_id": "f3fe47ad-c169-4f00-8993-68025a9195a1",
"sms_type": "outbound",
"contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"from": "+12125550100",
"to": "+13125550142",
"sms_body": "Here is the floor plan you asked about.",
"parts": 1,
"delivered": true,
"reason": "delivered",
"media_urls": [
"https://mms.dropcowboy.com/3f6c2a1e-8b4d-4c7a-9e2f-5a1b3c4d6e7f/0b7c9e2d-4f1a-5d3b-9e6c-2a8f4b1d7c35.png"
]
},
"meta": { "request_id": "78af89a4-2fcb-4956-bd6f-5053d50869d5" }
}
The example is trimmed; the stored message also lists its files under
mms_media. delivered: true means the carrier accepted the message, not
that the phone received it. The links in media_urls stop working 3 days
after sending. A sent reply also fires contact.msg.sent.
Files are fetched and scanned for malware before the route answers. The scan
waits up to 12 seconds, then the route answers 503. A retry of the same file on the same UTC day reuses the stored copy and its
finished scan.
Errors
Each call sends a new message, so check the status before you retry.
| Status | type ends with |
Cause | What to do |
|---|---|---|---|
400 |
invalid-parameters |
Bad media_urls or media_ids, more than 10 files, or a media_id that isn't found |
Fix the fields. |
400 |
server-error |
A missing field, a number on your do-not-contact list, a known litigator, or no usable sender. detail says which. |
Fix the request. Don't retry a do-not-contact refusal. |
402 |
server-error |
The account is past due | Update billing, then retry. |
403 |
server-error |
The account is inactive | Reactivate the account. |
409 |
server-error |
It's outside the contact's calling hours | Retry inside the window. |
413 |
payload-too-large |
A file is over 1 MiB, or the files total over 5 MiB | Send smaller files. |
415 |
unsupported-media-type |
A file isn't JPEG, PNG, GIF, WAV or MP3 | Convert the file. |
422 |
media-fetch-failed |
A URL didn't answer 200 in time, or redirected |
Retry once the URL answers 200 without a redirect. |
422 |
media-blocked |
The malware scan flagged a file | Don't retry. |
500 |
server-error |
Storing a file failed | Retry later. |
502 |
server-error |
The malware scan failed | Retry later. |
503 |
media-scan-pending |
The scan didn't finish in time | Retry after the Retry-After seconds. |
Reply to a web chat
POST /chat/public/reply posts a message into a live website chat. The
visitor sees it in the chat widget, and your team sees it in the shared inbox
as an automation reply. Your plan needs chat on at least one seat.
| Field | Type | Required | Description |
|---|---|---|---|
conversation_id |
string | Yes | From the chat automation triggers, such as "Chat Received" |
message |
string | Yes | The reply. Markdown is allowed. |
sender_name |
string | No | The name the visitor sees. Default Automation. |
metadata |
object | No | Saved with the message |
curl -X POST https://api-v2.dropcowboy.com/chat/public/reply \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": "2b7d9f1a-4c6e-4e8a-b0d2-6f8a1c3e5b79",
"message": "Thanks for reaching out! A teammate will follow up within the hour.",
"sender_name": "Example Dental"
}'
{
"data": {
"message_id": "6e8a0c2d-4f1b-4a3c-9e5d-7b9f1d3a5c28",
"conversation_id": "2b7d9f1a-4c6e-4e8a-b0d2-6f8a1c3e5b79"
},
"meta": { "request_id": "0c2e4a6b-8d1f-4b3e-a5c7-9e1b3d5f7a41" }
}
Each call posts a new message, and this route takes no Idempotency-Key.
Retrying after a success posts the reply twice, so retry only on an error.
Chat reply errors
| Status | type ends with |
Cause | What to do |
|---|---|---|---|
400 |
validation-error |
conversation_id or message is missing |
Add it. |
402 |
payment-required |
The account is past due | Update billing, then retry. |
403 |
addon_not_enabled |
Your plan has no chat seat | Add chat to a seat. |
403 |
account_inactive |
The account is inactive | Reactivate the account. |
404 |
not-found |
No conversation with that id on your account | Check conversation_id. |
See Responses, errors and limits for everything else.
List chat sites
GET /chat/public/sites lists the websites with your chat widget. Use
chat_site_id wherever a chat site is asked for, such as in chat
automations.
| Field | Type | Required | Description |
|---|---|---|---|
search_term |
string | No | Match on the site name |
limit |
integer | No | How many to return |
curl https://api-v2.dropcowboy.com/chat/public/sites \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": [
{ "chat_site_id": "1c5e9a3f-7b2d-4f8e-a6c4-3d9b7f1e5a20", "site_name": "Example Dental" }
],
"meta": { "request_id": "4a8c2e6f-1b3d-4e7a-9c5f-8d2b6e4a1c37" }
}
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.