Skip to content
Sandbox

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.

View as Markdown

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:

FieldContent
entity_idThe 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>.
nameThe entity's name, as the graph knows it.
kindWhat the entity is: brand or organisation.
evidenceHow 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.
semanticsWhat 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 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.

  • 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

    null is 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[].evidence in the form entite:<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. match says by which route (jetons: a whole alias in the descriptor; tronque: a descriptor fragment cut short by the bank). pattern is 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 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 (parent then code). It stops at the parent when the source stops there: a utility role says "network" without saying which one, and utilities is then returned, never utilities.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_based on 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. null when 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. nature ranks 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. strength only 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. A participant nature can never become a truth of the shared graph.

  • naturestring
    curateddeterministicproviderinferredparticipant
  • sourcestring
  • strengthnumber · 0–1
  • calibrationstring

    Always non_calibree in 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_source or role, and note, 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 counterparty at null. 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 under logement.electricite. source says where the line comes from: entity_graph when it is carried by the recognised merchant, generic_trade_entry when it is deduced from a trade word read in the descriptor. The first is bounded by confidence.entity: a poorly recognised merchant does not yield a reliable category. When a rule of your organisation set the category (see customization), source is absent: slug and label then 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.outcome can remain resolved if the archetype did not require an entity.

  • intermediariesarray<object>

    The payment facilitators read in the descriptor (PAYPAL *NETFLIX yields one). They are never the counterparty. evidence is 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. evidence is a rule code, not an explanation: the rule's internal note came out here in an earlier version; the responses created at that time carry legacy_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 (see signal_conflicts). no_rule: nothing applies. The +counterparty_nature suffix 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 rail and transaction_type. Not data.

  • signal_conflictsarray<SignalConflict>

    The signals that contradict each other. When there are any, policy.outcome is partial: 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: use policy.outcome. 0 has a precise meaning per field: abstention (entity), unknown role (counterparty), conflict with no common ancestor (transaction_type). location is always 0: 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_calibree in 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_calibree in 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 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.

QuestionFieldExample
What is it? (entity type)counterparty.kindbrand
What does it do? (industry names, parent then code)counterparty.semantics.activityretail.grocery, parent retail
Where did the money go? (budget lines)resolution.categoryalimentation.supermarche
What happened?resolution.transaction_type.canonicalcard_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.

Note

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 (parent then code). It stops at the parent when the source stops there: a utility role says "network" without saying which one, and utilities is then returned, never utilities.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_based on 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. null when 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. nature ranks 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. strength only 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. A participant nature can never become a truth of the shared graph.

  • naturestring
    curateddeterministicproviderinferredparticipant
  • sourcestring
  • strengthnumber · 0–1
  • calibrationstring

    Always non_calibree in 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_source or role, and note, 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, with counterparty_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.outcome can remain resolved if the archetype did not require an entity.
  • signal_conflicts not empty: two independent signals contradict each other, and policy.outcome becomes partial.
  • Location: nothing is resolved on that axis in this version, and confidence.location is always 0. The counterparty_hint IBAN 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"
  }'
Shell

Excerpt 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 }
}
JSON

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

Was this page helpful?