Errors
A small set of generic codes, not one per failure reason. Branch on code plus status, read message for detail.
Every error shares one flat envelope — there's no nested error object:
{
"statusCode": 400,
"request_id": "a78010ec-a2bd-41e7-9c0d-654449103c7b",
"timestamp": "2026-08-13T06:52:02.829Z",
"error_code": "VALIDATION_ERROR",
"message": "exchange_rate is required when invoice_currency_code is not OMR",
"details": null
}error_code is one of a small, fixed set — it tells you the category of failure, not the specific
reason. The specific reason is in message (and, where relevant, details) — read it, don't try to
branch on it, since exact wording isn't guaranteed stable.
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR | Covers everything from a missing/malformed field to a document that breaks an Oman schematron rule — details carries the issues array on the latter, same shape as Validate's issues[] |
| 401 | UNAUTHORIZED_ERROR | Missing, unrecognised, or revoked API key — see Authentication |
| 403 | FORBIDDEN_ERROR | The taxpayer named doesn't belong to your organisation, or an environment (live/test) mismatch |
| 404 | NOT_FOUND_ERROR | No such transaction, document or taxpayer |
| 409 | CONFLICT_ERROR | Covers a duplicate VAT id on taxpayer creation, an Idempotency-Key reused for a different request, and changing an already-registered taxpayer's identity fields (vat_id, electronic_address, electronic_address_scheme) — message says which |
| 429 | RATE_LIMITED | Slow down. Retry-After header says by how long |
| 502 | UPSTREAM_ERROR | Peppol or the Tax Authority is unreachable. Retry later |
A request that fails Zod's own field-level validation (before it reaches application logic) is shaped
slightly differently — no error_code at all, just statusCode: 400, message (the first issue), and
details as the full array of issues. Treat that as an alias for VALIDATION_ERROR.
Schematron failures are 400, not 422
A document that fails Oman's Peppol schematron rules — the same failure Validate
reports as valid: false — comes back as 400 VALIDATION_ERROR when it happens on a send, not a
422. The API declares a NON_PROCESSABLE_ERROR → 422 mapping internally, but nothing in the
codebase currently throws it; every validation failure, field-level or schematron-level, is a
VALIDATION_ERROR. Don't branch on 422 for this today.