Send lifecycle

This page covers what happens after you send: what the response means, how to retry without sending twice, the rules every send is checked against, and where the result arrives. It applies to the four send routes, POST /rvm, POST /sms, POST /voice-broadcast and POST /ai-broadcast, and notes where email differs.

What a 202 means

A send route answers 202 as soon as it has your request. Your credentials, the recipient, the delivery rules and your balance are checked after that. The result arrives later on a signed webhook and on your callback_url.

{ "status": "queued", "message_id": "8e4a2c6f-9d1b-4f7e-b3a5-6c9e1f4d2b78" }

Treat 202 as "received", not "sent". An invalid number or a missing media file still gets 202 and then fails. Wait for the result before you report a send as delivered.

The four send routes answer with this flat object, without the data / meta envelope used elsewhere. message_id doesn't appear on later results, so match results as described in Act on the result.

Only these responses come back on the request itself:

Status Body Meaning What to do
202 {"status":"queued","message_id":"..."} Received Wait for the result
400 Problem details, type ending validation-error The body is empty, isn't a JSON object, or is over 256 KB, or a header has characters that aren't printable ASCII Fix the request
401 Problem details, type ending missing-credentials No x-key / x-secret pair and no Authorization: Bearer token Add credentials
429 {"message":"Too Many Requests"} You're sending too fast Back off and retry. See Rate limits.
500 {"status":"error"} Nothing was received Retry with the same Idempotency-Key
502 Problem details, type ending internal-error The request may or may not have been received Retry with the same Idempotency-Key

Wrong credentials still get 202. A wrong key or secret, or a bearer token that is invalid, expired or lacks the route's send scope, fails the send with 3007 (Not authorized). That failure arrives on callback_url only, because the request can't be tied to your account, so no webhook fires. Set a callback_url while you build. See Authentication for scopes.

A recipient that can't be resolved, such as a malformed number or a deleted contact, also gets 202 and then fails. See Choose a recipient.

Retry safely with Idempotency-Key

Send an Idempotency-Key header with every send so a retry can't send twice:

  1. Generate a new key for each new send. A random UUID works. Keys are 1 to 255 printable ASCII characters.
  2. If you don't know whether a request got through (a timeout, a network error, a 500 or 502), retry with the same key and the same body. Within 24 hours the retry is skipped if the first request was received, so the contact gets the message at most once.
  3. To send again on purpose, for example after fixing a failed send, use a new key.
curl -X POST https://api-v2.dropcowboy.com/sms \
  -H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4f8c2e1a-7b3d-4a9e-8c6f-2d1e5b7a9c34" \
  -d '{
    "to": "+13125550142",
    "phone_line_id": "e2b6f9a3-5c1d-4e8b-a4f7-9c3e1b5d7a28",
    "body": "Your order is ready for pickup. Reply STOP to opt out.",
    "callback_url": "https://hooks.example.com/dropcowboy/outcome"
  }'

Keys are compared exactly as sent, per account. Key order and whitespace in the body don't affect the match, but the route does, so never reuse a key on a different route. If no result has arrived 15 minutes after a request, retry with the same key: the retry sends only if the first attempt stopped before sending.

When a retry is ignored or fails

Retry Send routes Email
Same key, same body 202. The retry is skipped: nothing is sent and no second callback or webhook fires. The first request's result is the one that counts. The first successful response, with Idempotent-Replayed: true. 409 idempotency-request-in-progress while the first request is still running.
Same key, different body or route 202, then the send fails with 3027 (Idempotency Key Conflict). Nothing is sent. 409 idempotency-key-reused
Invalid key 202, then the send fails with 3028 (Invalid Idempotency Key). Nothing is sent. 400 invalid-idempotency-key
Empty header Treated as no key Treated as no key

Email keeps a key only after a successful send, so a retry after an error or success: false sends again. Email has the details.

Delivery rules

Every send is checked against these rules after the 202. A send that breaks one fails with a reason_code; Outcomes says what each code means and whether to retry. Email has its own rules, described on Email.

Calling hours

Texts (SMS, MMS and RCS) sent outside the contact's allowed hours fail with 4011. They aren't retried, so resend inside the window. The window follows federal and state-specific calling hours in the contact's time zone.

Ringless voicemails, voice broadcasts and AI calls outside the window are held and tried again until the window opens. A voice send that still can't go out after 3 days fails.

Contact frequency limit

Each account has a contact frequency limit: the most attempts to one phone number in a rolling window. The default is 3 attempts in 3 days. Campaign sends and dialer calls to the same number count together across your team. Test numbers are exempt.

A send over the limit fails with 4013 (Too Many Attempts). It isn't retried, so try again once the window has moved on.

Account owners change the limit on the Dialing rules page, for example to 7 attempts in 7 days for debt collection. The page accepts at least 3 attempts and a window of at least 3 days. Read the current value from GET /register/public/account (see Account):

{
  "delivery_limits": {
    "frequency": { "max_attempts": 3, "window_days": 3, "window_ms": 259200000, "source": "default" }
  }
}

source is team when your account has its own setting and default when it uses the platform default.

A send can make the limit stricter for itself, never looser:

Field Applied when Otherwise
max_attempts Lower than the account's attempts Ignored
max_attempt_window_ms Longer than the account's window. Over 30 days is capped at 30 days. Ignored

Values that aren't positive whole numbers are ignored without an error. A 4013 status webhook carries the limit that was applied and the attempts counted. Read it there, because the callback_url body doesn't include it:

"frequency_limit": { "max_attempts": 3, "window_days": 3, "attempts_in_window": 3 }

Consent is read from the contact record and your opt-in lists at the moment of sending:

  • A number on your do-not-contact list fails with 4016.
  • When your account requires consent, a contact without consent for the channel fails with 6011. Record consent first; see Consent.
  • When your account requires double opt-in for texts, an unconfirmed contact gets a confirmation request first. Your text goes once they confirm.

Manage the list with Do-not-contact.

Balance

Send routes never answer 402. An empty balance fails the send with 3000 (No Funds). Check it with GET /campaign/public/balance (see Account), then add funds or turn on auto-recharge. Routes that spend money while you wait answer 402 instead; see Payment required.

Test numbers

List your own phone numbers as test numbers on the Dialing rules page. They skip the contact frequency limit, so you can send to yourself repeatedly while you build. Calling hours still apply. Test email addresses on the same page skip the email frequency cap.

Where results arrive

Every send produces one result: success, or failure with a reason_code and reason. It arrives in two places:

  • Signed webhooks. These are signed and retried, so rely on them. Subscribe once per event type; Results of your sends lists the event for each channel.
  • callback_url. One unsigned POST per send, described below.

When a voicemail system took the call, the result can also carry a link to a recording that proves delivery. That covers ringless voicemail, and voice broadcasts and AI calls left in a mailbox. It's available for 7 days; see Proof of delivery.

If no result arrives, work through I got a 202 but nothing happened.

callback_url

Every send route accepts a callback_url. It gets one POST per send with the result, including early failures such as 3007 and 3027. Use it for convenience and rely on signed webhooks for anything that matters:

  • It isn't signed, and it's tried once with a 10-second timeout and no retry.
  • It must be a publicly reachable http or https URL. A value that isn't a URL fails the send with 3019 (Invalid Callback). A URL on a private address is skipped without affecting the send.
  • Redirects aren't followed, so a 3xx answer is final.
  • It echoes your foreign_id. Status webhooks don't.
{
  "drop_id": "b3e7a1c9-8d5f-4b2e-9a6c-1f4d7b3e8a52",
  "contact_id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
  "phone_number": "+13125550142",
  "caller_id": "+13125550100",
  "product_code": "sms",
  "status": "failure",
  "reason": "TCPA Hours",
  "reason_code": 4011,
  "foreign_id": "order-1042",
  "attempt_date": "2026-03-20T03:15:00.000Z"
}

The example is trimmed. The full body also carries team_id, session_id, log_id, quantity, the cost fields and dnc, and, for a result with proof of delivery, proof_of_delivery_url.

A send that fails before anything is sent (bad credentials, an idempotency conflict, a validation error) has a different body. It has no drop_id, and phone_numbers lists the numbers the request named:

{
  "status": "failure",
  "reason": "Idempotency Key Conflict",
  "reason_code": 3027,
  "product_code": "sms",
  "team_id": "3f6c2a1e-8b4d-4c7a-9e2f-5a1b3c4d6e7f",
  "log_id": "6b2d9f4e-1a7c-4e3b-9d8f-2c5a7e1b4d93",
  "session_id": null,
  "contact_id": null,
  "phone_selector": null,
  "phone_number": "+13125550142",
  "phone_numbers": ["+13125550142"],
  "foreign_id": "order-1042",
  "attempt_date": "2026-03-20T03:15:00.000Z"
}

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.

Code samples

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

Retry safely with Idempotency-Key
curl -X POST https://api-v2.dropcowboy.com/sms \
  -H "x-key: $DC_KEY" -H "x-secret: $DC_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 4f8c2e1a-7b3d-4a9e-8c6f-2d1e5b7a9c34" \
  -d '{
    "to": "+13125550142",
    "phone_line_id": "e2b6f9a3-5c1d-4e8b-a4f7-9c3e1b5d7a28",
    "body": "Your order is ready for pickup. Reply STOP to opt out.",
    "callback_url": "https://hooks.example.com/dropcowboy/outcome"
  }'