Types de transaction
Le type de transaction dit ce qui s'est passé ; il ne se confond ni avec le rail de paiement, ni avec la catégorie, ni avec la contrepartie.
Chaque enrichissement porte, sous resolution.transaction_type, une lecture de ce qui s'est passé : un achat par carte, par exemple. Cette page décrit ce que ce bloc affirme, comment il est établi à partir des signaux reçus, et ce qu'il ne dit pas. Elle suppose que vous avez déjà enrichi une transaction et que vous savez relire un enrichissement.
Ce que le type affirme
resolution.transaction_type est un objet à trois champs, tous requis.
| Champ | Type | Contenu |
|---|---|---|
canonical | chaîne | Le type retenu, par exemple card_purchase. |
evidence | chaîne | Le code de la règle qui l'a établi. Un code, pas une explication. |
contradiction | chaîne ou null | null quand aucune contradiction n'est à signaler, comme dans tous les exemples du contrat. |
Le contrat le formule ainsi : transaction_type dit ce que la transaction signifie, par opposition à payment, qui dit comment l'argent a circulé. Un salaire reçu par virement SEPA est les deux à la fois : le virement est le rail, le salaire est ce que ce virement représente.
{
"transaction_type": {
"canonical": "card_purchase",
"evidence": "rail_rule",
"contradiction": null
},
"payment": {
"rail": "card",
"mechanism": "card_purchase"
}
}JSONIci, payment.mechanism et transaction_type.canonical portent la même valeur : deux réponses à deux questions, qui coïncident sans se confondre. payment.note est une phrase constante, en français, qui rappelle cette différence ; ce n'est pas une donnée, ne l'analysez pas. Le schéma Resolution complet est dans la référence.
Quatre questions qui se ressemblent
Le contrat sépare quatre lectures sémantiques qui se ressemblent et ne se confondent pas : le type d'entité, l'activité, la catégorie et le type de transaction. Aucune ne se déduit d'une autre. Le rail s'y ajoute comme une cinquième question, d'une autre nature : il ne dit pas ce que la transaction signifie, mais comment l'argent a circulé.
| Question | Champ | Exemple |
|---|---|---|
| Comment l'argent a-t-il circulé ? | payment.rail en entrée ; resolution.payment.rail et resolution.payment.mechanism en sortie | card, sepa_direct_debit |
| Que s'est-il passé ? | resolution.transaction_type.canonical | card_purchase |
| Qu'est-ce que la contrepartie ? | resolution.counterparty.kind | brand |
| Que fait la contrepartie ? | resolution.counterparty.semantics.activity.code | retail.grocery |
| À quoi l'argent est-il allé ? | resolution.category.slug | alimentation.supermarche |
Un achat carte reste un card_purchase quelle que soit sa catégorie ; la catégorie est une ligne de budget, pas une sorte d'événement, et deux transactions de même catégorie peuvent avoir des types différents. La distinction entre nature d'entité, activité et catégorie est détaillée dans Contreparties et entités. Il n'existe aucune category = subscription : un abonnement n'est ni une catégorie ni un type de transaction, c'est un modèle de relation que seule une série peut établir.
Comment le type est établi
Deux signaux d'entrée portent le type : le rail déclaré dans payment.rail et le libellé descriptor. La nature de la contrepartie, quand elle est connue, peut affiner le résultat. Le champ evidence nomme la règle appliquée.
Code evidence | Signification |
|---|---|
rail_rule | Le rail seul a décidé. |
descriptor_rule | Le libellé seul a décidé. |
descriptor_sense_rail_mechanism | Le libellé donne le sens, le rail le mécanisme, sans conflit. |
rail_and_descriptor_agree | Les deux disent la même chose. |
descriptor_refines_rail | Le libellé précise le rail. |
rail_refines_descriptor | Le rail précise le libellé. |
rail_descriptor_conflict | Contradiction ; aucune certitude n'est fabriquée. |
no_rule | Aucune règle ne s'applique. |
legacy_note_redacted | Réponse créée par une version antérieure, où ce champ portait la note interne de la règle ; la note a été retirée et aucun code de règle n'est rendu. |
Le suffixe +counterparty_nature (par exemple rail_rule+counterparty_nature) indique que la nature de la contrepartie a affiné le type.
Le libellé descriptor et la direction direction (incoming ou outgoing) sont requis dans toute requête ; le rail est facultatif. La direction est transmise au moteur dans tous les cas : chaque exemple du contrat la liste dans signals.used. Le schéma accepte aussi payment.raw_type ; signals.used et signals.ignored vous disent, réponse par réponse, s'il a été lu.
Les champs payment.mcc, payment.card_present et payment.network_merchant_id sont acceptés en entrée mais figurent aujourd'hui dans signals.ignored : personne ne les lit. Un MCC envoyé pendant des mois ne pèse pas sur le type. Lisez signals.used dans chaque réponse plutôt que de supposer qu'un signal compte.
Les règles de type sont versionnées : resolution.versions.rules vaut txtype_v1 aujourd'hui. Si vous stockez un type, stockez cette version à côté ; sous une autre version, la même valeur peut ne plus dire la même chose.
Quand les signaux se contredisent
Quand le rail et le libellé racontent deux histoires, le moteur ne tranche pas en silence :
evidencecommence parrail_descriptor_conflict; lisezcontradictionà côté.resolution.signal_conflicts[]décrit chaque conflit : ladimensionconcernée, lessignalsen désaccord, une phraseresolutionen français qui dit comment le conflit a été arbitré, la valeurresolved_toretenue, et une entrée<signal>_suggestspar signal pour ce que chacun proposait. Le même tableau est copié danssignals.conflicting.policy.outcomepasse àpartial: le moteur refuse de conclure.confidence.transaction_typeest plafonné par la politique ; la valeur0signifie « conflit sans ancêtre commun ».
Schéma SignalConflict
Deux signaux indépendants qui se contredisent sur une dimension, et comment le conflit a été arbitré. Aucune certitude n'est fabriquée : le type retenu est plafonné (`confidence.transaction_type` <= 0,25).
- dimensionstringrequis
- signalsarray<string>requis
- resolutionstringrequis
Une phrase, en français : comment le conflit a été arbitré.
- resolved_tostringrequis
Ce qu'un type n'affirme pas
Ce n'est pas une identité. Un card_purchase peut être rendu avec counterparty à null : la nature de la transaction est connue, l'enseigne ne l'est pas. C'est l'exemple non_resolue du contrat : policy.outcome vaut partial, policy.missing contient counterparty_entity, et le type est bien card_purchase.
Ce n'est pas une catégorie. Le type dit ce qui s'est passé, pas à quoi l'argent est allé.
Ce n'est pas une série. Un type ne dit rien de la récurrence : un card_purchase isolé n'est pas un abonnement, et la valeur recurring_model d'une entité est une plausibilité de métier, pas un verdict. Seul series[].type conclut ; voir Séries récurrentes.
Ce n'est pas une relation. Le type ne dit pas qu'un remboursement répond à tel achat : cela relève de relationships[] et de son type (refund_of, reversal_of, transfer_pair). Le type résolu alimente ces détecteurs (l'exemple remboursement du contrat porte la preuve resolved_type_refund), mais la relation est un objet à part ; voir Relations.
Ce n'est pas l'archétype de résolution. policy.archetype dit quelle politique s'applique et ce que « résolu » exige pour cette nature de transaction : un virement à une personne est resolved sans identité, un retrait d'espèces est resolved sans marchand, un achat carte exige sa contrepartie. Son vocabulaire est français et conservé tel quel pour ti_v1 : achat_carte, prelevement, remboursement, retrait_especes, revenu, transfert, transfert_personne, frais_bancaires, impot, indetermine, inconnu (la nature même est inconnue) et non_couvert (la nature est connue mais aucune politique ne la couvre). transaction_type.canonical est le type retenu ; policy.missing peut contenir transaction_type quand l'archétype l'exige et qu'il manque.
confidence.transaction_type n'est pas une probabilité : c'est une constante déclarée par la règle, la même pour toute transaction qui touche la même règle. Ne l'utilisez ni comme seuil ni comme rang ; lisez policy.outcome. Voir Sémantique des scores.
Lire le type dans votre code
const enrichmentId = "enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301";
const response = await fetch(
`https://api.noryo.app/transactions/v1/enrichments/${enrichmentId}`,
{ headers: { Authorization: `Bearer ${process.env.NORYO_API_KEY}` } },
);
const enrichment = await response.json();
const type = enrichment.resolution.transaction_type;
const missing = enrichment.resolution.policy?.missing ?? [];
if (type?.evidence?.startsWith("rail_descriptor_conflict") || enrichment.resolution.signal_conflicts?.length) {
// Signaux contradictoires : policy.outcome vaut "partial".
// Ne concluez pas sur `canonical` seul ; lisez resolution.signal_conflicts.
}
if (missing.includes("transaction_type")) {
// L'archétype exigeait un type et aucune règle ne l'a fourni.
}TypeScriptPublié le 2026-09-17 11:19 UTC (version 1).