Errors

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

1{
2 "detail": "Import job not found."
3}

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:

1{
2 "detail": {
3 "code": "permission_denied",
4 "message": "You don't have permission to view CRM leads.",
5 "required": "crm:lead:view"
6 }
7}
1{
2 "detail": {
3 "code": "module_not_subscribed",
4 "message": "Your organization hasn't enabled the CRM module.",
5 "module": "crm"
6 }
7}

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:

1{
2 "detail": "Validation error",
3 "errors": [
4 { "loc": ["body", "email"], "msg": "value is not a valid email address" }
5 ],
6 "request_id": "..."
7}

Status codes

StatusMeaning
400Bad Request — invalid input outside of body validation
401Unauthorized — missing/invalid/expired session
403Forbidden — authenticated, but blocked by RBAC or module entitlement
404Not Found
409Conflict — e.g. approved expense can’t be edited, duplicate reference
422Unprocessable Entity — body validation failure
429Rate Limited
500Internal 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 500s. 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.