Error Codes

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.

HTTP/1.1 422 Unprocessable Entity Content-Type: application/problem+json { "type": "https://api.cloudserverforemail.com/errors/invalid_email_address", "title": "Invalid email address", "status": 422, "detail": "The 'to' address 'not-an-email' failed RFC 5321 validation.", "instance": "/v1/messages", // Extension fields specific to this API "code": "invalid_email_address", "field": "to", "request_id": "req_01HV8K2P9F3MZQX", "errors": [ {"field": "to", "issue": "Must be a valid RFC 5321 address"} ] }

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-readable title. title can change between releases for clarity; code is 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.
  • errors array — 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.
Content-Type: application/problem+json. The Content-Type on error responses is the IANA-registered 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.

StatusMeaningWho fixes it
200 OKSuccessful request returning data
201 CreatedSuccessful resource creation (POST)
202 AcceptedRequest accepted for asynchronous processing (e.g., webhook delivery)
204 No ContentSuccessful operation with no response body (e.g., DELETE)
400 Bad RequestMalformed JSON, missing required header, or syntactically invalid requestClient — fix request structure
401 UnauthorizedMissing or invalid API key. The Bearer token is wrong, expired, or revokedClient — check credentials
403 ForbiddenValid key but insufficient scope for this endpointClient — use correctly scoped key
404 Not FoundThe requested resource does not exist (or is invisible to this key)Client — verify resource ID
405 Method Not AllowedEndpoint exists but does not support this HTTP methodClient — check method
409 ConflictRequest conflicts with current state (e.g., duplicate idempotency key with different payload)Client — resolve conflict
413 Payload Too LargeMessage exceeds the 25 MB hard limit on body sizeClient — split or shrink payload
415 Unsupported Media TypeContent-Type is not application/json (or not what the endpoint accepts)Client — set correct header
422 Unprocessable EntitySyntactically valid JSON but semantically invalid (missing field, bad email, invalid IP pool name)Client — fix payload semantics
429 Too Many RequestsRate limit exceeded; see Retry-After headerClient — back off, retry
500 Internal Server ErrorUnhandled server-side condition. Includes a request_id for supportServer — retry with backoff
502 Bad GatewayUpstream component (PowerMTA, queue) returned an invalid responseServer — retry with backoff
503 Service UnavailableService is temporarily overloaded or in maintenance. Includes Retry-AfterServer — retry with backoff
504 Gateway TimeoutUpstream component did not respond within the timeout windowServer — retry with backoff
4xx errors are about the request you made. Retrying without changing anything will produce the same 4xx every time — you are not the victim of a transient condition, you are the cause. The exception is 429 (rate limit), which is transient by nature and resolves on its own. 5xx errors are about us. Retrying with backoff is the right behaviour, because the condition is server-side and may resolve before your next attempt.

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.

CodeHTTP statusMeaning
invalid_request400Malformed JSON or missing required headers (Content-Type, Authorization)
invalid_api_key401API key missing, malformed, expired, or revoked
scope_insufficient403Key valid but does not have the scope required for this operation
key_disabled403Key administratively disabled (security event, account-level action)
resource_not_found404The requested message ID, suppression entry, or pool does not exist
duplicate_idempotency_key409Same idempotency key with a different request body (cached request differs)
message_too_large413Total message size including attachments exceeds 25 MB
invalid_email_address422One or more recipient or sender addresses failed RFC 5321 syntax validation
sender_not_verified422The From domain has not completed SPF/DKIM verification in the dashboard
recipient_suppressed422One or more recipients are on the suppression list (the response lists which)
ip_pool_not_found422The specified pool name does not exist on the account
scheduling_window_exceeded422send_at scheduled more than 72 hours in the future
invalid_attachment422Attachment failed validation (executable extension, invalid MIME, corrupted base64)
tracking_disabled422Open/click tracking requested but disabled at the account level
rate_limit_exceeded429Per-key or per-account rate limit hit. Retry-After header included
internal_error500Unexpected server-side error. Always includes a request_id
upstream_error502PowerMTA queue, suppression service, or webhook dispatcher returned an error
service_overloaded503Backpressure from the queue layer. Retry with exponential backoff
maintenance503Scheduled 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)

CodeStatusOperational meaning
4214.7.0Service not available, closing channel. Often greylisting; retry honoured automatically
4214.7.28Suspicious sending pattern detected. Often a reputation warning before harder block
4504.2.1Mailbox temporarily disabled (vacation, suspension)
4514.3.0Local error in processing — receiving server problem, will resolve
4514.7.500Microsoft 365 throttling: temporarily rate-limited by destination
4524.2.2Mailbox full. Often resolves; can become 5.2.2 if persistent
4524.5.3Too many recipients in the envelope; split the send

5xx codes — hard bounces (permanent, suppression triggered)

CodeStatusOperational meaning
5505.1.1Mailbox does not exist. The most common hard bounce. Address is invalid; suppress.
5505.1.2Domain does not exist (DNS lookup failed). Often a typo in the address
5505.1.10Address rejected by recipient policy. May be temporary (employee on leave)
5505.2.1Mailbox disabled. Address known but no longer accepts mail. Suppress
5505.7.1Message blocked by content filter or sender reputation. Investigate before resending
5505.7.26Authentication failure. SPF, DKIM, or DMARC failed on the receiving side
5505.7.28Sending IP listed on the receiver's local blocklist
5515.1.6User has moved; new address available in response (rarely actioned automatically)
5525.2.3Message exceeds receiver's size limit
5535.1.7From address does not pass syntactic validation at receiver
5545.7.1Message rejected for policy reasons. Receiver-specific; check the response text
The 550 5.1.1 cliff. By volume, 550 5.1.1 ("mailbox does not exist") accounts for over half of all hard bounces in production. This is the cleanest signal of a bad address; the suppression engine adds it to the list automatically. If you see your 550 5.1.1 rate above 0.5% of sends, the upstream issue is list quality — verify before send, do not try to clean up after.

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.

StatusRetry?Strategy
400 Bad RequestNOThe request structure is broken; fix it. Retry produces the same 400.
401 UnauthorizedNOCredential is wrong. Retry storms can trigger lockout (50 401s in a window = IP throttled).
403 ForbiddenNOScope insufficient. Use a correctly scoped key.
404 Not FoundNOResource does not exist. The next request will get the same answer.
409 ConflictIF FIXEDIdempotency key conflict; retry only after resolving the conflict in the request body.
413 / 415 / 422NOPayload validation failed. Fix the payload, then send a different request.
429 Too Many RequestsYESWait Retry-After seconds (exponential backoff with jitter as fallback). Cap at 5 retries.
500 / 502YESExponential backoff with jitter starting at 1s. Cap at 5 retries; surface the error after.
503 Service UnavailableYESRetry-After indicates the expected wait. Cap at 5 retries.
504 Gateway TimeoutYESSame backoff as 500. Use idempotency keys to avoid duplicate side effects on retry.
# Pseudocode for the canonical retry handler def should_retry(status_code): if status_code == 429: return True if 500 <= status_code <= 599: return True return False # 4xx errors are not retryable as-is def retry_with_backoff(request, max_attempts=5): for attempt in range(max_attempts): response = send(request) if response.status < 400: return response # success if not should_retry(response.status): raise raise_for_status(response) # client must fix wait = response.retry_after or jitter_backoff(attempt) sleep(wait) raise MaxRetriesExceeded(response)

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.

curl -X POST https://api.cloudserverforemail.com/v1/messages \ -H "Authorization: Bearer $CSE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: msg_pay_2026-05-09_user-1234" \ -d '{ "from": "...", "to": "...", "subject": "...", ... }'

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.
When idempotency matters most. Transactional sends in financial flows (payment confirmations, password resets, account verifications) are the highest-stakes case. Sending one email twice is a customer support call; not sending the email at all is a frozen user account. Idempotency keys make retry safe enough that you can be aggressive about retrying on transient failures, instead of having to choose between duplicate sends and silent drops.

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.

POST /v1/messages Content-Type: application/json Authorization: Bearer cse_live_... { "from": "alerts@yourdomain.com", "to": "not-a-real-email", "subject": "Welcome", "html": "<p>Hi</p>" }
HTTP/1.1 422 Unprocessable Entity Content-Type: application/problem+json { "type": "https://api.cloudserverforemail.com/errors/invalid_email_address", "title": "Invalid email address", "status": 422, "detail": "The 'to' address 'not-a-real-email' failed RFC 5321 validation.", "code": "invalid_email_address", "field": "to", "request_id": "req_01HV8K2P9F3MZQX" }

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.

HTTP/1.1 503 Service Unavailable Retry-After: 5 Content-Type: application/problem+json { "type": "https://api.cloudserverforemail.com/errors/service_overloaded", "title": "Service temporarily overloaded", "status": 503, "detail": "The send queue is at capacity; retry after the indicated delay.", "code": "service_overloaded", "retry_after_seconds": 5, "request_id": "req_01HV8K2P9F3MZQY" }

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 title and detail fields are for humans; they may be reworded between releases. Branch on the code field, 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.