Errors
Every error is an RFC 9457 Problem Details object whose code alone is a machine contract. The format, the complete catalogue of codes by status, and what to do for each.
Every Noryo Merchant API error is a Problem Details document (RFC 9457), served as application/problem+json.
{
"type": "https://developer.noryo.app/errors/idempotency_key_reuse",
"title": "Idempotency key reused",
"status": 409,
"code": "idempotency_key_reuse",
"detail": "this Idempotency-Key is bound to a different request for the remainder of its TTL",
"request_id": "req_01a07784-d343-75f8-b0ae-b0629b305cd4"
}JSONThe fields
| Field | Content | Stable? |
|---|---|---|
type | The address of the code's page in the error catalogue | The link may change domain; the code does not |
title | A short title, in English, for a human | No: may be reworded |
status | The HTTP status of the response, for every code | |
code | The machine code, in snake_case | Yes: it is the contract |
detail | A sentence in English, sometimes more specific than the title | No: may be reworded |
request_id | The identifier of the attempt, identical to the Noryo-Request-Id header |
A 410 adds enrichment_id, and so does a 503 engine_error: there it designates the failed attempt, not a result. No other member is added.
Build your logic on code, and on the HTTP status. title and detail are written for humans and may be reworded at any time without a version change: a client that parses them will break at the first editorial pass.
Error bodies never carry a SQLSTATE, a table name, a stack trace, an internal hostname, or the value of your key or your Idempotency-Key. A 500 carries only code and request_id as useful information.
The catalogue
Each code has its own page in the error catalogue.
| Status | Code | When | What to do |
|---|---|---|---|
| 400 | invalid_request | Body outside the schema, unknown field, external_transaction_id on a refresh, unknown host | Fix the request |
| 400 | invalid_json | Body that is not valid JSON | Fix the request |
| 400 | identifier_required | Create call with neither Idempotency-Key nor external_transaction_id; refresh without Idempotency-Key | Add the identifier |
| 400 | invalid_idempotency_key | Idempotency-Key header empty, too long, repeated or outside visible ASCII | Fix the key |
| 400 | invalid_input | Not returned by the current routes | None |
| 400 | refresh_requires_transaction_id | Not returned by the current routes: a refresh of an enrichment without external_transaction_id returns 409 refresh_not_available | See Refresh |
| 400 | target_enrichment_required | Not returned by the current routes: the path always names the target | None |
| 400 | unsupported_canonicalization | Not returned by the current routes | Contact support with the request_id |
| 401 | invalid_api_key | Key missing, malformed, unknown, revoked or with the wrong prefix | Fix the key |
| 402 | sandbox_quota_exhausted | Sandbox allowance exhausted | Move to Production |
| 402 | no_active_subscription | No active subscription covers the operation | Put the account in order |
| 403 | insufficient_scope | The key lacks the route's scope | Use a key with the right scope |
| 403 | account_inactive | Organisation or workspace inactive | Contact Noryo |
| 403 | production_not_enabled | The key is not allowed in Production | See Going live |
| 404 | enrichment_not_found | enrichment_id unknown, malformed, from another workspace or environment | Check the identifier, the key, the environment |
| 404 | transaction_not_found | Refresh of a transaction with no recorded result | Create an enrichment first |
| 409 | idempotency_key_reuse | Same Idempotency-Key, different request, within 24 hours | A new key for a new request |
| 409 | transaction_id_conflict | Same external_transaction_id, different economic input | Fix the input or the identifier |
| 409 | input_mismatch | Refresh whose input differs from the transaction's | Send the original input |
| 409 | refresh_not_available | Original enrichment without external_transaction_id | Read with GET, or create with an identifier |
| 410 | result_content_expired | Content purged after 90 days | See Retention and 410 |
| 413 | request_entity_too_large | Body over 16 KiB | Shrink the body |
| 415 | unsupported_media_type | Content-Type other than application/json | Send JSON |
| 429 | rate_limited | Rate-limit bucket empty | Retry after Retry-After |
| 500 | internal_error | Unexpected error | Retry with the same key; quote the request_id |
| 503 | engine_error | The resolution engine did not answer; nothing billed or memorised | Retry with the same key |
| 503 | busy | Transient contention, or an identical request in progress | Retry after Retry-After |
| 503 | pricing_not_activated | Billing not yet activated for the operation | Retry after Retry-After |
Retry policy
| Status | Retry? |
|---|---|
429 | Yes, after Retry-After |
503 | Yes, with the same Idempotency-Key: nothing was billed or memorised |
500 | Yes, with the same Idempotency-Key |
409 | No: fix the payload or the key |
Other 4xx | No |
Details are in Retries.
The attempt identifier
Every response carries Noryo-Request-Id, and every error body repeats it in request_id. One attempt has exactly one identifier: the same one in the header, the body and Noryo's logs, including on a 500. Quote it to support and we can find the exact attempt. See Request IDs.
Published on 2026-10-09 10:18 UTC (version 2).