# Errors and rate limits

Every error has the same shape:

```json
{ "error": { "code": "insufficient_credits", "message": "Not enough credits. Top up your balance and try again.", "details": { "available": 12, "required": 40 } } }
```

Rely on `code` (stable), not on `message` (may change).

| HTTP | code | Meaning | What to do |
| --- | --- | --- | --- |
| 400 | `validation_error` | Invalid body/parameter; `details` lists fields | Fix the request. Unknown fields are rejected |
| 400 | `idempotency_key_required` | Missing `Idempotency-Key` on a send | Add a unique UUID per logical send |
| 401 | `unauthorized` | Key missing, wrong, expired or revoked | Check the key |
| 402 | `insufficient_credits` | Not enough credits | Buy a package in the portal |
| 403 | `insufficient_scope` | The key lacks the scope | Create a key with the right scope |
| 403 | `sender_not_verified` | The sender name is not approved for your account | Use a name from `GET /senders` or request one in the portal |
| 403 | `forbidden` / `org_suspended` | No access / account suspended | Contact support |
| 404 | `not_found` | Resource does not exist (or belongs to another account) | — |
| 409 | `idempotency_conflict` | Same key with a different request, or the original is still processing | Use a new key for a new request |
| 422 | `invalid_destination` | No valid destination; see `details.rejected` | Check the numbers |
| 429 | `rate_limited` / `otp_rate_limited` / `daily_limit_exceeded` | Limit exceeded | Wait `retry_after` seconds |
| 500/503 | `internal_error` / `service_unavailable` | Temporary problem | Retry with the same `Idempotency-Key` |

## Delivery failure reasons (`reason`)

`undeliverable`, `network_failure`, `expired`, `kosher_phone` (a "kosher" number that blocks SMS), `send_window` (sending not allowed at this hour), `blocked` (recipient blocked messages — auto-added to your blacklist), `invalid_content`, `no_coverage`, `rejected`, `cancelled`, `temporary_unavailable`, `internal_error`.

Rejection reasons in a send response (`rejected[].reason`): `invalid_phone`, `international_not_allowed`, `blacklisted`, `recently_messaged`, `duplicate`, `body_too_long`, `empty_body`.

## Rate limits

| Kind | Limit per key |
| --- | --- |
| Send (POST) | 60 requests / minute |
| Read (GET) | 600 requests / minute |
| OTP to the same phone | 3 per 10 minutes, 10 per hour |

Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`; a 429 also has `Retry-After`. For high volume use `/messages/batch` (up to 2,500 messages per request) instead of one request per message.

New accounts are limited to 500 messages per day until business details are verified.
