All API errors follow a consistent JSON envelope:
{
"error": {
"code": "error_code",
"message": "Human-readable description of what went wrong",
"details": { },
"suggestion": "Actionable advice to fix the issue"
}
}
| Field | Type | Always present | Description |
|---|
code | string | ✅ | Machine-readable error identifier |
message | string | ✅ | Human-readable description |
details | object | — | Additional context (validation errors, etc.) |
suggestion | string | — | Recommended fix |
Error codes reference
Authentication errors (401)
| Code | Description | Suggestion |
|---|
invalid_api_key | API key is missing, malformed, or not recognized | Check your key is correct and includes the full string |
expired_api_key | API key has passed its expiration date | Generate a new API key from your dashboard |
revoked_api_key | API key has been revoked | Generate a new API key from your dashboard |
Authorization errors (403)
| Code | Description | Suggestion |
|---|
insufficient_permissions | API key lacks the required scope | Use a key with the required scope (e.g. email:send) |
account_suspended | Your organization account is suspended | Contact support |
Validation errors (400)
| Code | Description | Suggestion |
|---|
invalid_request | Request body or parameters failed validation | Check the request against the API docs |
invalid_email | Email address format is invalid | Verify the email address format |
invalid_domain | Domain name format is invalid | Check the domain name |
missing_required_field | A required field is missing | Include all required fields |
validation_failed | Business logic validation failed | Check details for specifics |
Not found errors (404)
| Code | Description |
|---|
email_not_found | Email ID does not exist or is not accessible |
domain_not_found | Domain ID does not exist or is not accessible |
organization_not_found | Organization not found |
api_key_not_found | API key resource not found |
resource_not_found | Generic — the requested resource does not exist |
Business logic errors (422)
| Code | Description | Suggestion |
|---|
domain_not_verified | The sending domain is not yet verified | Verify your domain first (see Domain Setup) |
recipient_suppressed | Recipient is on the suppression list | Check Suppressions |
invalid_content | Email content flagged by content filter | Review your email content for spam triggers |
attachment_too_large | Attachment exceeds the maximum size | Keep attachments under the size limit |
too_many_recipients | More than 50 recipients in to, cc, or bcc | Reduce recipients per request or use batch send |
Rate limiting (429)
| Code | Description | Suggestion |
|---|
rate_limit_exceeded | Too many requests in the current window | Wait until X-RateLimit-Reset and retry |
quota_exceeded | Account quota (e.g. daily sends) exceeded | Upgrade your plan or wait for the next billing period |
Server errors (5xx)
| Code | HTTP | Description |
|---|
internal_error | 500 | Unexpected server error |
database_error | 500 | Database connectivity issue |
provider_error | 502 | Upstream email provider failure |
provider_timeout | 502 | Upstream email provider timed out |
Validation error details
When code is invalid_request, the details object contains per-field validation errors:
{
"error": {
"code": "invalid_request",
"message": "Request validation failed",
"details": {
"validation": {
"subject": ["Subject is required"],
"to": ["Must include at least one recipient"],
"html": ["Either html or text body is required"]
}
},
"suggestion": "Check the request body against the API documentation"
}
}
Handling errors in code
const response = await fetch("https://api.mail.gorillaa.one/v1/emails", {
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify(emailData),
});
if (!response.ok) {
const { error } = await response.json();
switch (error.code) {
case "rate_limit_exceeded":
const resetAt = response.headers.get("X-RateLimit-Reset");
// Wait and retry
break;
case "recipient_suppressed":
// Remove recipient from your mailing list
break;
case "domain_not_verified":
// Alert admin to verify the domain
break;
default:
console.error(`API error: ${error.code} — ${error.message}`);
}
}
import requests
response = requests.post(
"https://api.mail.gorillaa.one/v1/emails",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json=email_data,
)
if not response.ok:
error = response.json()["error"]
if error["code"] == "rate_limit_exceeded":
reset_at = response.headers.get("X-RateLimit-Reset")
# Wait and retry
elif error["code"] == "recipient_suppressed":
# Remove recipient from your mailing list
pass
elif error["code"] == "domain_not_verified":
# Alert admin to verify the domain
pass
else:
print(f"API error: {error['code']} — {error['message']}")
Retry strategy
For transient errors (5xx, 429), use exponential backoff with jitter:
async function sendWithRetry(emailData, apiKey, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch("https://api.mail.gorillaa.one/v1/emails", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
"Content-Type": "application/json",
"X-Idempotency-Key": emailData.idempotencyKey,
},
body: JSON.stringify(emailData),
});
if (response.ok) return response.json();
const status = response.status;
if (status >= 400 && status < 500 && status !== 429) {
// Client error — don't retry
throw new Error(`Client error: ${status}`);
}
// Exponential backoff with jitter
const delay = Math.min(1000 * 2 ** attempt, 30000);
const jitter = delay * 0.5 * Math.random();
await new Promise((r) => setTimeout(r, delay + jitter));
}
throw new Error("Max retries exceeded");
}
Always include an X-Idempotency-Key when retrying send requests. This ensures retries don’t result in duplicate emails.