API reference / Track results
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:
- Generate a new key for each new send. A random UUID works. Keys are 1 to 255 printable ASCII characters.
- If you don't know whether a request got through (a timeout, a network
error, a
500or502), 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. - 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 | |
|---|---|---|
| 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 and do-not-contact
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 unsignedPOSTper 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
httporhttpsURL. A value that isn't a URL fails the send with3019(Invalid Callback). A URL on a private address is skipped without affecting the send. - Redirects aren't followed, so a
3xxanswer 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.