# Errors

> The error format, every error code the API returns, and what to do about each one.

Source: https://docs.maylee.app/errors

Every error has the same shape:

```json
{
  "code": "INSUFFICIENT_SCOPE",
  "error": "This API key is missing the `write` scope. Edit the key in your workspace settings to add it.",
  "details": { "required": "write", "granted": ["read"] }
}
```

| Field     | Description                                                                                    |
| --------- | ---------------------------------------------------------------------------------------------- |
| `code`    | Machine-readable code, `UPPER_SNAKE_CASE`. A stable contract: **branch your logic on it**.     |
| `error`   | Human-readable message. It may be reworded: never parse it.                                    |
| `details` | Optional. Context that helps fix the request: faulty fields, the bound that was exceeded, ids. |

A business refusal returns a `4xx`, not a `500`: it describes a request you can fix, and retrying it unchanged will never succeed.

## Authentication, 401

| Code              | Meaning                            | What to do                                       |
| ----------------- | ---------------------------------- | ------------------------------------------------ |
| `INVALID_API_KEY` | Key missing, malformed or unknown. | Check the `Authorization: Bearer mk_...` header. |
| `API_KEY_REVOKED` | The key was revoked.               | Create a new one in the workspace settings.      |
| `API_KEY_EXPIRED` | The key's expiry date has passed.  | Create a new one; `details.expiredAt` says when. |

A session cookie never works on the API, whatever its validity. `401` responses carry no `X-RateLimit-*` headers.

## Payment required, 402

| Code            | Meaning                                                                       | What to do                                                      |
| --------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `PLAN_REQUIRED` | Turning on auto-draft or auto-reply on an AI label rule needs a subscription. | Upgrade the workspace. Turning these features off always works. |

## Authorization, 403

| Code                        | Meaning                                                                                          | What to do                                                             |
| --------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- |
| `INSUFFICIENT_SCOPE`        | The key lacks a scope.                                                                           | `details.required` names it, `details.granted` lists what the key has. |
| `OUTBOUND_SEND_NOT_ALLOWED` | The action makes email leave the workspace and the key does not have the Send emails permission. | Create a key with **Send emails** ticked.                              |
| `WORKSPACE_SUSPENDED`       | The workspace is suspended.                                                                      | Reactivate the subscription.                                           |

`OUTBOUND_SEND_NOT_ALLOWED` is not a missing scope but a separate permission: `write` alone must not be enough to write to your customers, or to forward their mail elsewhere, if the key leaks. It is returned by `POST /drafts/send`, by every write on `/auto-forward-rules`, and by `/ai-label-rules` when a request sets `autoReplyEnabled: true`.

## Not found, 404

| Code                | Returned by                                                                                                   |
| ------------------- | ------------------------------------------------------------------------------------------------------------- |
| `EMAIL_NOT_FOUND`   | `GET /emails/{emailId}`, and label actions on an email.                                                       |
| `NOT_FOUND`         | `GET /emails/{emailId}/body`, `PATCH /emails/{emailId}`, `DELETE /emails/{emailId}`.                          |
| `DRAFT_NOT_FOUND`   | `GET /drafts/{draftId}`.                                                                                      |
| `MAILBOX_NOT_FOUND` | `POST /drafts`, `POST /drafts/send` and `POST /ai-label-rules/backfill`, when `mailboxId` is unknown.         |
| `LABEL_NOT_FOUND`   | The `/labels/{labelId}` endpoints, applying a label to an email, and `POST /emails/bulk` with a label action. |
| `RULE_NOT_FOUND`    | The `/ai-label-rules/{ruleId}` and `/auto-forward-rules/{ruleId}` endpoints.                                  |
| `VIEW_NOT_FOUND`    | The `/views/{viewId}` endpoints.                                                                              |
| `FILTER_NOT_FOUND`  | The `/filters/{filterId}` endpoints.                                                                          |

A resource that belongs to another workspace returns **404**, not 403: a 403 would confirm that it exists. Treat `EMAIL_NOT_FOUND` and `NOT_FOUND` alike: both mean the email is not reachable with this key.

## Invalid request, 400

| Code                   | Meaning                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------ |
| `VALIDATION_FAILED`    | Invalid body, parameter or filter tree.                                              |
| `INVALID_SENDER_ALIAS` | The `from` alias is not a verified send-as address of the mailbox.                   |
| `MAILBOX_INACTIVE`     | The mailbox is disconnected: reconnect it in Maylee before sending.                  |
| `NO_TRASH_FOLDER`      | The mailbox has no trash folder, so the email cannot be moved there.                 |
| `INVALID_TAG`          | The label is an AI label: rename or delete it through its rule on `/ai-label-rules`. |
| `REQUEST_ERROR`        | The request itself could not be read, typically malformed JSON.                      |

On `VALIDATION_FAILED`, `details` names the faulty fields:

```json
{
  "code": "VALIDATION_FAILED",
  "error": "Invalid email payload.",
  "details": {
    "mailboxId": ["Required"],
    "to": ["Required"],
    "body": ["Required"]
  }
}
```

## Timeout, 408

| Code            | Meaning                                 |
| --------------- | --------------------------------------- |
| `QUERY_TIMEOUT` | The query ran for more than 10 seconds. |

The query was **cancelled on the database side**: it is not still running. Narrow the filters, lower `limit`, or target a single mailbox.

## Conflict, 409

| Code                 | Meaning                                                                                    |
| -------------------- | ------------------------------------------------------------------------------------------ |
| `CONFLICT`           | `Idempotency-Key` reused with a different body, or the original call is still in progress. |
| `DUPLICATE_TAG_NAME` | A label with this name already exists in the workspace.                                    |
| `DUPLICATE_RULE`     | An AI label with this name already exists in the workspace.                                |
| `DUPLICATE_FILTER`   | An inbox filter with this type and value already exists (`details.filterId` names it).     |

For `CONFLICT`, generate a new key if you are describing a new operation, or retry in a moment if the original call is still running. Label names are unique per workspace, manual and AI labels alike.

## Payload too large, 413

| Code                | Meaning                       |
| ------------------- | ----------------------------- |
| `PAYLOAD_TOO_LARGE` | The JSON body exceeds 256 KB. |

## Too many requests, 429

| Code                  | Meaning                           | What to do                                                 |
| --------------------- | --------------------------------- | ---------------------------------------------------------- |
| `RATE_LIMIT_EXCEEDED` | Too many requests this minute.    | Wait `Retry-After` seconds.                                |
| `QUOTA_EXCEEDED`      | The monthly allowance is used up. | Wait for the 1st of next month (UTC), or upgrade the plan. |

See [Rate limits and quotas](/rate-limits).

## Server error, 500

| Code             | Meaning                                                                                 |
| ---------------- | --------------------------------------------------------------------------------------- |
| `INTERNAL_ERROR` | Unexpected failure, including a refusal from the email provider on `POST /drafts/send`. |

This is the only case where retrying the same request makes sense. On a send, nothing was sent and the idempotency key is released, so a retry with the same key sends again rather than replaying a failure. If a `500` persists on a request you believe is valid, report it with the time and the path you called.
