Errors
ScaiSend returns structured error responses. The HTTP status code classifies the error broadly; the response body gives you the specifics. This page describes the shapes you'll see, when each is used, and how to handle them.
The two shapes#
ScaiSend uses two different error shapes depending on the endpoint:
SendGrid-style (for /v3/mail/send and related)#
1 2 3 4 5 6 7 8 9 | |
errorsis an array. Multiple errors may be returned for a single request (e.g., several validation failures at once).messageis human-readable; safe to display or log.fieldis present when the error relates to a specific input field. Use it to highlight the offending field in a UI.helpis optional; when present, it's a link to relevant documentation.
FastAPI-style (for everything else)#
1 2 3 | |
detailis the human-readable description. Single string for most errors.- For Pydantic validation errors,
detailbecomes an array of field-level error objects:{"loc": ["body", "personalizations"], "msg": "...", "type": "..."}.
Which shape where#
| Endpoint family | Shape |
|---|---|
/v3/mail/send, /v3/mail/batch, and related sending |
SendGrid-style (errors[]) |
/v3/api_keys, /v3/templates, /v3/messages, /v3/stats, /v3/suppression/*, /v3/asm/*, /v3/user/webhooks, /v3/images |
FastAPI-style (detail) |
/api/admin/* |
FastAPI-style (detail) |
/v3/auth/* |
FastAPI-style (detail) |
Your client code should handle both. A typical pattern:
1 2 3 4 5 6 7 8 9 10 | |
HTTP status codes#
| Status | Meaning | When |
|---|---|---|
| 200 OK | Success | Read endpoints, updates |
| 201 Created | Resource created | POST that creates a new entity (API key, template, domain, webhook) |
| 202 Accepted | Queued for processing | POST /v3/mail/send — always 202 on success |
| 204 No Content | Success, no body | Successful DELETE |
| 400 Bad Request | Malformed request | Missing fields, wrong types, logic violations (e.g., both content and template_id) |
| 401 Unauthorized | Missing/invalid credentials | No Authorization header; expired JWT; invalid API key |
| 403 Forbidden | Insufficient permissions | Missing scope; suspended tenant; cross-tenant access |
| 404 Not Found | Resource not found | Unknown message ID; unknown template ID; endpoint path typo |
| 409 Conflict | State conflict | Domain already exists; duplicate role name |
| 413 Payload Too Large | Request body exceeds limit | /v3/mail/send body over 20 MB |
| 422 Unprocessable Entity | Schema validation failed | Pydantic caught a type or format error before business logic |
| 429 Too Many Requests | Rate limited | Too many requests from this API key or tenant |
| 500 Internal Server Error | Unexpected server error | Unhandled exception; file a support ticket with the request_id |
| 503 Service Unavailable | Dependency down | MySQL or Redis unreachable; ScaiKey JWKS endpoint down |
Retry classification#
Not every error is retryable. Retrying a 4xx almost never helps (your request is malformed); retrying a 5xx or 429 often does.
| Code | Retry? | How |
|---|---|---|
| 400 / 422 | No | Fix the request |
| 401 | No | Refresh JWT or rotate API key |
| 403 | No | Get the right scope; don't retry |
| 404 | No | The resource doesn't exist |
| 409 | No | Resolve the state conflict |
| 413 | No | Shrink the payload (typically attachments) |
| 429 | Yes | Honor Retry-After header; exponential backoff if absent |
| 500 | Yes, limited | Max 2–3 attempts, exponential backoff |
| 503 | Yes | Exponential backoff; check service health if persistent |
A safe default: retry 429, 500, and 503, up to 4 attempts total, with exponential backoff + jitter. Do not retry anything else.
Common error scenarios#
Authentication#
1 | |
Add Authorization: Bearer <your-api-key-or-jwt>.
1 | |
Typo, revoked, or deleted key. Check GET /v3/api_keys.
1 | |
Refresh with /v3/auth/refresh.
Authorization#
1 | |
The credential doesn't have the needed scope. Either create a differently scoped key, or assign the missing permission to the user's role.
Validation#
/v3/mail/send:
1 2 3 4 5 6 | |
A request body with multiple problems returns all of them at once. Fix each.
1 2 3 4 5 | |
Dynamic template IDs start with d-. Legacy template IDs aren't valid on /v3/mail/send.
Rate limiting#
1 2 3 4 | |
Wait Retry-After seconds before retrying. See Rate Limiting for the limits per endpoint.
Payload too large#
1 | |
The total serialized JSON (including base64-encoded attachments) must fit under 20 MB. Large attachments eat into that quickly — base64 adds ~33%. For very large files, host them externally and link from the email body.
Not found#
1 | |
The template doesn't exist, or isn't visible to your tenant. Check that the template belongs to your tenant and that the ID is spelled correctly.
Error handling pattern#
A pragmatic retry loop that surfaces a useful error when retry is exhausted:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | |
Wrap it once; use it everywhere.
What's next#
- Error Codes — the exhaustive list of errors by endpoint.
- Rate Limiting — how
429is classified and how to stay under it. - Your First Integration — a complete send loop with error handling.