Counterparties and entities
How identity resolution names a counterparty, what the entity graph keeps apart (brand, legal entity, intermediary) and what is deliberately left unresolved.
An enriched transaction carries, under resolution, what Noryo understood about the other end of the operation: the counterparty. This page explains how that identity is established, what the graph keeps apart, and what an absence means.
What identity resolution returns
resolution.counterparty is an object, or null. When present:
| Field | Content |
|---|---|
entity_id | The entity's stable, opaque identifier in the shared graph: a bare UUID, the same from one response and one workspace to the next. series[].evidence quotes it as entite:<uuid>. |
name | The entity's name, as the graph knows it. |
kind | What the entity is: brand or organisation. |
evidence | How the pattern matched the descriptor: match (jetons for a whole alias read in the descriptor, tronque for a fragment cut short by the bank), pattern (the matched alias), coverage, and covered on the truncated lane. |
semantics | What the graph knows about the entity (see below), or null when it knows nothing. |
coverage and covered are ratios of lengths, properties of the text rather than proof of identity: do not use them as a threshold (see Score semantics). Alongside, counterparty_type qualifies the counterparty as a string (merchant, unknown in the contract's examples).
Schema Resolution
The shape returned by the `ti_v4` engine.
- policyobject
What "RESOLVED" means depends on the nature of the transaction. A transfer to a person is
resolvedwithout any identity being created: that is the right answer, not a failure. A cash withdrawal isresolvedwithout a merchant. A card purchase, however, requires its counterparty. - outcomestringrequired
resolved: every dimension the archetype requires is present.partial: the nature is known, but a required dimension is missing, or signals contradict each other, in which case the engine declines to conclude.unresolved: even the nature is unknown. Technical errors (401, 409, 503) are never a semantic outcome: they do not carry this field.resolvedpartialunresolved - archetypestringrequired
The vocabulary is french and it is the database's (the vocabulary of resolution archetypes), kept as is for
ti_v1.inconnu: the very nature of the transaction is unknown.non_couvert: the nature is known but no policy covers it (missing = [resolution_policy]).achat_cartefrais_bancairesimpotindetermineprelevementremboursementretrait_especesrevenutransferttransfert_personneinconnunon_couvert - missingarray<string>required
The dimensions required by the archetype that are absent.
- policy_versionstringrequired
ti_resolution_v1 - normalizedobject
What the normaliser read. Strings, not scores.
- descriptorstring
- signalstring
- usefulstring
- residualstring
- normalizer_versionstring
- counterpartyobject· nullable
nullis a result, not a failure: an honest unknown. - entity_idstring · uuid
The stable identifier of the entity in the graph, as a bare UUID, with no prefix. It is opaque: do not assume any structure in it. The same identifier designates the same entity from one response to the next and from one workspace to the next (the graph is shared; the identifier is not data belonging to your workspace). It is quoted as is in
series[].evidencein the formentite:<uuid>. - namestring
- kindstring
brandorganisation - evidenceobject
How the pattern matched the descriptor. Two length ratios, which are properties of the text and not proof of identity.
matchsays by which route (jetons: a whole alias in the descriptor;tronque: a descriptor fragment cut short by the bank).patternis the matched alias, in plain text. - matchstring
jetonstronque - patternstring
- coveragenumber ≥ 0
- coverednumber · 0.6–1
- semanticsCounterpartySemantics· nullable
Four different things that look alike, and that this block refuses to confuse. The entity type (
counterparty.kind: brand, legal entity, payment intermediary) says what it is. The activity (activity) says what it does, in industry names. The category (category, outside this object) says where the money went, in budget lines. The transaction type (transaction_type) says what happened. None is derived from another. And there is nocategory = subscription: a subscription is not a kind of spending, it is a model of commercial relationship.nullwhen the graph knows nothing about this entity: an abstention, not a failure. - activityobject· nullable
The trade, on two levels (
parentthencode). It stops at the parent when the source stops there: autilityrole says "network" without saying which one, andutilitiesis then returned, neverutilities.electricity. What is not known is not filled in. - codestring
- labelstring
- parentstring· nullable
- recurring_modelstring· nullable
What is usual in this trade, not a verdict on this transaction. A streaming service sells by subscription; an electricity supplier bills repeatedly without anyone subscribing; a supermarket sells per purchase. It is a plausibility that reinforces a rhythm already observed in the workspace. On its own, it never concludes:
commonly_subscription_basedon an isolated transaction does not make a subscription, and it is the series (series[].type = subscription) that decides.commonly_subscription_basedcommonly_recurring_but_not_subscriptionusually_one_offboth_recurring_and_one_offunknownnull - rolesarray<string>
The roles the entity holds in the graph (
merchant,utility,government,financial_institution,payment_intermediary…). A role is not a trade, but it says enough to derive one when no catalogue covers the entity. - legal_entityobject· nullable
A brand is not a legal entity. "Netflix" is what you read on the statement; "Netflix Inc" is what contracts. The two exist separately in the graph, linked by
brand_of, because merging them would erase the day a brand changes owner.nullwhen no parent company is known, not when there is none. - entity_idstring · uuid
The same identifier space as
counterparty.entity_id: a bare, opaque, stable UUID. - namestring
- provenanceobject· nullable
Where what we assert comes from.
natureranks knowledge by declared authority, in this order:curated(a person verified it),deterministic(derived mechanically from a fact already held),provider(a third party states it),inferred(we deduced it),participant(a single client said so). This order is declared, not deduced from a number: without it, a deduced 0.95 would beat a verified 0.90.strengthonly breaks ties at equal nature, and is not calibrated. And no global knowledge comes from a single client: what a workspace observes stays in that workspace. Aparticipantnature can never become a truth of the shared graph. - naturestring
curateddeterministicproviderinferredparticipant - sourcestring
- strengthnumber · 0–1
- calibrationstring
Always
non_calibreein this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it.non_calibree - evidenceobject
How the provenance was established. The keys vary:
derivation(categorie_de_depense_vers_activite,role_vers_activite…),categorie_sourceorrole, andnote, a constant sentence in French. None of these values is a number. - counterparty_typestring
- counterparty_notestring· nullable
Says why no identity was established.
- categoryobject· nullable
A category is not an identity. When the descriptor names a trade ("BOULANGERIE DUPONT", "PHARMACIE DE LA GARE") rather than a known merchant, the response returns that trade here and leaves
counterpartyatnull. Returning "Boulangerie" as the counterparty would amount to naming a shop aisle instead of a business. And it is a budget line, not a trade. This category answers "where did this money go?", from the person's point of view. What the entity does is another field,counterparty.semantics.activity, and the two do not substitute for each other: EDF sells electricity (activity = utilities.electricity) and the spend files underlogement.electricite.sourcesays where the line comes from:entity_graphwhen it is carried by the recognised merchant,generic_trade_entrywhen it is deduced from a trade word read in the descriptor. The first is bounded byconfidence.entity: a poorly recognised merchant does not yield a reliable category. When a rule of your organisation set the category (seecustomization),sourceis absent:slugandlabelthen come from the Noryo catalogue, and neither Noryo origin would be true. - slugstring
- labelstring· nullable
- sourcestring
entity_graphgeneric_trade_entry - identity_abstentionstring· nullable
Why the engine declined to name, when it could have.
trade_not_identity: a trade was recognised, not a merchant.atm_operator_not_counterparty: on a cash movement, the recognised bank operates the cash machine; it is not on the other side.account_bank_not_counterparty: on a bank fee, it is the account's own bank, and naming it teaches nothing.person_named_not_brand: the descriptor names a person, and the recognised merchant belongs to the transfer reference. An abstention is not a failure:policy.outcomecan remainresolvedif the archetype did not require an entity. - intermediariesarray<object>
The payment facilitators read in the descriptor (
PAYPAL *NETFLIXyields one). They are never the counterparty.evidenceis a code and a source, and no longer the matching pattern itself, which is an internal rule. - namestringrequired
- rolestringrequired
- entity_idstring · uuid· nullable
The facilitator's entity in the graph, when it is there.
- carries_sub_merchantboolean
- evidenceobjectrequired
- codestringrequired
The facilitator's code in the reference catalogue.
- sourcestringrequired
referentiel_facilitateurs - transaction_typeobject
What the transaction means, as opposed to
payment, which says how the money moved.evidenceis a rule code, not an explanation: the rule's internal note came out here in an earlier version; the responses created at that time carrylegacy_note_redacted. - canonicalstringrequired
- evidencestringrequired
rail_rule: the rail alone decided.descriptor_rule: the descriptor alone.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/rail_refines_descriptor: one narrows down the other.rail_descriptor_conflict: contradiction, no certainty manufactured (seesignal_conflicts).no_rule: nothing applies. The+counterparty_naturesuffix says that the nature of the counterparty refined the type.^(rail_rule|descriptor_rule|descriptor_sense_rail_mechanism|rail_and_descriptor_agree|descriptor_refines_rail|rail_refines_descriptor|rail_descriptor_conflict|no_rule)(\+counterparty_nature)?$|^legacy_note_redacted$ - contradictionstring· nullablerequired
- paymentobject· nullable
What the engine understood of the rail and the payment mechanism.
- railstring· nullable
- mechanismstring· nullable
- notestring· nullable
A constant explanatory sentence, in French, restating the difference between
railandtransaction_type. Not data. - signal_conflictsarray<SignalConflict>
The signals that contradict each other. When there are any,
policy.outcomeispartial: the engine declines to conclude. - dimensionstringrequired
- signalsarray<string>required
- resolutionstringrequired
A sentence, in French: how the conflict was arbitrated.
- resolved_tostringrequired
- submerchant_lanestring
The outcome of the "sub-merchant" lane (a facilitator carries the real merchant).
none: lane not opened.no_candidate: opened, no entity.resolved: a single entity.ambiguous: several, abstention. Absent on the oldest responses, created before this lane existed.noneno_candidateresolvedambiguous - truncated_lanestring
The outcome of the "truncated descriptor" lane (the bank cut the name short). Same values as
submerchant_lane. Absent on the oldest responses, created before this lane existed.noneno_candidateresolvedambiguous - confidenceobject
The name is kept for
ti_v1; this is not a confidence. Each of these numbers is a declared constant, set by the person who wrote the pattern or the rule, and read as is. Every transaction that hits the same pattern receives the same number. Measured on 644 descriptors: precision as a function of the threshold is not monotone: 0.85 gives 0.86 precision, 0.88 gives 0.33, 0.92 gives 0.25, 0.97 gives 0.79. This number does not rank right above wrong. Use it neither as a probability nor as a threshold: usepolicy.outcome.0has a precise meaning per field: abstention (entity), unknown role (counterparty), conflict with no common ancestor (transaction_type).locationis always0: nothing is resolved on this axis in this version. - entitynumber · 0–1required
- counterpartynumber · 0–1required
- transaction_typenumber · 0–1required
- categorynumber · 0–1required
- locationnumberrequired
0 - calibrationstringrequired
Always
non_calibreein this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it.non_calibree - timings_msobject
Durations, in milliseconds. Nothing to do with a confidence.
- normalizationnumber
- entity_resolutionnumber
- submerchantnumber
- totalnumber
- versionsobject
The versions that produced this response. A client that keeps a number must keep the version alongside it: the same value, under another version, may no longer measure the same thing.
- graphstring
The ontology of the entity graph (
onto_v1). - rulesstring
The transaction type rules (
txtype_v1). - enginestringrequired
The resolution engine (
ti_v4). - resolution_policystringrequired
The same value as
policy.policy_version. - calibrationstringrequired
Always
non_calibreein this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it.non_calibree
The entity graph: brand, legal entity, intermediary
The graph separates three things a bank statement blends together.
The brand is what you read on the statement: "Netflix", "Monoprix". The response returns it as the counterparty, with kind = brand.
The legal entity is what contracts: "Netflix Inc". It lives separately in the graph, linked to the brand by brand_of, and is returned under counterparty.semantics.legal_entity (entity_id, name). Merging the two would erase the day a brand changes owner. legal_entity is null when no parent company is known, not when there is none.
The payment intermediary is the facilitator read in the descriptor: PAYPAL *NETFLIX yields one. It is never the counterparty. It is returned in resolution.intermediaries[]: name, role, entity_id when it is in the graph, carries_sub_merchant, and an evidence (code and source). submerchant_lane reports the outcome of the sub-merchant lane, opened when a facilitator carries the real merchant: none (not opened), no_candidate, resolved (a single entity) or ambiguous (several candidates, abstention). truncated_lane does the same for a descriptor cut short by the bank.
Four things that look alike
Four fields answer four questions, and none is derived from another.
| Question | Field | Example |
|---|---|---|
| What is it? (entity type) | counterparty.kind | brand |
What does it do? (industry names, parent then code) | counterparty.semantics.activity | retail.grocery, parent retail |
| Where did the money go? (budget lines) | resolution.category | alimentation.supermarche |
| What happened? | resolution.transaction_type.canonical | card_purchase |
EDF illustrates the gap between activity and category: the entity sells electricity (activity = utilities.electricity) and the spend files under logement.electricite. Activity stops at the parent when the source stops there: a utility role yields utilities, never utilities.electricity. Transaction type, for its part, is detailed in Transaction types.
There is no category = subscription. A subscription is not a kind of spend, it is a model of commercial relationship: it is read in recurring_model and, above all, in the series.
Nor is a category an identity. When the descriptor names a trade ("BOULANGERIE DUPONT") rather than a known merchant, the response returns that trade in category with source = generic_trade_entry and leaves counterparty at null: "Boulangerie" would name a shop aisle, not a business. source = entity_graph signals, on the contrary, a category carried by the recognised merchant.
recurring_model: a relationship model, not a category
counterparty.semantics.recurring_model says what is usual in that trade: commonly_subscription_based (a streaming service sells by subscription), commonly_recurring_but_not_subscription (an electricity supplier bills repeatedly without anyone subscribing), usually_one_off (a supermarket sells per purchase), both_recurring_and_one_off, unknown, or null.
It is not a verdict on the transaction you sent, but a plausibility that reinforces a rhythm already observed in your workspace; on its own, it never concludes. commonly_subscription_based on an isolated transaction does not make a subscription: the series decides, through series[].type = subscription. And a counterparty is not a series: one merchant can carry several series, and a series never reduces to "every transaction with that counterparty" (see Recurring series).
Provenance: where a claim comes from
counterparty.semantics.provenance says where the knowledge comes from. nature ranks it by declared authority, in this order: curated (a person verified it), deterministic (derived mechanically from a fact already held), provider (a third party states it), inferred (Noryo deduced it), participant (a single client said so). The order is declared, never deduced from a number: strength (class authority_tiebreak) only breaks ties between two sources of the same nature, and is not calibrated (calibration = non_calibree).
What a workspace observes stays in that workspace: a participant nature never becomes a truth of the shared graph.
roles lists the roles the entity holds in the graph (among them merchant, utility, government, financial_institution, payment_intermediary); a role is not a trade, but it says enough to derive one when no catalogue covers the entity.
Schema CounterpartySemantics
Four different things that look alike, and that this block refuses to confuse. The entity type (`counterparty.kind`: brand, legal entity, payment intermediary) says what it is. The activity (`activity`) says what it does, in industry names. The category (`category`, outside this object) says where the money went, in budget lines. The transaction type (`transaction_type`) says what happened. None is derived from another. And there is no `category = subscription`: a subscription is not a kind of spending, it is a model of commercial relationship. `null` when the graph knows nothing about this entity: an abstention, not a failure.
- activityobject· nullable
The trade, on two levels (
parentthencode). It stops at the parent when the source stops there: autilityrole says "network" without saying which one, andutilitiesis then returned, neverutilities.electricity. What is not known is not filled in. - codestring
- labelstring
- parentstring· nullable
- recurring_modelstring· nullable
What is usual in this trade, not a verdict on this transaction. A streaming service sells by subscription; an electricity supplier bills repeatedly without anyone subscribing; a supermarket sells per purchase. It is a plausibility that reinforces a rhythm already observed in the workspace. On its own, it never concludes:
commonly_subscription_basedon an isolated transaction does not make a subscription, and it is the series (series[].type = subscription) that decides.commonly_subscription_basedcommonly_recurring_but_not_subscriptionusually_one_offboth_recurring_and_one_offunknownnull - rolesarray<string>
The roles the entity holds in the graph (
merchant,utility,government,financial_institution,payment_intermediary…). A role is not a trade, but it says enough to derive one when no catalogue covers the entity. - legal_entityobject· nullable
A brand is not a legal entity. "Netflix" is what you read on the statement; "Netflix Inc" is what contracts. The two exist separately in the graph, linked by
brand_of, because merging them would erase the day a brand changes owner.nullwhen no parent company is known, not when there is none. - entity_idstring · uuid
The same identifier space as
counterparty.entity_id: a bare, opaque, stable UUID. - namestring
- provenanceobject· nullable
Where what we assert comes from.
natureranks knowledge by declared authority, in this order:curated(a person verified it),deterministic(derived mechanically from a fact already held),provider(a third party states it),inferred(we deduced it),participant(a single client said so). This order is declared, not deduced from a number: without it, a deduced 0.95 would beat a verified 0.90.strengthonly breaks ties at equal nature, and is not calibrated. And no global knowledge comes from a single client: what a workspace observes stays in that workspace. Aparticipantnature can never become a truth of the shared graph. - naturestring
curateddeterministicproviderinferredparticipant - sourcestring
- strengthnumber · 0–1
- calibrationstring
Always
non_calibreein this version, and that is a constraint, not a convention: the value is a vocabulary in the database, the columns that carry it reference it, and a guard rejects any literal outside the vocabulary at every producer. When a real calibration exists, it will enter through a declared migration, together with the corpus that supports it.non_calibree - evidenceobject
How the provenance was established. The keys vary:
derivation(categorie_de_depense_vers_activite,role_vers_activite…),categorie_sourceorrole, andnote, a constant sentence in French. None of these values is a number.
policy.outcome: what "resolved" means
resolution.policy reports the outcome of resolution:
outcome:resolved(every dimension the archetype requires is present),partial(the nature is known, but a required dimension is missing or signals contradict each other),unresolved(the very nature of the transaction is unknown).archetype: the nature of the transaction, in the database's French vocabulary:achat_carte,prelevement,transfert_personne,retrait_especes,frais_bancaires,impot,remboursement,revenu,transfert,indetermine,inconnu,non_couvert.missing: the required dimensions that are absent (counterparty_entity,counterparty_type,transaction_type,resolution_policy).
What "resolved" means depends on the archetype. A transfer to a person is resolved without any identity being created: that is the right answer, not a failure. A cash withdrawal is resolved without a merchant. A card purchase, however, requires its counterparty: without it, outcome is partial and missing contains counterparty_entity.
To decide on a treatment, read policy.outcome, never the numbers under confidence: those are constants declared per pattern, not probabilities.
What stays unresolved, and why
An absence is a result, and the response says which one.
counterparty=null, withcounterparty_note: no identity was established, and the note says why.identity_abstention: the engine declined to name when it could have:trade_not_identity(a trade was recognised, not a merchant),atm_operator_not_counterparty(the recognised bank operates the cash machine),account_bank_not_counterparty(on a bank fee, naming the account's own bank teaches nothing),person_named_not_brand(the descriptor names a person).policy.outcomecan remainresolvedif the archetype did not require an entity.signal_conflictsnot empty: two independent signals contradict each other, andpolicy.outcomebecomespartial.- Location: nothing is resolved on that axis in this version, and
confidence.locationis always0. Thecounterparty_hintIBAN is reduced on entry to its country and bank code.
Read signals.used and signals.ignored: it is the only way to know whether a signal you sent weighs on the response.
Example
The contract's achat_carte example:
curl -X POST https://api.noryo.app/transactions/v1/enrichments \
-H "Authorization: Bearer $NORYO_API_KEY" \
-H "Content-Type: application/json" \
-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"
}'ShellExcerpt of the resolution returned:
{
"policy": { "outcome": "resolved", "archetype": "achat_carte", "missing": [], "policy_version": "ti_resolution_v1" },
"counterparty": {
"entity_id": "b83d1756-258c-4541-adc7-54eec83dbd3c",
"name": "Monoprix",
"kind": "brand",
"evidence": { "match": "jetons", "pattern": "MONOPRIX", "coverage": 0.4 },
"semantics": {
"roles": ["merchant"],
"activity": { "code": "retail.grocery", "label": "alimentation generale", "parent": "retail" },
"legal_entity": { "entity_id": "5ecee4f8-fab0-444f-8711-29ff6e3f875a", "name": "Groupe Casino" },
"recurring_model": "usually_one_off",
"provenance": { "nature": "curated", "source": "catalogue_cure", "strength": 0.75, "calibration": "non_calibree" }
}
},
"counterparty_type": "merchant",
"counterparty_note": null,
"category": { "slug": "alimentation.supermarche", "label": "Supermarché", "source": "entity_graph" },
"identity_abstention": null,
"intermediaries": [],
"transaction_type": { "canonical": "card_purchase", "evidence": "rail_rule", "contradiction": null }
}JSONPublished on 2026-09-17 11:19 UTC (version 1).