Basics
Errors & rate limits
Errors use conventional HTTP status codes and a small, stable JSON body. Branch on the code field, not on the message.
Error format
Every non-2xx response has a JSON body with a machine-readable code and a human-readable message:
Error response
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "invalid_parameter",
"message": "sender.email is missing or invalid"
}Messages may change over time and are meant for logs and humans. Codes are part of the API contract.
Status codes
| Status | Code | Meaning |
|---|---|---|
200 / 201 / 204 | — | Success. Sends and creates return 201 Created; contact updates/deletes return 204 No Content. |
400 | invalid_parameter | The body is not valid JSON, a required field is missing, an address is invalid, the sender domain is not verified, the template is inactive or has a syntax error. |
400 | duplicate_parameter | POST /v3/contacts was called for an existing email without updateEnabled: true. |
401 | unauthorized | Missing, unknown or revoked API key. |
402 | quota_exceeded | The brand's monthly email quota is used up. Upgrade the plan or buy an add-on pack. |
403 | permission_denied | The API key lacks the permission this endpoint requires. |
403 | email_not_verified | The brand owner has not verified their email address yet. |
403 | account_suspended | Sending is disabled for this brand. |
404 | document_not_found | Unknown templateId or message id. |
409 | whatsapp_not_connected | WhatsApp send attempted before a number was connected. |
422 | outside_service_window | WhatsApp free-form text sent outside the 24-hour customer service window. |
429 | too_many_requests | Rate limit exceeded. Wait for Retry-After seconds. |
5xx | internal_error | Something went wrong on our side. Safe to retry with backoff. |
Rate limits
Each brand has a token-bucket rate limit shared by all of its API keys and the SMTP relay. Every API request takes one token; the bucket refills continuously at your plan's rate.
| Plan | Requests per second | Monthly emails |
|---|---|---|
| Starter | 15/s | 40,000 |
| Business | 50/s | 150,000 |
| Enterprise | 150/s | 600,000 |
When the bucket is empty the API answers with 429 and a Retry-After header (in seconds):
429 response
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json
{"code": "too_many_requests", "message": "Rate limit exceeded"}Over SMTP, the same limit is reported as the temporary error 451 4.7.1; standard mail clients retry automatically.
Retrying
- Retry
429after the number of seconds inRetry-After, and5xxor network errors with exponential backoff plus jitter. - Do not retry other
4xxerrors unchanged — fix the request (or the account state) first. - To send many messages at once, prefer
messageVersions(up to 1000 per request) over one request per recipient; it uses a single rate-limit token.
# curl retries 429 and 5xx responses on its own and honours Retry-After
curl -X POST 'https://app.snappymails.com/v3/smtp/email' \
--retry 5 --retry-max-time 60 \
-H "api-key: $SNAPPYMAILS_API_KEY" \
-H 'Content-Type: application/json' \
-d @message.jsonQuota vs. rate limit
The rate limit (
429) is about speed and recovers within seconds. The quota (402) is about volume and only resets with the next billing month or a plan change, so retrying will not help.