> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.moyoinnovations.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.moyoinnovations.com/_mcp/server.

# Errors

> How Moyo Suite reports errors — the shapes you'll actually see.

Moyo Suite doesn't use one uniform error envelope everywhere. Most endpoints return
a plain message; a handful of endpoints — permissions, plan/module gating,
and a few business-rule checks — return a richer structured object. Both are
real, and you should handle both.

## The common case: a plain message

```json
{
  "detail": "Import job not found."
}
```

## The structured case: permission and entitlement errors

Requests blocked by RBAC or by your organization's plan return a `code` you
can branch on, plus a human-readable `message`:

```json
{
  "detail": {
    "code": "permission_denied",
    "message": "You don't have permission to view CRM leads.",
    "required": "crm:lead:view"
  }
}
```

```json
{
  "detail": {
    "code": "module_not_subscribed",
    "message": "Your organization hasn't enabled the CRM module.",
    "module": "crm"
  }
}
```

A few other endpoints — expense approval/rejection, data import mapping —
use the same `{code, message}` shape for their own business-rule errors
(e.g. `read_only_field`, `approved_expense_cannot_be_edited`).

## Validation errors (422)

Request-body validation failures always come back the same way, with one
entry per invalid field plus a `request_id`:

```json
{
  "detail": "Validation error",
  "errors": [
    { "loc": ["body", "email"], "msg": "value is not a valid email address" }
  ],
  "request_id": "..."
}
```

## Status codes

| Status | Meaning                                                               |
| ------ | --------------------------------------------------------------------- |
| 400    | Bad Request — invalid input outside of body validation                |
| 401    | Unauthorized — missing/invalid/expired session                        |
| 403    | Forbidden — authenticated, but blocked by RBAC or module entitlement  |
| 404    | Not Found                                                             |
| 409    | Conflict — e.g. approved expense can't be edited, duplicate reference |
| 422    | Unprocessable Entity — body validation failure                        |
| 429    | Rate Limited                                                          |
| 500    | Internal Server Error                                                 |

## Every response carries a request ID — in the header

Every response — success or failure — includes an `X-Request-ID` header.
Include it when reporting an issue.

The response *body* only echoes `request_id` for two cases: **422**
validation errors (shown above) and unhandled **500**s. A plain
`{"detail": "..."}` error (404s, most 400s, RBAC/entitlement 403s, etc.)
does not include `request_id` in its body — read it from the header
instead.