Reference
Errors
Every error has the same shape, modeled on Resend's: the HTTP status, a stable machine-readable name, and a message written for the developer reading the logs.
Error shape#
{ "statusCode": 422, "name": "validation_error", "message": "to must be an email address like \"sam@example.com\" or \"Sam <sam@example.com>\".", "details": { "issues": [{ "path": "to", "message": "to must be an email address like \"sam@example.com\" or \"Sam <sam@example.com>\"." }] }}NameDescription
- statusCodenumberRequired
- Same as the HTTP status.
- namestringRequired
- Stable identifier. Branch on this, never on the message.
- messagestringRequired
- What went wrong and how to fix it. Wording can change.
- detailsobject
- Extra data when there is some: issues, reasons, retry_after, index.
Every response, error or not, carries an X-Request-Id header. Include it when you contact support and we can find the exact request.
All errors#
name · StatusDescription
- invalid_request400 / 413
- The request itself is malformed: not JSON, wrong Content-Type, a bad Idempotency-Key, or a body over 10 MB.
- validation_error422
- A field is missing or invalid. details.issues lists each problem with its path. A disposable (throwaway) email address is one too: details.reason is "disposable_email" when adding a contact, and details.disposable_recipients lists them when sending.
- missing_api_key401
- No Authorization header. Send Authorization: Bearer mm_live_…
- invalid_api_key401
- The key is malformed, doesn't exist or was revoked.
- restricted_api_key403
- The key lacks the permission this endpoint needs, or the option (like skip_double_opt_in or permanent=true) needs a full-access key.
- browser_request_blocked403
- The request came from a web page. Call the API from your server.
- forbidden403
- The workspace can't do this right now, e.g. it's suspended.
- not_found404
- The resource doesn't exist, belongs to another workspace, or is outside the key's audiences.
- method_not_allowed405
- Wrong HTTP method for this path. The Allow header lists the right ones.
- conflict409
- The change clashes with existing data, e.g. pushing over a template built in the editor.
- invalid_idempotent_request409
- This Idempotency-Key was already used for a different request.
- concurrent_idempotent_requests409
- A request with this Idempotency-Key is still running. Retry in a moment.
- invalid_from_address422
- The from domain isn't verified in this workspace.
- sending_blocked422
- The send was refused by a safety check, or our automatic review held an earlier copy of this email. details.reasons says why. Change the email and send it again.
- workspace_paused423
- Sending is paused for this workspace (high bounces, complaints or abuse reports, or while the account is reviewed). Contacts and templates still work.
- rate_limit_exceeded429
- Too many requests. Wait for Retry-After seconds.
- quota_exceeded429
- A plan or workspace limit was reached, like Free's 100 API emails a day. details says which (period, limit, used) and Retry-After says when it resets. Don't retry before then.
- plan_required402
- Your plan doesn't include this, e.g. pushing templates or tracking events on Free, or a Free workspace already has its 500 subscribed contacts (details.limit is "contacts"). details.required_plan names the cheapest plan that does.
- application_error500
- Something broke on our side. Retrying is safe with an Idempotency-Key.
- service_unavailable503
- A dependency is briefly unavailable. Retry with backoff.
Handling errors with the SDK#
SDK methods resolve to { data, error } and never throw for API errors. Network failures and timeouts come back the same way, with statusCode: null and the name network_error or timeout.
const { data, error } = await mm.emails.send(email);
if (error) { switch (error.name) { case "validation_error": case "invalid_from_address": // A bug on our side: fix the request, don't retry it. throw new Error(error.message); case "rate_limit_exceeded": case "service_unavailable": // Temporary. The SDK already retried; queue it and try again later. return retryLater(email); case "quota_exceeded": // A plan limit: wait until it resets (details.retry_after seconds), or upgrade. return retryAfter(email, error.details?.retry_after); default: console.error(error.statusCode, error.name, error.message); }}