API Documentation
Error codes
The complete reference: HTTP status codes, the RFC 9457 Problem Details response shape, application-level error codes with operational meaning, the SMTP enhanced status codes that arrive in delivery events, and the retry decision matrix that says exactly which errors are retryable and which are not.
Response shape
Every error response from the API follows the RFC 9457 Problem Details format (the 2023 update to RFC 7807, with the same wire format and clarified semantics). The shape is consistent across every endpoint, every error class, every HTTP method — you write one error handler in your client and it works everywhere.
The five RFC 9457 fields (type, title, status, detail, instance) are always present. Three extension fields matter operationally:
code— the machine-readable application error identifier. This is what your code branches on, not the human-readabletitle.titlecan change between releases for clarity;codeis part of the API contract and is stable.request_id— unique identifier for this specific request. Always include this when contacting support — it lets us find the exact log entry for the failure in milliseconds rather than searching by timestamp.errorsarray — for validation errors, the API returns every validation problem at once, not the first one it found. A request with three invalid fields produces one response listing all three, not three round-trips fixing one field at a time.
application/problem+json media type, not application/json. HTTP clients that distinguish between the two (newer .NET, Spring Boot, AWS SDKs) get RFC-compliant deserialisation automatically. Clients that only check status codes still work fine.HTTP status codes
The status code communicates the category of failure. The code field disambiguates within a category. As a rule, the status code tells you who needs to fix the problem; the application code tells you what to fix.
| Status | Meaning | Who fixes it |
|---|---|---|
| 200 OK | Successful request returning data | — |
| 201 Created | Successful resource creation (POST) | — |
| 202 Accepted | Request accepted for asynchronous processing (e.g., webhook delivery) | — |
| 204 No Content | Successful operation with no response body (e.g., DELETE) | — |
| 400 Bad Request | Malformed JSON, missing required header, or syntactically invalid request | Client — fix request structure |
| 401 Unauthorized | Missing or invalid API key. The Bearer token is wrong, expired, or revoked | Client — check credentials |
| 403 Forbidden | Valid key but insufficient scope for this endpoint | Client — use correctly scoped key |
| 404 Not Found | The requested resource does not exist (or is invisible to this key) | Client — verify resource ID |
| 405 Method Not Allowed | Endpoint exists but does not support this HTTP method | Client — check method |
| 409 Conflict | Request conflicts with current state (e.g., duplicate idempotency key with different payload) | Client — resolve conflict |
| 413 Payload Too Large | Message exceeds the 25 MB hard limit on body size | Client — split or shrink payload |
| 415 Unsupported Media Type | Content-Type is not application/json (or not what the endpoint accepts) | Client — set correct header |
| 422 Unprocessable Entity | Syntactically valid JSON but semantically invalid (missing field, bad email, invalid IP pool name) | Client — fix payload semantics |
| 429 Too Many Requests | Rate limit exceeded; see Retry-After header | Client — back off, retry |
| 500 Internal Server Error | Unhandled server-side condition. Includes a request_id for support | Server — retry with backoff |
| 502 Bad Gateway | Upstream component (PowerMTA, queue) returned an invalid response | Server — retry with backoff |
| 503 Service Unavailable | Service is temporarily overloaded or in maintenance. Includes Retry-After | Server — retry with backoff |
| 504 Gateway Timeout | Upstream component did not respond within the timeout window | Server — retry with backoff |
Application error codes
The code field in the response identifies the specific problem. These codes are stable across API versions; messages and titles may evolve, but the codes are the contract. Branch on these in your error handlers, not on the human-readable strings.
| Code | HTTP status | Meaning |
|---|---|---|
| invalid_request | 400 | Malformed JSON or missing required headers (Content-Type, Authorization) |
| invalid_api_key | 401 | API key missing, malformed, expired, or revoked |
| scope_insufficient | 403 | Key valid but does not have the scope required for this operation |
| key_disabled | 403 | Key administratively disabled (security event, account-level action) |
| resource_not_found | 404 | The requested message ID, suppression entry, or pool does not exist |
| duplicate_idempotency_key | 409 | Same idempotency key with a different request body (cached request differs) |
| message_too_large | 413 | Total message size including attachments exceeds 25 MB |
| invalid_email_address | 422 | One or more recipient or sender addresses failed RFC 5321 syntax validation |
| sender_not_verified | 422 | The From domain has not completed SPF/DKIM verification in the dashboard |
| recipient_suppressed | 422 | One or more recipients are on the suppression list (the response lists which) |
| ip_pool_not_found | 422 | The specified pool name does not exist on the account |
| scheduling_window_exceeded | 422 | send_at scheduled more than 72 hours in the future |
| invalid_attachment | 422 | Attachment failed validation (executable extension, invalid MIME, corrupted base64) |
| tracking_disabled | 422 | Open/click tracking requested but disabled at the account level |
| rate_limit_exceeded | 429 | Per-key or per-account rate limit hit. Retry-After header included |
| internal_error | 500 | Unexpected server-side error. Always includes a request_id |
| upstream_error | 502 | PowerMTA queue, suppression service, or webhook dispatcher returned an error |
| service_overloaded | 503 | Backpressure from the queue layer. Retry with exponential backoff |
| maintenance | 503 | Scheduled maintenance in progress. Retry-After indicates expected completion |
SMTP error codes (delivery events)
SMTP error codes are what receiving mail servers return when our PowerMTA infrastructure attempts delivery. These are not API errors — they show up in webhook delivery events (message.bounced, message.deferred) as the smtp_code and smtp_status fields. Understanding them is core operational literacy because they tell you exactly why an individual message failed and what to do about it.
The structure follows RFC 3463: a three-digit code (the legacy SMTP code, e.g. 550) plus an enhanced status of the form X.Y.Z (e.g. 5.1.1). The first digit of the code is the most important: 4xx is temporary, 5xx is permanent.
4xx codes — soft bounces (transient, retryable)
| Code | Status | Operational meaning |
|---|---|---|
| 421 | 4.7.0 | Service not available, closing channel. Often greylisting; retry honoured automatically |
| 421 | 4.7.28 | Suspicious sending pattern detected. Often a reputation warning before harder block |
| 450 | 4.2.1 | Mailbox temporarily disabled (vacation, suspension) |
| 451 | 4.3.0 | Local error in processing — receiving server problem, will resolve |
| 451 | 4.7.500 | Microsoft 365 throttling: temporarily rate-limited by destination |
| 452 | 4.2.2 | Mailbox full. Often resolves; can become 5.2.2 if persistent |
| 452 | 4.5.3 | Too many recipients in the envelope; split the send |
5xx codes — hard bounces (permanent, suppression triggered)
| Code | Status | Operational meaning |
|---|---|---|
| 550 | 5.1.1 | Mailbox does not exist. The most common hard bounce. Address is invalid; suppress. |
| 550 | 5.1.2 | Domain does not exist (DNS lookup failed). Often a typo in the address |
| 550 | 5.1.10 | Address rejected by recipient policy. May be temporary (employee on leave) |
| 550 | 5.2.1 | Mailbox disabled. Address known but no longer accepts mail. Suppress |
| 550 | 5.7.1 | Message blocked by content filter or sender reputation. Investigate before resending |
| 550 | 5.7.26 | Authentication failure. SPF, DKIM, or DMARC failed on the receiving side |
| 550 | 5.7.28 | Sending IP listed on the receiver's local blocklist |
| 551 | 5.1.6 | User has moved; new address available in response (rarely actioned automatically) |
| 552 | 5.2.3 | Message exceeds receiver's size limit |
| 553 | 5.1.7 | From address does not pass syntactic validation at receiver |
| 554 | 5.7.1 | Message rejected for policy reasons. Receiver-specific; check the response text |
Retry decision matrix
The single most useful diagnostic table in this document. For each error class, this is whether retrying makes sense and under what strategy. Hardcoding this matrix into your error handler eliminates the most common cause of cascading failures: retrying things that will never succeed.
| Status | Retry? | Strategy |
|---|---|---|
| 400 Bad Request | NO | The request structure is broken; fix it. Retry produces the same 400. |
| 401 Unauthorized | NO | Credential is wrong. Retry storms can trigger lockout (50 401s in a window = IP throttled). |
| 403 Forbidden | NO | Scope insufficient. Use a correctly scoped key. |
| 404 Not Found | NO | Resource does not exist. The next request will get the same answer. |
| 409 Conflict | IF FIXED | Idempotency key conflict; retry only after resolving the conflict in the request body. |
| 413 / 415 / 422 | NO | Payload validation failed. Fix the payload, then send a different request. |
| 429 Too Many Requests | YES | Wait Retry-After seconds (exponential backoff with jitter as fallback). Cap at 5 retries. |
| 500 / 502 | YES | Exponential backoff with jitter starting at 1s. Cap at 5 retries; surface the error after. |
| 503 Service Unavailable | YES | Retry-After indicates the expected wait. Cap at 5 retries. |
| 504 Gateway Timeout | YES | Same backoff as 500. Use idempotency keys to avoid duplicate side effects on retry. |
Idempotency keys
Network failures during retry can produce duplicate side effects unless the API supports idempotency. Send a charge twice; charge twice. Send a transactional email twice; deliver twice. The pattern that prevents this is the idempotency key: a client-generated identifier that the server uses to deduplicate requests.
The Cloud Server for Email API supports the Idempotency-Key header on every state-changing endpoint (POST, DELETE). Generate a key per logical operation, store it client-side, and include it on the request and any retry. The server caches the response for 24 hours; any retry with the same key returns the cached response without producing a duplicate effect.
Three rules for using idempotency keys correctly:
- One key per logical operation. The same payment attempt, the same password reset, the same notification — one key. A new operation gets a new key.
- Keys must be unpredictable to attackers. A UUID v4 is the standard choice. Sequential or guessable keys are a security problem.
- Send the same request body on retry. If the server sees the same key with a different body, it returns 409 Conflict (
duplicate_idempotency_key). The semantics are: same key + same body = idempotent retry; same key + different body = client bug.
Worked examples
Two complete error scenarios and the correct client response to each. The same patterns apply across the rest of the error space.
Validation error: invalid recipient
You POST a message with an invalid to address.
Correct response: Do not retry. The status is 422, the code is invalid_email_address, and the field is to. Fix the address upstream (validate at form-submission time, not at send time) or skip this recipient. Logging the request_id makes troubleshooting easier if the same address shows up again.
Transient server error: 503 Service Unavailable
You POST a message during a brief queue overload.
Correct response: Wait 5 seconds (or however the Retry-After header indicates), then retry with the same Idempotency-Key from the original request. Cap retries at 5; if all fail, surface the error to the caller and do not silently drop the request.
Common pitfalls
Patterns that recur in production incidents and support tickets. Each is preventable; each is preventable in less code than you think.
- Branching on the human-readable message instead of the code. The
titleanddetailfields are for humans; they may be reworded between releases. Branch on thecodefield, which is contract. - Treating all 4xx as retryable. Only 429 in the 4xx range is retryable. Retrying 401 produces a lockout. Retrying 422 produces the same validation failure forever. Retrying 404 confirms that the resource still does not exist.
- Treating 5xx as a permanent failure. Server-side errors are usually transient. Backoff with jitter and retry up to 5 times before surfacing the error.
- Not capturing the request_id. When you contact support about a failure, the first thing we ask for is the request_id. If you have not logged it, the support call gets longer.
- Polling for retry timing instead of reading Retry-After. The header tells you exactly how long to wait. Polling at fixed intervals during a throttle wastes rate-limit budget you would rather use on real requests.
- Dropping errors silently. When the retry cap is exhausted, surface the error to the caller. Silent drops produce silent data loss; the caller needs to know that the operation did not complete so they can decide what to do.
- Implementing custom error formats instead of using the response shape. The Problem Details format is consistent across every endpoint. Building a custom error wrapper around it complicates testing without adding value. Trust the format.
- Using the same idempotency key across distinct operations. The key identifies one logical operation. Reusing it returns a cached response from the prior operation, which is rarely what you intended.
- Skipping idempotency keys on transactional sends. The cost of an idempotency key is a UUID; the cost of a duplicate transactional email is a customer support ticket. Use them.