Skip to content
Sandbox

Transaction types

The transaction type says what happened; it is neither the payment rail, nor the category, nor the counterparty.

View as Markdown

Every enrichment carries, under resolution.transaction_type, a reading of what happened: a card purchase, for instance. This page explains what that block asserts, how it is established from the signals you send, and what it does not say. It assumes you have already enriched a transaction and know how to read an enrichment.

What the type asserts

resolution.transaction_type is an object with three fields, all required.

FieldTypeContent
canonicalstringThe type that was retained, for example card_purchase.
evidencestringThe code of the rule that established it. A code, not an explanation.
contradictionstring or nullnull when there is no contradiction to report, as in every example in the contract.

The contract puts it this way: transaction_type says what the transaction means, as opposed to payment, which says how the money moved. A salary received by SEPA transfer is both at once: the transfer is the rail, the salary is what that transfer stands for.

{
  "transaction_type": {
    "canonical": "card_purchase",
    "evidence": "rail_rule",
    "contradiction": null
  },
  "payment": {
    "rail": "card",
    "mechanism": "card_purchase"
  }
}
JSON

Here payment.mechanism and transaction_type.canonical hold the same value: two answers to two questions, which coincide without being the same thing. payment.note is a constant sentence, in French, that restates this difference; it is not data, so do not parse it. The full Resolution schema is in the reference.

Four questions that look alike

The contract separates four semantic readings that look alike and must not be confused: the entity kind, the activity, the category and the transaction type. None of them is derived from another. The rail joins them as a fifth question of a different nature: it does not say what the transaction means, but how the money moved.

QuestionFieldExample
How did the money move?payment.rail on input; resolution.payment.rail and resolution.payment.mechanism on outputcard, sepa_direct_debit
What happened?resolution.transaction_type.canonicalcard_purchase
What is the counterparty?resolution.counterparty.kindbrand
What does the counterparty do?resolution.counterparty.semantics.activity.coderetail.grocery
Where did the money go?resolution.category.slugalimentation.supermarche

A card purchase remains a card_purchase whatever its category; the category is a budget line, not a kind of event, and two transactions in the same category can have different types. The distinction between entity kind, activity and category is covered in Counterparties and entities. There is no category = subscription: a subscription is neither a category nor a transaction type, it is a commercial relationship model that only a series can establish.

How the type is established

Two input signals carry the type: the rail declared in payment.rail and the descriptor label. The nature of the counterparty, when it is known, can refine the result. The evidence field names the rule that was applied.

evidence codeMeaning
rail_ruleThe rail alone decided.
descriptor_ruleThe descriptor alone decided.
descriptor_sense_rail_mechanismThe descriptor gives the meaning, the rail the mechanism, with no conflict.
rail_and_descriptor_agreeBoth say the same thing.
descriptor_refines_railThe descriptor narrows down the rail.
rail_refines_descriptorThe rail narrows down the descriptor.
rail_descriptor_conflictContradiction; no certainty is manufactured.
no_ruleNo rule applies.
legacy_note_redactedResponse created by an earlier version, when this field carried the rule's internal note; the note has been removed and no rule code is returned.

The +counterparty_nature suffix, as in rail_rule+counterparty_nature, indicates that the nature of the counterparty refined the type.

The descriptor label and the direction (incoming or outgoing) are required in every request; the rail is optional. The direction is passed to the engine in every case: each example in the contract lists it in signals.used. The schema also accepts payment.raw_type; signals.used and signals.ignored tell you, response by response, whether it was read.

Note

The payment.mcc, payment.card_present and payment.network_merchant_id fields are accepted on input but are currently listed in signals.ignored: nobody reads them. An MCC sent for months has no bearing on the type. Read signals.used in each response rather than assuming a signal counts.

Type rules are versioned: resolution.versions.rules is txtype_v1 today. If you store a type, store that version alongside it; under another version the same value may no longer mean the same thing.

When the signals contradict each other

Sometimes the rail and the descriptor tell two different stories. The engine does not settle the matter silently:

  • evidence starts with rail_descriptor_conflict; read contradiction alongside it.
  • resolution.signal_conflicts[] describes each conflict: the dimension concerned, the signals in disagreement, a resolution sentence in French stating how the conflict was arbitrated, the resolved_to value that was retained, and one <signal>_suggests entry per signal for what each proposed. The same array is copied into signals.conflicting.
  • policy.outcome becomes partial: the engine declines to conclude.
  • confidence.transaction_type is capped by policy; the value 0 means "conflict with no common ancestor".

Schema SignalConflict

Two independent signals that contradict each other on one dimension, and how the conflict was arbitrated. No certainty is manufactured: the retained type is capped (`confidence.transaction_type` <= 0.25).

  • dimensionstringrequired
  • signalsarray<string>required
  • resolutionstringrequired

    A sentence, in French: how the conflict was arbitrated.

  • resolved_tostringrequired

What a type does not assert

It is not an identity. A card_purchase can be returned with counterparty at null: the nature of the transaction is known, the brand is not. That is the contract's non_resolue example, where policy.outcome is partial and policy.missing contains counterparty_entity, while transaction_type.canonical is card_purchase.

It is not a category. The type says what happened, not where the money went.

It is not a series. A type says nothing about recurrence: an isolated card_purchase is not a subscription, and an entity's recurring_model value is a plausibility of the trade, not a verdict. Only series[].type concludes; see Recurring series.

It is not a relationship. The type does not say that a refund answers a given purchase: that belongs to relationships[] and its type (refund_of, reversal_of, transfer_pair). The resolved type feeds those detectors (the contract's remboursement example carries the resolved_type_refund evidence), but the relationship is a separate object; see Relationships.

It is not the resolution archetype. policy.archetype says which policy applies and what "resolved" requires for that nature of transaction: a transfer to a person is resolved with no identity, a cash withdrawal is resolved with no merchant, a card purchase requires its counterparty. Its vocabulary is French and kept as is for ti_v1: achat_carte, prelevement, remboursement, retrait_especes, revenu, transfert, transfert_personne, frais_bancaires, impot, indetermine, inconnu (the very nature is unknown) and non_couvert (the nature is known but no policy covers it). transaction_type.canonical is the retained type; policy.missing can contain transaction_type when the archetype requires one and none was established.

Warning

confidence.transaction_type is not a probability: it is a constant declared by the rule, the same for every transaction that hits the same rule. Do not use it as a threshold or a rank; read policy.outcome. See Score semantics.

Reading the type in your 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) {
  // Contradictory signals: policy.outcome is "partial".
  // Do not conclude on `canonical` alone; read resolution.signal_conflicts.
}

if (missing.includes("transaction_type")) {
  // The archetype required a type and no rule supplied one.
}
TypeScript

Published on 2026-09-17 11:19 UTC (version 1).

Was this page helpful?