---
title: Error Codes
path: reference/error-codes
status: published
---

# 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](../concepts/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):**

```json
{
  "errors": [
    {"message": "...", "field": "...", "help": "..."}
  ]
}
```

**FastAPI-style (everywhere else):**

```json
{"detail": "..."}
```

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:

```json
{
  "detail": [
    {"loc": ["body", "personalizations"], "msg": "field required", "type": "value_error.missing"}
  ]
}
```

### 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](../concepts/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_message` on the message record has the exception.
- A `dropped` event is emitted with reason `template_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](../concepts/errors#retry-classification) for recommended retry loops.

## Related

- [Errors (concept)](../concepts/errors)
- [Rate Limiting](../concepts/rate-limiting)
- [Your First Integration](../tutorials/first-integration) — includes a reference retry loop.
