API Reference
Errors
The error envelope, common codes, and how to handle them.
Every error response from the API uses the same shape. One handler, three channels.
The error envelope
{
"error": {
"code": "invalid_phone",
"message": "Recipient must be a phone number in international format, e.g. +2348012345678.",
"param": "to",
"type": "validation_error",
"request_id": "req_01HXYZ..."
}
}
| Field | Meaning |
|---|---|
code | Stable, machine-readable identifier. Switch on this in your code. |
message | Human-readable explanation. Safe to show to developers. |
param | The request field at fault, when applicable. |
type | High-level category: validation_error, auth_error, etc. |
request_id | Unique id for this request. Include it when you contact support. |
Status codes
| Status | Type | Example codes |
|---|---|---|
400 | validation_error | invalid_phone, invalid_email, body_too_long |
401 | auth_error | missing_api_key, invalid_api_key |
403 | permission_error | key_domain_scope, team_suspended |
404 | not_found | template_not_found, domain_not_found |
409 | conflict | template_channel_mismatch |
422 | validation_error | missing_variable, unknown_variable |
429 | rate_limited | rate_limited |
500 | internal_error | internal_error |
Common error codes
invalid_phone
The to field isn’t a valid phone number in E.164 format.
E.164 means international form with a leading + and country
code, no spaces or dashes: +2348012345678.
invalid_email
The to field isn’t a valid email address.
template_not_found
The template_id you passed doesn’t exist on this team, or
isn’t enabled. Check that the template hasn’t been archived.
wrong_template_channel
You sent a template through the wrong endpoint — e.g. an SMS
template via POST /v1/emails. Send it through the matching
/v1/{channel} endpoint.
missing_variable / unknown_variable
Your variables object doesn’t match the template’s variable
manifest. The param field names the offending key.
no_approved_brand_domain
You’re in live mode but no verified domain on the team is approved yet. Add a verified domain and complete the brand profile.
rate_limited
You exceeded the rate limit. The response includes a
Retry-After header (seconds) telling you when to retry.
See Rate limits.
internal_error
Something went wrong on our side. Include the request_id
when you contact support. These errors are safe to retry with
exponential backoff.
Handling errors
A reasonable client retries 429 and 5xx with exponential
backoff and surfaces everything else to the developer:
async function send(payload: SendInput, attempt = 0): Promise<Sent> {
const res = await fetch('https://api.zevsend.com/v1/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZEVSEND_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (res.ok) return res.json();
const body = await res.json();
const retryable = res.status === 429 || res.status >= 500;
if (retryable && attempt < 5) {
const wait =
res.status === 429
? Number(res.headers.get('Retry-After') ?? 1) * 1000
: 2 ** attempt * 250;
await new Promise((r) => setTimeout(r, wait));
return send(payload, attempt + 1);
}
throw new Error(`${body.error.code}: ${body.error.message}`);
} Updated at, Thursday, October 1, 2026