Error Codes
Every error from the SendFleet API follows a consistent shape. Use the machine-readable error field to branch your error-handling logic; use detail to log or surface a human-readable message.
Error response shape
All non-2xx responses (and some 400-level responses) return a JSON body.
{
"success": false,
"error": "domain_not_verified",
"detail": "Domain 'acme.com' is not yet verified by SES. Add the TXT record and click Verify in the dashboard."
}HTTP 400 - Bad Request
The request body is malformed, missing required fields, or contains invalid values. The response contains field-level validation errors.
{
"to": ["Enter a valid email address."],
"message": ["This field may not be blank."]
}Fix the fields listed in the response. Do not retry without modifying the request.
HTTP 401 - Unauthorized
The X-API-Key header is missing, the key is invalid, the key has been revoked, or the key has expired.
{
"detail": "Authentication credentials were not provided."
}| Cause | Fix |
|---|---|
| Header missing entirely | X-API-Key: YOUR_KEY to every request. |
| Wrong header name | Must be exactly X-API-Key (case-insensitive on most clients). |
| Key revoked | Generate a new key in Dashboard → API Keys. |
| Key expired | Generate a new key or extend the expiry date. |
| Key not found | Verify you copied the full secret, including the part after the dot. |
HTTP 403 - Forbidden
The API key is valid but the account or domain configuration prevents sending. These errors require action in the dashboard - retrying the same request will always fail.
| error | Cause | Fix |
|---|---|---|
| unauthorized | Account email not verified, account not approved for Shared SES, or no active Growth/Pro subscription. | Verify your email address. For Shared SES: submit a verification request and wait for approval. For BYOC: email verification is all that's needed. |
| domain_not_registered | The domain in from_email has not been added to your account. | Go to Dashboard → Domains → Add domain. Then complete DNS verification. |
| domain_not_verified | Domain is registered but SES has not confirmed ownership (TXT record missing or not propagated). | Add the TXT ownership record at your DNS provider, wait for propagation (up to 48 h), and click Verify in the domain detail page. |
| dkim_not_verified | Domain ownership is verified but the 3 DKIM CNAME records are not confirmed. | Add all 3 CNAME records shown in the domain detail page, wait for propagation, and click Verify DKIM. |
| domain_mode_mismatch | The domain's sending mode (BYOC or Shared) doesn't match your current plan. | Either upgrade/change your plan to match, or add a new domain in the correct mode. |
| sending_suspended | Your account's API sending has been suspended, typically due to high bounce or complaint rates. | Contact support to discuss reinstatement. Review your recipient list quality. |
HTTP 422 - Unprocessable Entity
| error | Cause | Fix |
|---|---|---|
| recipient_suppressed | The to address is on your suppression list due to a previous hard bounce or spam complaint. | Remove the address from your recipient list. Do not attempt to resend to suppressed addresses - this will damage your sender reputation. Contact support if a suppression was added in error. |
HTTP 429 - Too Many Requests
Two distinct rate limits can produce a 429:
| error | Cause | Retry strategy |
|---|---|---|
| usage_limit_exceeded | Monthly email quota exhausted for your plan (50 for Starter BYOC, 25,000 for Growth, 100,000 for Pro). | Upgrade your plan in Dashboard → Billing. Monthly quota resets at the start of each calendar month. |
| (none - DRF throttle) | Per-key rate limit exceeded (60 req/min). | Implement exponential backoff. Wait at least 1 second before retrying. Spread burst sends over multiple seconds. |
HTTP 502 - Bad Gateway
| error | Cause | Retry strategy |
|---|---|---|
| queue_error | AWS SQS was temporarily unavailable when we tried to enqueue your email. | Safe to retry. Use exponential backoff: wait 1 s, 2 s, 4 s, 8 s. After 3–4 retries, treat as a permanent failure and alert. |
HTTP 503 - Service Unavailable
| error | Cause | Fix |
|---|---|---|
| byoc_not_configured | SendFleet's BYOC relay credentials are not configured on our infrastructure. | This is an issue on our end. Contact support. Do not retry indefinitely. |
HTTP 500 - Internal Server Error
An unexpected error on our end. These are rare. Retry once with exponential backoff. If the error persists, contact support with the approximate timestamp and your request details.
Retry strategy reference
| HTTP status | Retryable? | Strategy |
|---|---|---|
| 400 | No | Fix the request payload first. |
| 401 | No | Fix the API key first. |
| 403 | No | Fix account/domain config in dashboard. |
| 422 | No | Remove suppressed address from list. |
| 429 (usage) | No | Upgrade plan or wait for monthly reset. |
| 429 (rate limit) | Yes | Exponential backoff starting at 1 s. |
| 502 | Yes | Exponential backoff, max 4 attempts. |
| 503 | No | Contact support. |
| 500 | Yes | One retry after 2–5 s, then alert. |
Error handling example
import requests, time
def send_email(payload, retries=3):
for attempt in range(retries):
resp = requests.post(
"https://sendfleet.net/api/send/",
headers={"X-API-Key": "YOUR_KEY"},
json=payload,
timeout=10,
)
if resp.status_code == 200:
return resp.json()
data = resp.json()
error_code = data.get("error")
# Non-retryable errors - fix and stop
if resp.status_code in (400, 401, 403, 422):
raise ValueError(f"[{error_code}] {data.get('detail')}")
# Usage limit - upgrade plan
if error_code == "usage_limit_exceeded":
raise RuntimeError("Monthly quota exhausted")
# Retryable - backoff and retry
if resp.status_code in (429, 502, 500):
time.sleep(2 ** attempt)
continue
break
raise RuntimeError("Max retries exceeded")