Skip to content
Sandbox

Errors

How to read an application/problem+json error from Transaction Intelligence, what to do for each HTTP status, and which requests to retry.

View as Markdown

Every error returned by the Transaction Intelligence API has the same shape: an application/problem+json body (RFC 9457) whose type field identifies the error in a stable way. The full catalogue, generated from the contract, is at the bottom of this page.

The shape of an error

An error is a Problem object.

FieldPresenceRole
typealwaysA stable URI of the form https://noryo.app/problems/transactions/<code>. This is what a client branches on.
titlealwaysA human-readable sentence. It may be reworded: do not compare it.
statusalwaysThe HTTP status, repeated in the body.
detaildepends on the errorA context sentence, never the rejected data: the scope (key or workspace) on a 429, a reminder that the schema is closed on invalid_input. Absent on some errors.
errorson a 422A list of { field, code } objects: the offending field as a dotted path (amount.currency, account_hint.iban), and the code of the rule.
request_idalwaysThe request identifier, to quote to support.

Schema Problem

  • typestring · urirequired
    https://noryo.app/problems/transactions/account_inactivehttps://noryo.app/problems/transactions/engine_unavailablehttps://noryo.app/problems/transactions/idempotency_conflicthttps://noryo.app/problems/transactions/internal_errorhttps://noryo.app/problems/transactions/invalid_api_keyhttps://noryo.app/problems/transactions/invalid_idempotency_keyhttps://noryo.app/problems/transactions/invalid_inputhttps://noryo.app/problems/transactions/invalid_requesthttps://noryo.app/problems/transactions/malformed_bodyhttps://noryo.app/problems/transactions/not_foundhttps://noryo.app/problems/transactions/rate_limitedhttps://noryo.app/problems/transactions/request_entity_too_largehttps://noryo.app/problems/transactions/unknown_currencyhttps://noryo.app/problems/transactions/unsupported_media_type
  • titlestringrequired
  • statusintegerrequired
  • detailstring
  • errorsarray<object>
  • fieldstringrequired
  • codestringrequired
  • request_idstring

    Present on every error returned by the API: it is what you give to support. It is also in the noryo-request-id header of success responses. Two responses come from the host, before the API, and are not Problem objects: an undeclared verb on a declared path returns 405 with an empty body and no Allow header; an unknown path returns 404. Neither carries request_id. Measured at the public edge; documented rather than hidden.

An example, for a timestamp without a UTC offset:

{
  "type": "https://noryo.app/problems/transactions/invalid_input",
  "title": "Une contrainte du schema d'entree n'est pas respectee.",
  "status": 422,
  "detail": "Le schema est ferme : un champ inconnu est refuse plutot qu'ignore.",
  "errors": [
    { "field": "occurred_at", "code": "must_be_rfc3339_with_offset" }
  ],
  "request_id": "req_01J9…"
}
JSON
Note

detail is a context sentence, never the rejected data: a rejected descriptor may contain a person's name, and an error message travels through logs. errors[].field tells you where to look, errors[].code tells you which rule.

Reading a response

Check the Content-Type first, then the type. A status on its own is not enough: two responses described further down come from the host, without a Problem body.

const transaction = {
  descriptor: "CARTE 09/09 MONOPRIX PARIS 15",
  direction: "outgoing",
  occurred_at: "2026-09-09T07:31:00+02:00",
  amount: { value: "49.90", currency: "EUR" },
  payment: { rail: "card" },
  external_transaction_id: "tx-2026-09-09-0042",
};

const response = await fetch("https://api.noryo.app/transactions/v1/enrichments", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.NORYO_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "tx-2026-09-09-0042",
  },
  body: JSON.stringify(transaction),
});

const contentType = response.headers.get("content-type") ?? "";
if (contentType.startsWith("application/problem+json")) {
  const problem = await response.json();
  const code = problem.type.replace("https://noryo.app/problems/transactions/", "");
  switch (code) {
    case "rate_limited": {
      const seconds = Number(response.headers.get("retry-after"));
      // wait `seconds`, then replay the same request with the same Idempotency-Key
      break;
    }
    case "invalid_input":
    case "unknown_currency":
      // problem.errors[].field and problem.errors[].code: fix before resending
      break;
    case "idempotency_conflict":
      // same key, different request: do not replay as is
      break;
    default:
      console.error(code, problem.request_id);
  }
}
TypeScript

Branch on type (or on its last segment) and on errors[].code, never on title or detail, which are sentences.

What to do, status by status

StatustypeCauseWhat to do
400malformed_bodyThe body is not valid JSON.Fix the serialisation. Do not replay as is.
400invalid_idempotency_keyThe Idempotency-Key header is outside the accepted alphabet.Fix the header.
400invalid_requestThe request was sent to a host not declared in the contract.Call https://api.noryo.app.
401invalid_api_keyCredential missing, malformed, from another family, unknown or revoked. A single code for all these cases.Check the key and the Authorization header. A revoked key does not come back: create a new one on Platform. See Authentication.
403account_inactiveThe key is recognised, but the workspace or organisation is not active.Nothing to fix in the request. Quote the request_id to support.
404not_foundOn GET /transactions/v1/enrichments/{enrichment_id}: identifier malformed, unknown, expired, or from another workspace. All four cases return the same response.Check the identifier you stored. An enrichment can be read back for 30 days; after that, enrich the transaction again. See Read an enrichment.
409idempotency_conflictThis Idempotency-Key has already been used for a different request.Do not replay as is: see below.
413request_entity_too_largeThe body exceeds the limit.Reduce the body. One call handles one transaction.
415unsupported_media_typeThe body is not declared as application/json.Send Content-Type: application/json.
422invalid_input, unknown_currencyThe schema is closed: unknown field or violated constraint.Fix what errors[] names. Do not replay as is.
429rate_limitedThe operational rate limit has been reached, for this key or for the workspace.Wait for Retry-After, then replay the same request.
500internal_errorAn internal error, not caused by your request.Retry later, with the same Idempotency-Key: if the first attempt had gone through, its response comes back as a 200 instead of a duplicate. If the error persists, quote the request_id to support.

422: fix before resending

The offending field is in errors[].field, the rule in errors[].code. Fix it, then resend; another rule may then be reported. Replaying the same request unchanged returns the same 422.

A currency outside the Money.currency enum is returned under the unknown_currency type; every other constraint under invalid_input. The list of possible codes is the runtime's own, by construction: read the 422 response of each POST in the reference, and the common cases in Enrich a transaction and Observation coverage.

409: same key, different request

"Different" is judged on the economic fingerprint (fingerprint) of the transaction: two bodies that differ only by external_transaction_id, account_hint or counterparty_hint share the same fingerprint and are the same request, replayed as 200. A 409 therefore cannot be replayed as is: either you meant to replay the original request, and it needs its original body back; or this is a new transaction, and it needs a new key. A key is retained for at least 24 hours.

429: wait for Retry-After

The detail says whether the limit reached is the key's or the workspace's. Nothing was computed, nothing was stored, no event was emitted. The Retry-After header gives, in whole seconds, the delay after which the request can go through; then replay the same request, with the same Idempotency-Key if there was one. See Rate limits.

What counts towards rate limits and usage

The gates run in a fixed order: host, Content-Type, body size, credential, authentication and rate limiting, JSON, Idempotency-Key, schema, processing. A refusal before the credential (400 on the host, 415, 413) or by it (401, 403) touches neither your rate limits nor your usage. Once the credential is accepted, a 400 on the JSON or on the Idempotency-Key, a 422, a 404 or a 409 has consumed a rate-limit token and counts towards your usage as a request. The 422, 404 and 409 responses carry the noryo-ratelimit-limit and noryo-ratelimit-remaining headers, as successes do. See Usage and API keys.

Two responses without a Problem

Two responses come from the host, before the API, and are not Problem objects: an undeclared verb on a declared path returns 405 with an empty body and no Allow header; an unknown path returns 404. Neither carries a request_id. Hence the Content-Type check before reading the body: a 404 without application/problem+json is a wrong path, not a missing enrichment.

Quoting the request_id to support

Every error returned by the API carries request_id in its body; successes carry it in the noryo-request-id header. Keep it in your logs, next to the key's public identifier (key_id), and give it to support. Never include the key's secret.

The catalogue

The catalogue below is generated from the contract served at https://api.noryo.app/openapi.json: the list of type values is the one the runtime can produce. Each code has its own page under /transaction-intelligence/errors/<code>, for example /transaction-intelligence/errors/rate_limited.

Code
StatusCodeTitle
400invalid_idempotency_keyThe Idempotency-Key header must be 1 to 255 bytes of visible ASCII (0x21 to 0x7E).
400invalid_requestThe request is not served on this host.
400malformed_bodyThe request body is not valid JSON.
401invalid_api_keyThe credential presented is not usable.
403account_inactiveThe workspace or organisation is not active.
404not_foundNo readable enrichment under this identifier.
Unknown, expired or outside your scope: these cases are not distinguished.
409idempotency_conflictThis Idempotency-Key has already been used for a different request.
413request_entity_too_largeThe request body exceeds the limit.
415unsupported_media_typeThe body must be application/json.
422invalid_inputA constraint of the input schema is not met.
The schema is closed: an unknown field is rejected rather than ignored.
422unknown_currencyThe currency is not in the reference list.
429rate_limitedToo many requests.
The rate limit for this key has been reached. Replay after the Retry-After delay.
500internal_errorAn internal error occurred.
503engine_unavailableThe resolution engine cannot be reached.

All errors

Code
StatusCodeTitle
400invalid_idempotency_keyThe Idempotency-Key header must be 1 to 255 bytes of visible ASCII (0x21 to 0x7E).
400invalid_requestThe request is not served on this host.
400malformed_bodyThe request body is not valid JSON.
401invalid_api_keyThe credential presented is not usable.
403account_inactiveThe workspace or organisation is not active.
404not_foundNo readable enrichment under this identifier.
Unknown, expired or outside your scope: these cases are not distinguished.
409idempotency_conflictThis Idempotency-Key has already been used for a different request.
413request_entity_too_largeThe request body exceeds the limit.
415unsupported_media_typeThe body must be application/json.
422invalid_inputA constraint of the input schema is not met.
The schema is closed: an unknown field is rejected rather than ignored.
422unknown_currencyThe currency is not in the reference list.
429rate_limitedToo many requests.
The rate limit for this key has been reached. Replay after the Retry-After delay.
500internal_errorAn internal error occurred.
503engine_unavailableThe resolution engine cannot be reached.
Was this page helpful?