# Errors

> Every Mailmanny API error: its HTTP status, its name, and what to do about it.

Source: https://www.mailmanny.com/docs/errors
All docs for AI agents: https://www.mailmanny.com/llms.txt

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

**422 Unprocessable Entity**

```json
{
  "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>\"." }]
  }
}
```

- `statusCode` (number, required): Same as the HTTP status.
- `name` (string, required): Stable identifier. Branch on this, never on the message.
- `message` (string, required): What went wrong and how to fix it. Wording can change.
- `details` (object): 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_request` (400 / 413): The request itself is malformed: not JSON, wrong Content-Type, a bad Idempotency-Key, or a body over 10 MB.
- `validation_error` (422): 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_key` (401): No Authorization header. Send Authorization: Bearer mm_live_…
- `invalid_api_key` (401): The key is malformed, doesn't exist or was revoked.
- `restricted_api_key` (403): 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_blocked` (403): The request came from a web page. Call the API from your server.
- `forbidden` (403): The workspace can't do this right now, e.g. it's suspended.
- `not_found` (404): The resource doesn't exist, belongs to another workspace, or is outside the key's audiences.
- `method_not_allowed` (405): Wrong HTTP method for this path. The Allow header lists the right ones.
- `conflict` (409): The change clashes with existing data, e.g. pushing over a template built in the editor.
- `invalid_idempotent_request` (409): This Idempotency-Key was already used for a different request.
- `concurrent_idempotent_requests` (409): A request with this Idempotency-Key is still running. Retry in a moment.
- `invalid_from_address` (422): The from domain isn't verified in this workspace.
- `sending_blocked` (422): 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_paused` (423): 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_exceeded` (429): Too many requests. Wait for Retry-After seconds.
- `quota_exceeded` (429): 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_required` (402): 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_error` (500): Something broke on our side. Retrying is safe with an Idempotency-Key.
- `service_unavailable` (503): 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`.

```ts
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);
  }
}
```

> **Note:** What the SDK retries
> 429s, 5xx responses and network errors are retried twice with backoff (honoring `Retry-After`) for GET, PUT and DELETE, and for POSTs that carry an idempotency key. Change it with `new Mailmanny(key, { maxRetries })`.
