Error Handling
This guide maps the errors you can see when calling the Agile Telecom SMS API to clear recovery actions. There are three places errors surface: HTTP submission, per-message submission status codes, and DLR final states.
1. HTTP-level Errors (Submission)
| HTTP | Meaning | What to do |
|---|---|---|
200 | Submission accepted (check per-message accepted and statusCode) | Continue; expect DLRs |
400 | Malformed request | Fix the payload; don't retry as-is |
401 | Invalid credentials | Re-check API key / Basic Auth |
403 | IP not whitelisted | Add the calling IP to whitelist |
404 | Wrong endpoint or unknown account-type URL | Use the URL matching your account (legacy vs new) |
408 | Request timeout | Retry with back-off |
413 | Payload too large | Split the messages array into smaller batches |
415 | Wrong Content-Type | Set Content-Type: application/json |
429 | Rate limit exceeded | Exponential back-off, see Best Practices |
5xx | Platform error | Retry with back-off; alert if it persists |
A 200 response does not mean every individual destination was accepted — inspect the per-message statusCode in results.
2. Per-Message Submission Codes
Within a 200 response, each entry in results[] carries its own accepted and statusCode.
| Code | Meaning | Action |
|---|---|---|
200 | Message accepted | Wait for DLR |
400 | Invalid destination or body | Fix and resend |
401 | Sender ID not authorised for this destination/country | Use a different sender ID or pre-register it |
402 | Insufficient credit | Top up via the wholesale portal |
403 | Destination blacklisted or opted-out | Remove from list, do not retry |
429 | Per-account/per-route throttling | Slow down sends to that route |
500 | Internal error | Retry with back-off |
3. DLR Final States
Once submitted, the operator returns a delivery report. The full mapping is in the Delivery Reports guide; the recovery view:
| Status | Code | Recovery |
|---|---|---|
DELIVERED | 0 | None — success |
BUFFERED | 1 | Wait — operator will deliver or expire |
EXPIRED | 2 | Optional: retry once after a delay if the message is still useful |
REJECTED | 3 | Investigate sender ID / content; do not blindly retry |
UNDELIVERABLE | 4 | Mark the number as bad; never retry |
UNKNOWN | 5 | Treat as delivered or expired based on your tolerance for false negatives |
FAILED | 6 | Retry once with back-off; if it persists, alert |
Common Pitfalls
"Everything 401s after midnight." You rotated keys but the cron job still uses the old one. Fix: load keys from env on every restart and rotate via deployment.
"Some destinations silently dropped."
Often the sender ID isn't registered for that country (Italy and India are common offenders). Check REJECTED DLRs and the per-message statusCode in the submission response.
"Customers complain SMS arrived 6 hours late."
The handset was off and the operator buffered the message until power-on. Status was BUFFERED → DELIVERED. Lower the validity (if available on your account) for time-sensitive sends.
"Random 429s under low load." Check the granularity — limits apply per API key and sometimes per route. SMPP avoids most of these; see SMPP.
"Webhook keeps retrying."
Your DLR endpoint isn't returning 200 (or your inbound endpoint isn't returning +OK) within 10–30 seconds. Move heavy work behind a queue.
Retry Strategy in Code
import time, requests
def send_with_retry(payload, headers, max_attempts=3):
backoff = 1.0
for attempt in range(1, max_attempts + 1):
r = requests.post(
"https://wholesale.agiletelecom.com/services/sms/send",
json=payload, headers=headers, timeout=10,
)
if r.status_code == 200:
return r.json()
if r.status_code in (408, 429) or 500 <= r.status_code < 600:
time.sleep(backoff)
backoff *= 2
continue
# 4xx other than 408/429 — don't retry, the request is bad.
r.raise_for_status()
raise RuntimeError("send_with_retry: exhausted retries")
Next Steps
- Best Practices — Throughput, opt-out, idempotency.
- Delivery Reports — DLR contract and webhook setup.
- Wholesale portal — Inspect logs, sender ID approvals and credit.