Error Handling
The TopMail API uses standard HTTP status codes and returns consistent error response bodies. This guide covers error formats, common error codes, and retry strategies.
Error Response Format
All error responses follow a consistent JSON format with an error object containing a machine-readable code and a human-readable message.
{"error": {"code": "validation_error","message": "Subject is required","path": "subject"}}
HTTP Status Codes
| Code | Meaning | Description |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource was created successfully |
| 202 | Accepted | Request accepted for async processing (e.g., batch sends) |
| 400 | Bad Request | Invalid request body or parameters |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | Valid API key but insufficient permissions or limit reached |
| 404 | Not Found | Resource does not exist or does not belong to your workspace |
| 409 | Conflict | Duplicate resource or idempotency conflict |
| 429 | Too Many Requests | Rate limit exceeded. Check the Retry-After header. |
| 500 | Server Error | An unexpected error occurred on the server |
Common Error Codes
The code field in the error response provides a machine-readable identifier you can use for programmatic error handling.
| Code | HTTP Status | Description |
|---|---|---|
| validation_error | 400 | Request body failed validation. Check the message and path fields for details. |
| unauthorized | 401 | API key is missing, invalid, or revoked. |
| missing_from_email | 400 | No from_email provided and no default configured in workspace settings. |
| domain_invalid | 403 | Sending domain is not verified or DKIM is not enabled. |
| template_not_found | 404 | The specified template_id does not exist in your workspace. |
| contact_not_found | 404 | No contact found with the given email or contact_id. |
| not_found | 404 | The requested resource does not exist. |
| workspace_not_found | 404 | The workspace associated with the API key was not found. |
| duplicate | 409 | A resource with the same identifier already exists. |
| limit_reached | 403 | Email sending limit for your plan has been reached. |
| rate_limited | 429 | Too many requests. Retry after the duration in the Retry-After header. |
| invalid_schedule | 400 | send_at must be in the future and within 72 hours. |
| schedule_failed | 500 | Failed to enqueue the scheduled email for delivery. |
| no_content | 400 | No email content provided (html, text, or template_id required). |
| internal_error | 500 | An unexpected server error occurred. Safe to retry with backoff. |
Retry Strategy
Not all errors should be retried. Use the following guidelines to determine when and how to retry failed requests.
Safe to Retry
- 429 Too Many Requests -- Wait for the duration specified in the
Retry-Afterheader, then retry. - 500 Server Error -- Retry with exponential backoff (e.g., 1s, 2s, 4s, 8s). Use an
Idempotency-Keyheader to prevent duplicate sends. - Network errors / timeouts -- Retry with backoff. Always include an idempotency key for send endpoints.
Do Not Retry
- 400 Bad Request -- Fix the request body or parameters before retrying.
- 401 Unauthorized -- Check your API key is correct and has not been revoked.
- 403 Forbidden -- The API key lacks permission, the domain is invalid, or you have reached your plan limit.
- 404 Not Found -- The resource does not exist. Verify the ID or create the resource first.
- 409 Conflict -- A duplicate resource exists. This is typically not an error condition.
Exponential Backoff Example
# Retry with exponential backoff using a loopMAX_RETRIES=3ATTEMPT=0while [ $ATTEMPT -le $MAX_RETRIES ]; doRESPONSE=$(curl -s -w "\n%{http_code}" \https://api.topmail.so/api/v1/email/send \-H "Authorization: Bearer tm_live_abc123" \-H "Content-Type: application/json" \-d '{"to":"user@example.com","subject":"Hello","html":"<p>Hi</p>"}')HTTP_CODE=$(echo "$RESPONSE" | tail -1)if [ "$HTTP_CODE" -lt 400 ]; then break; fiif [ "$HTTP_CODE" -lt 500 ] && [ "$HTTP_CODE" -ne 429 ]; then break; fiATTEMPT=$((ATTEMPT + 1))sleep $((2 ** ATTEMPT))done