Enrichir une transaction
Envoyer une transaction brute à POST /transactions/v1/enrichments et lire ce que la réponse affirme, ignore ou refuse.
Une transaction brute entre, un enrichissement structuré sort. POST /transactions/v1/enrichments prend un objet TransactionInput, authentifié par votre clé nry_txn_sbx_* (voir Authentification), et rend un objet Enrichment : contrepartie, type de transaction, relations, séries. Cette page décrit le corps attendu, l'en-tête Idempotency-Key, la différence entre 201 et 200, et les deux blocs à lire en premier : signals et resolution.policy.
L'opération
Paramètres
- Idempotency-Keystringheader
Facultatif. Avec la même clef et la même requête : rejeu (200), la réponse d'origine est rendue telle quelle, version de moteur comprise. Avec la même clef et une requête différente : 409. « REQUÊTE DIFFÉRENTE » se juge sur l'empreinte économique (
fingerprint), et sur elle seule : deux corps qui ne diffèrent que parexternal_transaction_id,account_hintoucounterparty_hintont la même empreinte et sont la même requête pour l'idempotence — le premier envoi est rejoué tel quel. Ne réutilisez pas une clef pour deux transactions que vous distinguez par ces seuls champs. Aucune version de moteur n'entre dans cette identité : un déploiement entre deux tentatives ne transforme pas un rejeu en opération neuve. La valeur brute n'est jamais rangée ni journalisée. La clef est retenue au moins 24 heures : elle expire 24 heures après le premier envoi et n'est effacée que par la purge quotidienne qui suit cette expiration. Entre 24 et 48 heures après le premier envoi, la même clef ouvre donc une opération neuve (201) — jamais avant 24 heures. De 1 à 255 octets d'ASCII visible (0x21 à 0x7E) ; une valeur vide ou hors alphabet est refusée 400.
Corps de la requêterequis
application/json · TransactionInput
- descriptorstring · length 1–512requis
- directionstringrequis
incomingoutgoing - occurred_atstring · date-time
RFC 3339 avec décalage. Jamais réduit à une date : minuit n'est pas le même jour partout.
- amountMoney
- valuestringrequis
Décimal en chaîne. Un flottant perdrait des centimes en silence. Le nombre de décimales admis dépend de la devise :
5.905 EURest refusé,100.50 ISKaussi (ISK et JPY n'en ont aucune, KWD en a trois).^-?(0|[1-9][0-9]{0,15})(\.[0-9]{1,4})?$ - currencystringrequis
Une devise hors de cette liste est refusée 422
unknown_currency.CADCHFDKKEURGBPISKJPYKWDNOKPLNSEKUSD - paymentobject
- railstring · length ≤ 128
- mccstring
^[0-9]{4}$ - card_presentboolean
- network_merchant_idstring · length ≤ 128
- raw_typestring · length ≤ 128
- location_hintobject
- countrystring
^[A-Z]{2}$ - citystring · length ≤ 128
- postal_codestring · length ≤ 128
- counterparty_hintobject
L'IBAN est réduit dès l'entrée à son pays et son code banque. Le numéro de compte n'entre pas dans le système, donc ne peut pas en sortir.
- ibanstring
^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$ - bicstring
^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$ - account_hintobject
Le compte sur lequel la transaction est passée — à distinguer de
counterparty_hint, qui désigne l'autre bout. Sans lui, une paire de virement ne peut pas être identifiée : le seul signal qui la désigne est « mon compte est ta contrepartie, et ta contrepartie est mon compte ». L'IBAN est réduit dès l'entrée, comme celui de la contrepartie : le numéro de compte n'entre pas dans le système.external_account_id: votre propre identifiant du compte, pour les cartes, portefeuilles et rails sans IBAN. Il sert à une chose : relier la transaction à une couverture déclarée sur le même compte. Il n'entre pas dans l'empreinte. Mesuré : il était accepté, puis jeté — la série n'avait aucune source, etlifecycle.staterestaitunknown. - ibanstring
^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$ - external_account_idstring · length 1–128
- external_transaction_idstring · length 1–128
Votre propre identifiant de transaction. Il n'entre pas dans l'empreinte : il dit comment vous nommez la transaction, pas ce qu'elle est. Il sert à empêcher deux envois d'une même transaction de se rapprocher l'un de l'autre.
Réponses
200Déjà enrichie : requête rejouée, rien n'a été créé. Location est posé ici aussi : le client apprend où vit la ressource qu'il n'a pas créée.Enrichment
201Enrichissement créé.Enrichment
400Corps illisible, ou Idempotency-Key hors alphabet.Problem
401Credential absente, mal formée, d'une autre famille, inconnue, révoquée, ou dont le préfixe déclaré ne correspond pas au préfixe enregistré. Un seul code pour tous ces cas : les distinguer serait un oracle.Problem
403Workspace ou organisation inactif.Problem
409Cette Idempotency-Key a déjà servi pour une requête différente — c'est-à-dire d'empreinte économique (fingerprint) différente. Même clef + même empreinte = rejeu (200), jamais 409.Problem
413Corps trop volumineux.Problem
415Le corps doit être du application/json.Problem
422Schéma fermé : champ inconnu, ou contrainte violée. errors[].code nomme la règle, et la liste est celle du runtime, par construction : body_not_object, unknown_field, required_non_empty_string, too_long, contains_control_character, must_be_incoming_or_outgoing, must_be_rfc3339_with_offset, must_be_object, must_be_string, must_be_boolean, must_be_four_digits, must_be_iso_3166_1_alpha_2_uppercase, malformed_iban, malformed_bic, must_be_non_empty_string, amount_not_object, amount_value_not_string, amount_value_malformed, amount_currency_not_string, amount_currency_malformed, amount_currency_unknown, amount_precision_exceeds_currency. must_be_rfc3339_with_offset couvre la forme et le calendrier : 2026-02-30T00:00:00Z, +16:00 ou 23:59:60 sont refusés. must_be_object refuse aussi un tableau, pour les quatre familles. contains_control_character : un caractère de contrôle (C0, DEL, C1) dans un texte, tabulation, saut de ligne et retour chariot exceptés. amount_currency_unknown est rendu sous le type unknown_currency, les autres sous invalid_input.Problem
429rate_limited : la limite opérationnelle de débit est atteinte, pour cette clef ou pour le workspace (le detail le dit). Rien n'a été calculé, rien n'a été rangé, aucun événement n'a été émis. Rejouez après Retry-After. Voir x-noryo-rate-limit.Problem
500Erreur interne.Problem
Le corps est du application/json, sinon 415. Toute erreur porte request_id dans son corps, à citer au support ; une fois la credential acceptée, chaque réponse porte aussi les en-têtes noryo-request-id, noryo-ratelimit-limit et noryo-ratelimit-remaining (voir Limites de débit).
Le corps : TransactionInput
Schéma TransactionInput
Schéma fermé : un champ inconnu est refusé (422), jamais ignoré.
- descriptorstring · length 1–512requis
- directionstringrequis
incomingoutgoing - occurred_atstring · date-time
RFC 3339 avec décalage. Jamais réduit à une date : minuit n'est pas le même jour partout.
- amountMoney
- valuestringrequis
Décimal en chaîne. Un flottant perdrait des centimes en silence. Le nombre de décimales admis dépend de la devise :
5.905 EURest refusé,100.50 ISKaussi (ISK et JPY n'en ont aucune, KWD en a trois).^-?(0|[1-9][0-9]{0,15})(\.[0-9]{1,4})?$ - currencystringrequis
Une devise hors de cette liste est refusée 422
unknown_currency.CADCHFDKKEURGBPISKJPYKWDNOKPLNSEKUSD - paymentobject
- railstring · length ≤ 128
- mccstring
^[0-9]{4}$ - card_presentboolean
- network_merchant_idstring · length ≤ 128
- raw_typestring · length ≤ 128
- location_hintobject
- countrystring
^[A-Z]{2}$ - citystring · length ≤ 128
- postal_codestring · length ≤ 128
- counterparty_hintobject
L'IBAN est réduit dès l'entrée à son pays et son code banque. Le numéro de compte n'entre pas dans le système, donc ne peut pas en sortir.
- ibanstring
^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$ - bicstring
^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$ - account_hintobject
Le compte sur lequel la transaction est passée — à distinguer de
counterparty_hint, qui désigne l'autre bout. Sans lui, une paire de virement ne peut pas être identifiée : le seul signal qui la désigne est « mon compte est ta contrepartie, et ta contrepartie est mon compte ». L'IBAN est réduit dès l'entrée, comme celui de la contrepartie : le numéro de compte n'entre pas dans le système.external_account_id: votre propre identifiant du compte, pour les cartes, portefeuilles et rails sans IBAN. Il sert à une chose : relier la transaction à une couverture déclarée sur le même compte. Il n'entre pas dans l'empreinte. Mesuré : il était accepté, puis jeté — la série n'avait aucune source, etlifecycle.staterestaitunknown. - ibanstring
^[A-Z]{2}[0-9]{2}[A-Z0-9]{10,30}$ - external_account_idstring · length 1–128
- external_transaction_idstring · length 1–128
Votre propre identifiant de transaction. Il n'entre pas dans l'empreinte : il dit comment vous nommez la transaction, pas ce qu'elle est. Il sert à empêcher deux envois d'une même transaction de se rapprocher l'un de l'autre.
Seuls descriptor et direction sont requis ; un champ facultatif n'est utile que s'il est lu (voir signals.used plus bas).
| Champ | Requis | Ce qu'il porte |
|---|---|---|
descriptor | oui | Le libellé tel qu'il figure sur le relevé, de 1 à 512 caractères. |
direction | oui | incoming ou outgoing. |
occurred_at | non | L'instant de la transaction, RFC 3339 avec décalage. |
amount | non | Un objet Money : value en chaîne, currency parmi les devises acceptées. |
payment | non | rail, mcc (quatre chiffres), card_present, network_merchant_id, raw_type. |
location_hint | non | country (ISO 3166-1 alpha-2, en majuscules), city, postal_code. |
counterparty_hint | non | iban, bic de l'autre bout. |
account_hint | non | iban ou external_account_id du compte sur lequel la transaction est passée. |
external_transaction_id | non | Votre propre identifiant de transaction, de 1 à 128 caractères. |
Un schéma fermé
Un champ inconnu n'est jamais ignoré : il est refusé par un 422 invalid_input dont errors[] porte field et code, ici unknown_field. La règle vaut aussi à l'intérieur de payment, location_hint, counterparty_hint et account_hint, et un tableau à la place de l'un de ces objets est refusé must_be_object.
occurred_at : un instant, pas une date
La valeur est un horodatage RFC 3339 avec décalage, par exemple 2026-09-09T07:31:00+02:00. Une date seule est refusée : minuit n'est pas le même jour partout, et la cadence des séries se lit au calendrier. Le code must_be_rfc3339_with_offset couvre la forme et le calendrier : 2026-02-30T00:00:00Z, un décalage +16:00 ou une seconde 23:59:60 sont refusés.
amount : un décimal en chaîne
Schéma Money
- valuestringrequis
Décimal en chaîne. Un flottant perdrait des centimes en silence. Le nombre de décimales admis dépend de la devise :
5.905 EURest refusé,100.50 ISKaussi (ISK et JPY n'en ont aucune, KWD en a trois).^-?(0|[1-9][0-9]{0,15})(\.[0-9]{1,4})?$ - currencystringrequis
Une devise hors de cette liste est refusée 422
unknown_currency.CADCHFDKKEURGBPISKJPYKWDNOKPLNSEKUSD
amount.value est une chaîne ("49.90"), jamais un nombre JSON : un flottant perdrait des centimes en silence. Le nombre de décimales admis dépend de la devise : 5.905 en EUR est refusé (amount_precision_exceeds_currency), 100.50 en ISK aussi, car ISK et JPY n'en ont aucune et KWD en a trois. currency doit appartenir à l'enum de Money.currency ; toute autre devise rend un 422 de type unknown_currency.
account_hint et counterparty_hint
L'IBAN, du compte comme de la contrepartie, est réduit dès l'entrée à son pays et son code banque : le numéro de compte n'entre pas dans le système. Sans account_hint, une paire de virement ne peut pas être identifiée. Pour une carte ou un portefeuille sans IBAN, external_account_id porte votre propre identifiant ; il sert à relier la transaction à une couverture d'observation déclarée sur le même compte et n'entre pas dans l'empreinte.
external_transaction_id n'entre pas non plus dans l'empreinte : il dit comment vous nommez la transaction, pas ce qu'elle est, et il empêche deux envois d'une même transaction d'être rapprochés l'un de l'autre.
Idempotency-Key
L'en-tête est facultatif. Sa valeur va de 1 à 255 octets d'ASCII visible (0x21 à 0x7E) ; une valeur vide ou hors alphabet rend un 400 invalid_idempotency_key.
| Situation | Réponse |
|---|---|
| Clé jamais vue | 201, l'enrichissement est créé. |
Même clé, même empreinte (fingerprint) | 200 : rejeu, la réponse d'origine rendue telle quelle, version de moteur comprise. |
| Même clé, empreinte différente | 409 idempotency_conflict. |
« Requête différente » se juge sur l'empreinte économique, et sur elle seule. Deux corps qui ne diffèrent que par external_transaction_id, account_hint ou counterparty_hint ont la même empreinte : le premier envoi est rejoué. Ne réutilisez donc pas une clé pour deux transactions que vous ne distinguez que par ces champs. Une clé est retenue au moins 24 heures ; au-delà, elle peut ouvrir une opération neuve.
Un 409 ou un 400 sur la clé arrive après une credential valide : il consomme un jeton de débit, comme un 422.
201 ou 200
Un 201 signifie qu'un enrichissement a été créé ; un 200, qu'une requête déjà traitée a été rejouée, sans rien créer. Les deux portent un en-tête Location vers la ressource, relisible pendant 30 jours par GET /transactions/v1/enrichments/{enrichment_id} (voir Relire un enrichissement). Le corps est un Enrichment : enrichment_id, created_at, contract_version, canonical_version, fingerprint, signals, resolution, puis relationships et series (listes vides quand aucune relation ni série n'a été conclue) et relationships_policy, dont unresolved dit pourquoi un détecteur n'a pas conclu.
signals.used et signals.ignored
signals.used liste les signaux reçus qui ont été transmis au moteur : au résolveur d'identité, ou à la base pour les séries, les relations et la couverture d'observation. signals.ignored liste ceux que personne n'a lus. Seuls les champs présents dans votre requête figurent dans l'une ou l'autre liste. Aujourd'hui, payment.mcc, payment.card_present, payment.network_merchant_id, location_hint.city, location_hint.postal_code et counterparty_hint.bic sont acceptés puis ignorés.
Ce bloc n'est pas une commodité : sans lui, vous pourriez envoyer un MCC pendant des mois, recevoir 201, et croire qu'il pèse sur la réponse. Vérifiez dans vos tests d'intégration qu'aucun champ dont vous dépendez n'apparaît dans ignored. signals.conflicting recopie resolution.signal_conflicts, pour que le bloc soit complet.
Le sens de policy.outcome
resolution.policy dit ce que la réponse affirme, et c'est le champ à utiliser pour décider. Il porte outcome, archetype, missing et policy_version.
outcome | Signification |
|---|---|
resolved | Toutes les dimensions exigées par l'archétype sont là. |
partial | La nature est connue, mais une dimension exigée manque, ou des signaux se contredisent et le moteur refuse de conclure. |
unresolved | La nature même de la transaction est inconnue. |
Ce que « résolu » veut dire dépend de la nature de la transaction. Un virement à une personne est resolved sans qu'aucune identité ne soit créée ; un retrait d'espèces est resolved sans marchand ; un achat carte, lui, exige sa contrepartie, et son absence rend partial avec missing: ["counterparty_entity"]. Un counterparty à null est un résultat, pas une panne, et counterparty_note dit pourquoi. Les nombres de resolution.confidence sont des constantes déclarées, ni probabilités ni seuils : voir Sémantique des scores.
Exemples
Avec curl :
curl -X POST https://api.noryo.app/transactions/v1/enrichments \
-H "Authorization: Bearer $NORYO_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: tx-2026-09-09-0042" \
-d '{
"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", "mcc": "5411", "card_present": true },
"location_hint": { "country": "FR" },
"account_hint": { "iban": "FR7630006000011234567890189" },
"external_transaction_id": "tx-2026-09-09-0042"
}'ShellAvec fetch :
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({
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", mcc: "5411", card_present: true },
location_hint: { country: "FR" },
account_hint: { iban: "FR7630006000011234567890189" },
external_transaction_id: "tx-2026-09-09-0042",
}),
});
const enrichment = await response.json();
console.log(response.status, enrichment.signals, enrichment.resolution.policy);TypeScriptSur cette requête, la réponse liste payment.mcc et payment.card_present dans signals.ignored.
Réponses d'erreur
Toute erreur rendue par l'API est un objet Problem avec type, title, status et request_id, detail quand l'API en donne un et, sur un 422, errors[]. Un 401 signale une credential absente, mal formée, d'une autre famille, inconnue ou révoquée ; un 403 un workspace inactif ; un 413 un corps trop volumineux ; un 415 un corps qui n'est pas du application/json ; un 429 une limite de débit atteinte, à rejouer après Retry-After. Les cas 400, 409 et 422 sont décrits plus haut. Le catalogue complet est sur Erreurs et sur /transaction-intelligence/errors/<code>.
Publié le 2026-09-17 11:19 UTC (version 1).