Erreurs
Comment lire une erreur application/problem+json de Transaction Intelligence, quoi faire pour chaque statut HTTP, et quelles requêtes rejouer.
Toute erreur rendue par l'API Transaction Intelligence a la même forme : un corps application/problem+json (RFC 9457) dont le champ type identifie l'erreur de façon stable. Le catalogue complet, généré depuis le contrat, est en bas de cette page.
La forme d'une erreur
Une erreur est un objet Problem.
| Champ | Présence | Rôle |
|---|---|---|
type | toujours | Une URI stable, de la forme https://noryo.app/problems/transactions/<code>. C'est sur elle qu'un client se branche. |
title | toujours | Une phrase lisible. Elle peut être reformulée : ne la comparez pas. |
status | toujours | Le statut HTTP, répété dans le corps. |
detail | selon l'erreur | Une phrase de contexte, jamais la donnée refusée : la portée (clé ou workspace) sur un 429, le rappel du schéma fermé sur invalid_input. Absente sur certaines erreurs. |
errors | sur un 422 | Une liste d'objets { field, code } : le champ en cause, en chemin pointé (amount.currency, account_hint.iban), et le code de la règle. |
request_id | toujours | L'identifiant de la requête, à citer au support. |
Schéma Problem
- typestring · urirequis
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 - titlestringrequis
- statusintegerrequis
- detailstring
- errorsarray<object>
- fieldstringrequis
- codestringrequis
- request_idstring
Présent sur toute erreur rendue par l'API : c'est ce que vous donnez au support. Il est aussi dans l'en-tête
noryo-request-iddes réponses de succès. Deux réponses viennent de l'hébergeur, avant l'API, et ne sont pas des Problem : un verbe non déclaré sur un chemin déclaré rend405avec un corps vide et sans en-têteAllow; un chemin inconnu rend404. Ni l'un ni l'autre ne porterequest_id. Mesuré au bord public ; documenté plutôt que masqué.
Un exemple, pour une date sans décalage horaire :
{
"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…"
}JSONdetail est une phrase de contexte, jamais la donnée refusée : un descripteur refusé peut contenir le nom d'une personne, et un message d'erreur voyage dans des journaux. errors[].field dit où regarder, errors[].code dit quelle règle.
Lire une réponse
Testez d'abord le Content-Type, puis le type. Un statut seul ne suffit pas : deux réponses décrites plus bas viennent de l'hébergeur, sans corps Problem.
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"));
// attendre `seconds`, puis rejouer la même requête, même Idempotency-Key
break;
}
case "invalid_input":
case "unknown_currency":
// problem.errors[].field et problem.errors[].code : corriger avant de renvoyer
break;
case "idempotency_conflict":
// même clé, requête différente : ne pas rejouer tel quel
break;
default:
console.error(code, problem.request_id);
}
}TypeScriptBranchez-vous sur type (ou sur son dernier segment) et sur errors[].code, jamais sur title ni detail, qui sont des phrases.
Que faire, statut par statut
| Statut | type | Cause | Ce qu'il faut faire |
|---|---|---|---|
400 | malformed_body | Le corps n'est pas un JSON valide. | Corrigez la sérialisation. Ne rejouez pas tel quel. |
400 | invalid_idempotency_key | L'en-tête Idempotency-Key est hors alphabet. | Corrigez l'en-tête. |
400 | invalid_request | La requête est adressée à un hôte non déclaré dans le contrat. | Appelez https://api.noryo.app. |
401 | invalid_api_key | Credential absente, mal formée, d'une autre famille, inconnue ou révoquée. Un seul code pour tous ces cas. | Vérifiez la clé et l'en-tête Authorization. Une clé révoquée ne revient pas : créez-en une nouvelle sur Platform. Voir Authentification. |
403 | account_inactive | La clé est reconnue, mais le workspace ou l'organisation n'est pas actif. | Rien à corriger dans la requête. Citez le request_id au support. |
404 | not_found | Sur GET /transactions/v1/enrichments/{enrichment_id} : identifiant malformé, inconnu, expiré, ou d'un autre workspace. Les quatre cas rendent la même réponse. | Vérifiez l'identifiant stocké. Un enrichissement est relisible 30 jours ; au-delà, enrichissez de nouveau la transaction. Voir Relire un enrichissement. |
409 | idempotency_conflict | Cette Idempotency-Key a déjà servi pour une requête différente. | Ne rejouez pas tel quel : voir ci-dessous. |
413 | request_entity_too_large | Le corps dépasse la borne. | Réduisez le corps. Un appel traite une transaction. |
415 | unsupported_media_type | Le corps n'est pas déclaré application/json. | Envoyez Content-Type: application/json. |
422 | invalid_input, unknown_currency | Le schéma est fermé : champ inconnu ou contrainte violée. | Corrigez ce que errors[] nomme. Ne rejouez pas tel quel. |
429 | rate_limited | La limite de débit opérationnelle est atteinte, pour cette clé ou pour le workspace. | Attendez Retry-After, puis rejouez la même requête. |
500 | internal_error | Une erreur interne, qui ne vient pas de votre requête. | Rejouez plus tard, avec la même Idempotency-Key : si la première tentative avait abouti, sa réponse revient en 200 au lieu d'un doublon. Si l'erreur persiste, citez le request_id au support. |
422 : corriger avant de renvoyer
Le champ en cause est dans errors[].field, la règle dans errors[].code. Corrigez, puis renvoyez ; une autre règle peut alors être signalée. Rejouer la même requête sans la changer rend le même 422.
Une devise hors de l'enum Money.currency est rendue sous le type unknown_currency ; toute autre contrainte sous invalid_input. La liste des codes possibles est celle du runtime, par construction : lisez la réponse 422 de chaque POST dans la référence, et les cas fréquents dans Enrichir une transaction et Couverture d'observation.
409 : même clé, autre requête
« Différente » se juge sur l'empreinte économique (fingerprint) de la transaction : deux corps qui ne diffèrent que par external_transaction_id, account_hint ou counterparty_hint ont la même empreinte et sont la même requête, rejouée en 200. Un 409 ne se rejoue donc pas tel quel : soit vous vouliez rejouer la requête d'origine, et il faut lui rendre son corps ; soit c'est une nouvelle transaction, et il lui faut une nouvelle clé. Une clé est retenue au moins 24 heures.
429 : attendre Retry-After
Le detail dit si la limite atteinte est celle de la clé ou celle du workspace. Rien n'a été calculé, rien n'a été enregistré, aucun événement n'a été émis. L'en-tête Retry-After donne, en secondes entières, le délai après lequel la requête peut passer ; rejouez alors la même requête, avec la même Idempotency-Key s'il y en avait une. Voir Limites de débit.
Ce qui compte dans le débit et l'usage
Les portes s'enchaînent dans un ordre fixe : hôte, Content-Type, taille du corps, credential, authentification et débit, JSON, Idempotency-Key, schéma, traitement. Un refus avant la credential (400 sur l'hôte, 415, 413) ou par elle (401, 403) ne touche ni vos limites de débit ni votre usage. Une fois la credential acceptée, un 400 sur le JSON ou sur l'Idempotency-Key, un 422, un 404 ou un 409 a consommé un jeton de débit et compte dans votre usage comme une requête. Les réponses 422, 404 et 409 portent les en-têtes noryo-ratelimit-limit et noryo-ratelimit-remaining, comme les succès. Voir Usage et clés d'API.
Deux réponses sans Problem
Deux réponses viennent de l'hébergeur, avant l'API, et ne sont pas des Problem : un verbe non déclaré sur un chemin déclaré rend 405 avec un corps vide et sans en-tête Allow ; un chemin inconnu rend 404. Ni l'une ni l'autre ne porte request_id. D'où le test du Content-Type avant de lire le corps : un 404 sans application/problem+json est une faute de chemin, pas un enrichissement introuvable.
Citer le request_id au support
Toute erreur rendue par l'API porte request_id dans son corps ; les succès le portent dans l'en-tête noryo-request-id. Conservez-le dans vos journaux, à côté de l'identifiant public de la clé (key_id), et donnez-le au support. Ne joignez jamais le secret de la clé.
Le catalogue
Le catalogue ci-dessous est généré depuis le contrat servi sur https://api.noryo.app/openapi.json : la liste des type est celle que le runtime peut produire. Chaque code a sa page sous /transaction-intelligence/errors/<code>, par exemple /transaction-intelligence/errors/rate_limited.
| Statut | Code | Titre |
|---|---|---|
400 | invalid_idempotency_key | L'en-tête Idempotency-Key doit faire 1 à 255 octets d'ASCII visible (0x21 à 0x7E). |
400 | invalid_request | La requête n'est pas servie sur cet hôte. |
400 | malformed_body | Le corps de la requête n'est pas un JSON valide. |
401 | invalid_api_key | La credential présentée n'est pas utilisable. |
403 | account_inactive | Le workspace ou l'organisation n'est pas actif. |
404 | not_found | Aucun enrichissement lisible sous cet identifiant. Inconnu, expiré ou hors de votre périmètre : ces cas ne sont pas distingués. |
409 | idempotency_conflict | Cette Idempotency-Key a déjà été utilisée pour une requête différente. |
413 | request_entity_too_large | Le corps de la requête dépasse la borne. |
415 | unsupported_media_type | Le corps doit être du application/json. |
422 | invalid_input | Une contrainte du schéma d'entrée n'est pas respectée. Le schéma est fermé : un champ inconnu est refusé plutôt qu'ignoré. |
422 | unknown_currency | La devise n'est pas dans le référentiel. |
429 | rate_limited | Trop de requêtes. La limite de débit de cette clef est atteinte. Rejouez après le délai de Retry-After. |
500 | internal_error | Une erreur interne est survenue. |
503 | engine_unavailable | Le moteur de résolution n'est pas joignable. |
Toutes les erreurs
| Statut | Code | Titre |
|---|---|---|
400 | invalid_idempotency_key | L'en-tête Idempotency-Key doit faire 1 à 255 octets d'ASCII visible (0x21 à 0x7E). |
400 | invalid_request | La requête n'est pas servie sur cet hôte. |
400 | malformed_body | Le corps de la requête n'est pas un JSON valide. |
401 | invalid_api_key | La credential présentée n'est pas utilisable. |
403 | account_inactive | Le workspace ou l'organisation n'est pas actif. |
404 | not_found | Aucun enrichissement lisible sous cet identifiant. Inconnu, expiré ou hors de votre périmètre : ces cas ne sont pas distingués. |
409 | idempotency_conflict | Cette Idempotency-Key a déjà été utilisée pour une requête différente. |
413 | request_entity_too_large | Le corps de la requête dépasse la borne. |
415 | unsupported_media_type | Le corps doit être du application/json. |
422 | invalid_input | Une contrainte du schéma d'entrée n'est pas respectée. Le schéma est fermé : un champ inconnu est refusé plutôt qu'ignoré. |
422 | unknown_currency | La devise n'est pas dans le référentiel. |
429 | rate_limited | Trop de requêtes. La limite de débit de cette clef est atteinte. Rejouez après le délai de Retry-After. |
500 | internal_error | Une erreur interne est survenue. |
503 | engine_unavailable | Le moteur de résolution n'est pas joignable. |