API reference / Account and reference
Responses, errors and limits
Every REST route on https://api-v2.dropcowboy.com follows the same rules for
successful responses, errors, rate limits and paging. This page covers them.
What happens to a send after its 202 is in Send lifecycle.
Response format
A successful response wraps the result in data and puts request metadata in
meta:
{
"data": {
"_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"first_name": "Jordan",
"last_name": "Rivera",
"phone_number": "+13125550142"
},
"meta": { "request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24" }
}
datais an object or an array, depending on the route. Each route's page shows its shape.meta.request_idis on every response. The two knowledge-base list routes also puttotal,limitandoffsetinmeta(see Pagination).- Timestamps are epoch milliseconds unless a route's page says otherwise.
- Creates answer
200or201, depending on the route. Treat any2xxas success.
Two kinds of route answer without the envelope:
- The four send routes (
POST /rvm,/sms,/voice-broadcastand/ai-broadcast) answer202with{"status": "queued", "message_id": "..."}. See What a 202 means. GET /campaign/public/receipts/{token}plays a proof of delivery. See Proof of delivery.
Request ids
Every response carries X-Request-Id and X-Correlation-Id headers with the
same value as request_id. To trace a request from your own logs, send your
own X-Request-Id of up to 128 characters. The API uses it instead of
generating one. Quote the request id when you contact support.
Errors
Errors are RFC 9457 problem details, sent as JSON:
{
"type": "https://api-v2.dropcowboy.com/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "Contact not found",
"instance": "/contact/public/contacts/5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24"
}
| Field | Description |
|---|---|
status |
The HTTP status. Branch on this. |
type |
A URL ending in an error code, such as insufficient-scope. Many routes use server-error here for every failure, including 400 and 404, so don't branch on type alone. |
title |
A readable form of the code in type. |
detail |
What went wrong, in words. The wording can change, so log it but don't parse it. |
instance |
The path you called. |
request_id |
Same as the X-Request-Id header. |
details |
Extra fields on some errors, for example dunning_tier on a 402. |
Status codes
| Status | What it means | What to do |
|---|---|---|
400 |
The body or a parameter is invalid, or the body isn't valid JSON. | Fix the request using detail. Don't retry it unchanged. |
401 |
Credentials are missing or wrong, or the key was deleted or has expired. | Check x-key and x-secret, or get a new token. See Authentication. |
402 |
Out of funds, or the subscription payment failed. | Add funds or update the payment method, then retry. See Payment required. |
403 |
Your credential lacks the scope, your plan doesn't include the feature, your trial has ended or the account is inactive, or you can't change this resource. | Use a credential with the scope in detail, or fix the account in the dashboard. Don't retry unchanged. |
404 |
Not found, or it belongs to another account. Also any unknown path under /public/. |
Check the id, the path and the method. |
409 |
The request conflicts with current state, for example a reused Idempotency-Key. |
Read detail, fetch the current state, then decide. |
413 |
The body or an attached file is too large. | Send less. |
415 |
The body's encoding or an attached file's type isn't supported. | Send JSON, or a supported file type. |
422 |
The request is well formed but can't be processed, for example MMS media that can't be fetched. | Fix the input named in detail. |
429 |
Too many requests. | Retry with backoff. See Rate limits. |
500, 502, 503, 504 |
Something failed on the server. | Retry reads with backoff. Before retrying a write, check whether it took effect. |
Other error shapes
A few responses aren't problem details. Handle them by HTTP status:
429from the rate limit has the body{"message": "Too Many Requests"}.- Plan and verification gates on email sends and on starting a campaign answer
403with{"error": "...", "message": "..."}. Showmessageto the user. - An email the mail provider refuses answers
200withdata.success: falseanddata.error. Checksuccess, not only the status. See Email.
Send routes
The four send routes queue your request first and check it afterwards. The
request itself can only fail with 400 (empty or non-JSON body), 401 (no
credentials at all), 429, 500 or 502. Everything else, including a wrong
key, arrives later as a reason_code on your callback_url and status
webhook. What a 202 means lists each
response, and Outcomes lists every code.
Retry safely
Retry 429, 500, 502, 503 and 504 with exponential backoff and
jitter. Don't retry other 4xx responses unchanged.
async function withBackoff(call, maxAttempts = 6) {
for (let attempt = 0; ; attempt++) {
const res = await call();
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt + 1 >= maxAttempts) return res;
const delay = Math.min(1000 * 2 ** attempt, 30000);
await new Promise(r => setTimeout(r, delay / 2 + Math.random() * delay / 2));
}
}
Reads are always safe to retry. A retried write can happen twice, so retry it
only when a duplicate is harmless or you send an Idempotency-Key. The four
send routes and POST /email/public/email accept one. See
Retry safely with Idempotency-Key.
Rate limits
Each route accepts up to 1,500 requests per second, with bursts of up to
2,000. The limit counts all traffic to the route, so you can see a 429
below that rate.
A throttled request gets 429 with {"message": "Too Many Requests"} and no
Retry-After header. Retry it with the backoff above. If you send in bulk,
spread requests out rather than sending them all at once.
For sends to many contacts, a campaign is one request instead of one per contact.
Detection
The Detection WebSocket has its own limits. The upgrade is refused with 402
when the account has no funds, and with 503 when no capacity is free.
Reconnect with backoff after a 503. See Detection.
Pagination
List routes page in one of three ways. Default page sizes differ between
routes, so always pass limit.
| Style | Parameters | Routes |
|---|---|---|
| Offset | limit and offset (items to skip) |
Contacts, contact search, contact lists, follow-ups, contact timeline, knowledge bases, knowledge-base documents |
| Skip | limit and skip (items to skip) |
Media, voices, email history, documents, Inbox Tasks, templates, users, campaigns, a contact's text thread |
| Cursor | limit and after_id |
The contacts in a list: GET /contact/public/lists/{id}/contacts |
Offset and skip work the same way: request offset=0, then add limit each
time. Stop when a page returns fewer items than limit. Where the response
has a total, such as total_contacts, you can also stop when you reach it.
curl "https://api-v2.dropcowboy.com/contact/public/contacts?limit=100&offset=200" \
-H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"
{
"data": {
"contacts": [],
"total_contacts": 214,
"next_cursor": null
},
"meta": { "request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24" }
}
The contact and list routes also return a next_cursor. It isn't a page
token on these routes, so page them with offset.
The knowledge-base lists return the page as an array in data and report
the total in meta:
{
"data": [],
"meta": {
"request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24",
"total": 142,
"limit": 25,
"offset": 100
}
}
The contacts in a list page by cursor. Pass limit (1 to 500, default 25),
then pass each response's next_cursor as after_id on the next request.
Stop when has_more is false:
{
"data": {
"contacts": [],
"next_cursor": "65f2a9b7c8d4e1f234567890",
"has_more": true
},
"meta": { "request_id": "c2a6e8f4-3b9d-4c1e-8a7f-5d3b1e9c6a24" }
}
Tags, AI agents, pipelines and chat sites take only limit, with no way to
fetch a second page. Phone numbers, phone lines, webhooks and API keys return
everything in one response.
Payment required (402)
A route that spends money while you wait answers 402 when the account can't
pay. These include text-to-speech, transcription, voice cloning, number
rental, embed site tokens, email sends, creating and starting campaigns,
creating contacts and lists, adding or moving a contact between lists, and
replying to a web chat.
Every 402 is problem details with type
https://api-v2.dropcowboy.com/errors/payment-required. It means one of two
things:
- The prepaid balance and any plan allowance are used up. Add funds in the dashboard, then retry.
- The subscription payment failed. Update the payment method in the
dashboard, then retry. On email and campaign routes,
details.dunning_tierisrestricted,suspendedorcanceled.
{
"type": "https://api-v2.dropcowboy.com/errors/payment-required",
"title": "Payment Required",
"status": 402,
"detail": "Your subscription payment didn't go through, so sending is paused. This is separate from your messaging funds balance. Update your payment method to resume sending campaigns.",
"instance": "/campaign/public/campaigns",
"request_id": "9e1a3c5f-7b2d-4f4e-a6c8-0d2f4b6e8a17",
"details": { "dunning_tier": "suspended" }
}
The four send routes never answer 402. An empty balance shows up there as
outcome 3000 (No Funds). See Balance. Check
your balance with GET /campaign/public/balance (see
Get your balance).
API access is subject to rate limits and usage policies. API availability, endpoints, and features may change with notice. Breaking changes will be communicated via changelog with migration period when possible. API keys must be kept secure. Customers are responsible for all activity under their API credentials.