Skip to content
Sandbox

Relationships

How Noryo links two transactions of the same workspace through a refund_of, reversal_of or transfer_pair relationship, and how to read its role, its corroborations and its abstentions.

View as Markdown

What a relationship is

A relationship (Relationship) is an economic link between two transactions of the same workspace. It is rendered in the relationships array of the creation (Enrichment, see Enrich a transaction) and of the read-back (StoredEnrichment, see Read an enrichment).

A relationship is private to the workspace: it never leaves it and never feeds the entity graph, because a link between two private transactions is not a global truth. What does live in the graph: Counterparties and entities.

Schema Relationship

An economic link between two transactions of the same workspace. It never leaves that workspace and never feeds the entity graph: a relationship between two private transactions is not a global truth.

  • typestringrequired

    refund_of: the purchase took place, then the money came back; both entries remain. reversal_of: the original entry is erased; nothing happened. transfer_pair: two legs of a single movement.

    refund_ofreversal_oftransfer_pair
  • rolestringrequired

    The role of this transaction. peer for a symmetric relationship, where neither source nor target means anything.

    sourcetargetpeer
  • symmetricbooleanrequired
  • counterpartstringrequired

    The other transaction, by its enrichment identifier.

    ^enr_[0-9a-f-]{36}$
  • match_strengthobjectrequired

    This is not a probability. It is the share of the corroborating signals available for this type that are actually present. calibration is non_calibree as long as no ground truth has certified a quantified reading of this number.

  • valuenumber · 0.001–1required

    The denominator depends on the type: 5 for refund_of, 4 for reversal_of, 2 for transfer_pair. A 0.5 on a transfer pair (1/2) and a 0.5 on a reversal (2/4) are not the same strength. Read corroborations alongside. present only counts signals from the set specific to the type: a refund marker on a transfer pair stays visible in evidence but does not corroborate.

  • 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
  • corroborationsobjectrequired
  • presentinteger ≥ 0required
  • possibleinteger ≥ 1required
  • evidencearray<object>required

    The signals the relationship rests on, preconditions included. A relationship that cannot say why it exists is not created.

  • codestringrequired
  • valuestring
  • value_secondsinteger

    For within_window: the measured gap, in seconds. A duration, not a confidence.

  • window_secondsinteger· nullable

    The window of the relationship type, in seconds. A policy constant, not a measurement.

  • detector_versionstringrequired
  • statusstringrequired
    activesupersedeeinvalidee
  • created_atstring · date-time

The three types

The contract defines three values for type, no others.

typeContract definitionsymmetricPossible roles
refund_ofThe purchase took place, then the money came back: both entries remain.falsesource (the refund), target (the purchase)
reversal_ofThe original entry is erased: nothing happened.falsesource, target (oriented relationship; the contract shows no example)
transfer_pairTwo legs of a single movement.truepeer on both sides

A relationship is not a transaction type: resolution.transaction_type says what the entry is on its own, the relationship says which other one it is tied to (see Transaction types).

The role of this transaction

role is the role of the transaction you are reading, never the other one's: an enrichment carrying type: "refund_of" and role: "source" is the refund, and counterpart points to the purchase. The same link, read from the purchase, carries role: "target".

For transfer_pair, neither source nor target means anything: both legs carry role: "peer" and symmetric: true.

counterpart is the enrichment identifier of the other transaction (enr_…), readable directly:

curl https://api.noryo.app/transactions/v1/enrichments/enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301 \
  -H "Authorization: Bearer $NORYO_API_KEY"
Shell
Note

An enrichment can be read back for 30 days; beyond that, reading counterpart returns 404, like any expired identifier. See Sandbox limitations.

What makes a relationship possible

The detector only reads what you send: signals.used lists, in every creation response (Enrichment), the received signals passed to the engine (to the identity resolver, or to the store for series, relationships and observation coverage), without singling out the ones used for relationships. In the contract's refund example it contains descriptor, direction, occurred_at and amount: send the amount and the date as soon as you have them.

Two fields matter in particular:

  • account_hint and counterparty_hint: without both, a transfer pair cannot be identified, because the only signal that designates it is "my account is your counterparty, and your counterparty is my account";
  • external_transaction_id: your own transaction identifier, which stops two submissions of the same transaction from being matched to each other.
Warning

The IBANs in account_hint and counterparty_hint are reduced on entry to their country and bank code: the account number does not enter the system, so it cannot leave it. Sending them makes transfer_pair possible without exposing an account.

Corroborations and match strength

evidence lists the signals the relationship rests on, preconditions included. A relationship that cannot say why it exists is not created. Each element carries a code; within_window also carries value_seconds, the measured gap in seconds: a duration, not a confidence.

match_strength is not a probability: it is the share of the corroborating signals this type can produce that are actually present:

  • value is present / possible, rounded to three decimal places;
  • corroborations.present and corroborations.possible render the numerator and the denominator;
  • calibration is non_calibree: no statistical calibration is available.

The denominator depends on the type: five possible signals for refund_of, four for reversal_of, two for transfer_pair. A 0.5 on a transfer pair (one signal out of two) and a 0.5 on a reversal (two out of four) are not the same strength. present only counts signals from the set specific to the type: a refund marker on a transfer pair stays visible in evidence but does not corroborate.

This score class is called evidence_ratio. It is comparable between two relationships of the same type and the same detector_version, and never with series[].match_strength, which shares the name but has another denominator. The classes are described in Score semantics.

window_seconds is the window of the type, in seconds: a policy constant, not a measurement, to be read from the response rather than hard-coded.

Example

A refund attached to its purchase, as the contract shows it:

{
  "type": "refund_of",
  "role": "source",
  "symmetric": false,
  "counterpart": "enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "match_strength": {
    "value": 0.8,
    "calibration": "non_calibree",
    "corroborations": { "present": 4, "possible": 5 }
  },
  "evidence": [
    { "code": "same_counterparty_entity" },
    { "code": "refund_marker" },
    { "code": "within_window", "value_seconds": 259200 },
    { "code": "resolved_type_refund" }
  ],
  "window_seconds": 2592000,
  "detector_version": "rel_v1",
  "status": "active",
  "created_at": "2026-09-12T09:00:00Z"
}
JSON

Read corroborations: present is 4 out of possible 5, so value is 0.8, nothing more. The evidence list is not that count: it also carries the preconditions.

Why a relationship appears on read-back

A relationship links two transactions, and the second does not exist yet when the first is enriched. Monday's purchase cannot know about Friday's refund: its creation response carries relationships: []. That response is immutable: a replay with the same Idempotency-Key returns it exactly as it was, without the link.

Reading back, on the other hand, carries the link: GET /transactions/v1/enrichments/{enrichment_id} returns a StoredEnrichment with its relationships array. Read back on Friday, Monday's purchase carries refund_of with role: "target"; the refund carried it from its creation with role: "source". Keep enrichment_id and read back for the current state.

When nothing is concluded

Abstention is a result, not a silence: "no relationship" and "two equally plausible candidates" are two different things. That is what relationships_policy (RelationshipsPolicy) is for. It is rendered at creation (Enrichment) and absent from the read-back (StoredEnrichment).

Schema RelationshipsPolicy

Abstention is a result, not a silence. "No relationship" and "two equally plausible candidates" are two different things; a client that cannot tell them apart will conclude the first.

  • detector_versionstringrequired
  • calibrationstringrequired
    non_calibree
  • unresolvedarray<object>required

    Every detector that did not conclude says why. For a relationship: candidates_at_least (never the exact count, which would be an oracle on other people's transactions). For the series (type = series): occurrences, and a series outcome. A concluded relationship never appears here: it is in relationships. An earlier version wrongly listed the resolved and already_known outcomes here.

  • typestringrequired
  • outcomestringrequired
    ambiguoussingle_occurrenceno_recurrence_intervalno_series_keyexcluded_by_relationambiguous_membership
  • candidates_at_leastinteger ≥ 1
  • occurrencesinteger ≥ 0

unresolved lists every detector that did not conclude, with its reason:

  • for a relationship, type names the relationship type, outcome is ambiguous and candidates_at_least gives a floor, never the exact count, which would be an oracle on other people's transactions;
  • for the series, type is series, occurrences counts the occurrences seen and outcome carries a series outcome; one of them, excluded_by_relation, names a relationship as the reason (see Recurring series).

A concluded relationship never appears in unresolved: it is in relationships. Read the two together:

relationshipsunresolved (relationships)Reading
EmptyEmptyNo relationship established, no ambiguity met.
EmptyOne ambiguous entrySeveral equally plausible candidates: Noryo abstains.
One relationshipEmptyLink established; its signals are in evidence.

Status and version

The contract declares three values for status: active, supersedee and invalidee; the contract's examples are active. detector_version names the detector that produced the relationship: if you keep a match_strength value, keep detector_version next to it, because the same value, under another version, may no longer measure the same thing. created_at timestamps the relationship.

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

Was this page helpful?