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" }
}
  • data is an object or an array, depending on the route. Each route's page shows its shape.
  • meta.request_id is on every response. The two knowledge-base list routes also put total, limit and offset in meta (see Pagination).
  • Timestamps are epoch milliseconds unless a route's page says otherwise.
  • Creates answer 200 or 201, depending on the route. Treat any 2xx as success.

Two kinds of route answer without the envelope:

  • The four send routes (POST /rvm, /sms, /voice-broadcast and /ai-broadcast) answer 202 with {"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:

  • 429 from the rate limit has the body {"message": "Too Many Requests"}.
  • Plan and verification gates on email sends and on starting a campaign answer 403 with {"error": "...", "message": "..."}. Show message to the user.
  • An email the mail provider refuses answers 200 with data.success: false and data.error. Check success, 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_tier is restricted, suspended or canceled.
{
  "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.

Code samples

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

Retry safely
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));
  }
}
Pagination
curl "https://api-v2.dropcowboy.com/contact/public/contacts?limit=100&offset=200" \
  -H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET"