# Transaction types

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

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](https://developer.noryo.app/en-GB/transaction-intelligence/enrich-a-transaction) and know how to [read an enrichment](https://developer.noryo.app/en-GB/transaction-intelligence/read-an-enrichment).

## What the type asserts

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

| Field | Type | Content |
|---|---|---|
| `canonical` | string | The type that was retained, for example `card_purchase`. |
| `evidence` | string | The code of the rule that established it. A code, not an explanation. |
| `contradiction` | string or `null` | `null` 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.

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

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](https://developer.noryo.app/en-GB/transaction-intelligence/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.

| Question | Field | Example |
|---|---|---|
| How did the money move? | `payment.rail` on input; `resolution.payment.rail` and `resolution.payment.mechanism` on output | `card`, `sepa_direct_debit` |
| What happened? | `resolution.transaction_type.canonical` | `card_purchase` |
| What is the counterparty? | `resolution.counterparty.kind` | `brand` |
| What does the counterparty do? | `resolution.counterparty.semantics.activity.code` | `retail.grocery` |
| Where did the money go? | `resolution.category.slug` | `alimentation.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](https://developer.noryo.app/en-GB/transaction-intelligence/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](https://developer.noryo.app/en-GB/transaction-intelligence/recurring-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` code | Meaning |
|---|---|
| `rail_rule` | The rail alone decided. |
| `descriptor_rule` | The descriptor alone decided. |
| `descriptor_sense_rail_mechanism` | The descriptor gives the meaning, the rail the mechanism, with no conflict. |
| `rail_and_descriptor_agree` | Both say the same thing. |
| `descriptor_refines_rail` | The descriptor narrows down the rail. |
| `rail_refines_descriptor` | The rail narrows down the descriptor. |
| `rail_descriptor_conflict` | Contradiction; no certainty is manufactured. |
| `no_rule` | No rule applies. |
| `legacy_note_redacted` | Response 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).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `dimension` | string | yes | — |
| `signals` | array<string> | yes | — |
| `resolution` | string | yes | A sentence, in French: how the conflict was arbitrated. |
| `resolved_to` | string | yes | — |

## 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](https://developer.noryo.app/en-GB/transaction-intelligence/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](https://developer.noryo.app/en-GB/transaction-intelligence/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](https://developer.noryo.app/en-GB/transaction-intelligence/score-semantics).

## Reading the type in your code

```ts
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.
}
```

---

Canonical page: https://developer.noryo.app/en-GB/transaction-intelligence/transaction-types
