Error Codes
HTTP status codes and error responses from the MisarMail API
All errors return a JSON body with a success: false field and an error string.
{
"success": false,
"error": "Human-readable error message"
}Validation errors (400) include a details object in Zod flatten format:
{
"success": false,
"error": "Validation failed",
"details": {
"fieldErrors": {
"to": ["Required"]
},
"formErrors": []
}
}details.fieldErrors is a map of field names to arrays of error strings. details.formErrors contains top-level (non-field) validation errors.
Status Code Reference
| Status | Meaning | Common Causes |
|---|---|---|
200 | OK | Request succeeded; email delivered |
202 | Queued | Primary SMTP unavailable; email auto-retried |
400 | Bad Request | Invalid request body or failed validation |
401 | Unauthorized | API key missing, invalid, or revoked |
402 | Payment Required | Insufficient wallet credits for a metered action |
403 | Forbidden | from.email not in your verified accounts, or key lacks required scope |
404 | Not Found | Endpoint doesn't exist |
413 | Request Too Large | Request body exceeds maximum size |
415 | Unsupported Media Type | Content-Type must be application/json |
422 | Suppressed | Send suppressed — recipient unsubscribed or bounced |
429 | Too Many Requests | Rate limit or plan limit reached |
500 | Server Error | Unexpected platform error |
Error Type
Some endpoints (billing, wallet, plan limits, team members, email accounts, segments, and other typed routes) add a machine-readable error_type field alongside error. Branch on error_type rather than parsing the human-readable error string.
{
"success": false,
"error": "Insufficient wallet credits",
"error_type": "insufficient_balance"
}error_type | Typical status | Meaning |
|---|---|---|
invalid_request | 400 / 415 | Malformed body, failed validation, or wrong Content-Type |
authentication_error | 401 | Missing, invalid, or revoked credentials |
insufficient_balance | 402 | Not enough wallet credits for the requested action |
authorization_error | 403 | Authenticated but not permitted (wrong plan, missing scope, or no access) |
internal_error | 500 | Unexpected server-side failure |
error_type is present only on typed routes. The core send and campaign endpoints use the { success, error, details } shape shown above and do not return error_type.
Error Details
401 — Invalid API Key
{
"success": false,
"error": "Invalid or missing API key. Use: Authorization: Bearer msk_..."
}Fix: Check that you're sending the correct msk_... key in the Authorization: Bearer header.
403 — Wrong Sender
{
"success": false,
"error": "'from.email' is not a verified account for this API key"
}Fix: The from.email must exactly match one of the email accounts owned by the API key's user. Create or verify the address in Settings → Email Accounts.
403 — Missing Scope
{
"success": false,
"error": "API key does not have 'send' scope"
}Fix: Revoke the key and create a new one with the required scope checked.
400 — Validation Failed
{
"success": false,
"error": "Validation failed",
"details": {
"fieldErrors": {
"subject": ["Required"],
"to.0.email": ["Invalid email address"]
},
"formErrors": []
}
}Fix: Check details.fieldErrors for per-field error messages.
400 — Inactive SMTP Pool
{
"success": false,
"error": "alias_id not found or pool inactive"
}Fix: The specified alias_id doesn't exist or the SMTP pool is currently paused. Omit alias_id to use automatic pool selection.
413 — Request Too Large
{
"success": false,
"error": "Request body exceeds maximum size"
}Fix: Reduce the size of your request body. Large HTML bodies or base64-encoded attachments are common causes.
415 — Unsupported Media Type
{
"success": false,
"error": "Content-Type must be application/json"
}Fix: Set the Content-Type: application/json header on your request.
422 — Suppressed
{
"success": false,
"suppressed": true,
"error": "Send suppressed — recipient unsubscribed or bounced"
}The suppressed: true flag indicates the send was intentionally blocked because the recipient is on your suppression list (previously unsubscribed or hard-bounced). This protects your sender reputation and deliverability. Do not retry — check the recipient's status via the Contacts API and remove them from future send lists.
429 — Plan Limit Reached
{
"success": false,
"error": "Daily send limit reached. Upgrade at mail.misar.io/pricing"
}Or monthly:
{
"success": false,
"error": "Monthly send limit reached. Upgrade at mail.misar.io/pricing"
}Fix: Upgrade your plan at mail.misar.io/pricing.
202 — Queued (Not an Error)
{
"success": false,
"queued": true,
"message": "Email queued for retry"
}Despite success: false, 202 is not an error. The email will be delivered automatically when the SMTP provider recovers. Do not retry on 202.
Retry Strategy
Do retry
Network errors, 5xx responses, connection timeouts
Do NOT retry
4xx errors (fix the request first), 202 Queued (already handled), 422 Suppressed (intentional block)
Recommended backoff for retryable errors:
async function sendWithRetry(payload: EmailPayload, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch("https://api.misar.io/mail/v1/send", {
method: "POST",
headers: { "Authorization": `Bearer ${process.env.MISARMAIL_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
// 202 = queued — success, stop retrying
if (res.status === 202) return data;
// 422 = suppressed — intentional, stop retrying
if (res.status === 422 && data.suppressed) return data;
// 2xx = success
if (res.ok) return data;
// 4xx = client error — do not retry
if (res.status >= 400 && res.status < 500) {
throw new Error(data.error || `HTTP ${res.status}`);
}
// 5xx — retry with exponential backoff
if (attempt < maxRetries - 1) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
}
}
throw new Error("Max retries exceeded");
}