Skip to content
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#

Response422 Unprocessable EntityJSON
{  "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>\"." }]  }}
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#

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.

TypeScript
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);  }}