Error Codes
Complete reference of HTTP status codes and error-response shapes across every ScaiSend endpoint. For the conceptual overview and retry guidance, see Errors.
Response shapes#
ScaiSend returns errors in one of two shapes depending on the endpoint:
SendGrid-style (for /v3/mail/send and related sending endpoints):
1 2 3 4 5 | |
FastAPI-style (everywhere else):
1 | |
For Pydantic validation errors, detail becomes an array of {"loc": [...], "msg": "...", "type": "..."} objects.
HTTP status codes#
200 OK#
Generic success. Returned on read endpoints and on updates.
201 Created#
Resource was created successfully. Response body contains the new resource.
202 Accepted#
Queued for asynchronous processing. Returned exclusively by POST /v3/mail/send. The message is queued; delivery will happen in the background.
204 No Content#
Success with no body. Returned on successful deletes and some idempotent operations (e.g., POST /api/admin/users/{id}/roles/{role_id}).
400 Bad Request#
The request is malformed or violates a logical constraint.
| Endpoint family | Example cause |
|---|---|
/v3/mail/send |
template_id doesn't start with d-; both content and template_id supplied; personalizations empty |
/v3/templates/* |
Cannot delete only active version; referenced template not dynamic |
/v3/api_keys |
Unknown scope name |
/api/admin/domains |
Invalid DMARC policy value |
/v3/suppression/bounces (DELETE) |
delete_all=true missing from bulk delete query |
401 Unauthorized#
Missing or invalid credentials.
| Body | When |
|---|---|
{"detail": "Missing Authorization header"} |
No Authorization header present |
{"detail": "Invalid API key"} |
Key not found in DB; typo; revoked |
{"detail": "JWT expired"} |
Access token past exp |
{"detail": "JWT signature invalid"} |
Token signature didn't verify against JWKS |
403 Forbidden#
Authenticated but not authorized for this action.
| Body | When |
|---|---|
{"detail": "Missing required scope: <scope>"} |
Credential lacks the scope |
{"detail": "Tenant suspended"} |
Tenant has been administratively suspended in ScaiKey |
{"detail": "Sender domain not verified"} |
from.email domain hasn't been verified (live sends only) |
{"detail": "Cross-tenant access denied"} |
Trying to access another tenant's resource |
404 Not Found#
Resource does not exist, or belongs to a different tenant.
| Endpoint family | Example |
|---|---|
/v3/messages/{id} |
Unknown message ID |
/v3/templates/{id} |
Unknown template ID |
/v3/api_keys/{id} |
Unknown or revoked key |
/v3/user/webhooks/{id} |
Unknown endpoint |
/v3/suppression/bounces/{email} |
Address not on the list |
/api/admin/domains/{id} |
Unknown domain |
/i/{image_id} |
Unknown image (as opposed to 410 Gone for deleted) |
409 Conflict#
State conflict.
| Endpoint family | Example |
|---|---|
/api/admin/domains |
Domain already exists for this tenant |
/api/admin/roles |
Role name already used |
/v3/templates |
Template name already used |
410 Gone#
The resource existed but has been deleted.
| Endpoint | When |
|---|---|
/i/{image_id} |
Image was deleted after being referenced in a sent email |
413 Payload Too Large#
Request body exceeds the configured limit.
| Endpoint | Limit |
|---|---|
/v3/mail/send |
20 MB total body (including base64 attachments) |
/v3/images |
10 MB per image |
422 Unprocessable Entity#
Pydantic schema validation failed. detail is an array:
1 2 3 4 5 | |
429 Too Many Requests#
Rate limit exceeded.
Response headers:
| Header | Notes |
|---|---|
Retry-After |
Seconds to wait before retrying |
X-RateLimit-Limit |
Limit for this endpoint/credential |
X-RateLimit-Remaining |
Remaining requests in the current window |
X-RateLimit-Reset |
Unix timestamp when the window resets |
See Rate Limiting.
500 Internal Server Error#
Unexpected server error. Capture the response headers — the X-Request-ID (or X-Scaisend-Request-Id) is needed for support tickets.
503 Service Unavailable#
A dependency is down (MySQL, Redis, ScaiKey JWKS). Retry after a delay.
Common error sources#
mail_settings.sandbox_mode forced to true#
If the credential is a test key (sg_test_*), any value of sandbox_mode.enable is ignored and sandbox is active. This is a feature, not an error — the message is accepted normally.
from.email domain not verified#
Live sends require a verified sender domain. Sandbox sends (test key or sandbox_mode.enable: true) skip this check.
Template rendering failure#
Renders happen in the worker, after the 202 response. A template error doesn't cause a send-time 4xx. Instead:
- Message status becomes
FAILED. error_messageon the message record has the exception.- A
droppedevent is emitted with reasontemplate_render_error.
Webhook endpoint auto-disabled#
If a webhook endpoint fails 10 consecutive deliveries, ScaiSend sets disabled_at. Subsequent events are not delivered to this endpoint until you re-enable with PATCH {"enabled": true}.
Retry classification#
| Status | Retry? | How |
|---|---|---|
| 400, 401, 403, 404, 409, 410, 413, 422 | No | Fix the request |
| 429 | Yes | Honor Retry-After |
| 500 | Yes, limited | 2–3 attempts with exponential backoff |
| 503 | Yes | Exponential backoff |
See Errors for recommended retry loops.
Related#
- Errors (concept)
- Rate Limiting
- Your First Integration — includes a reference retry loop.