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.
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.
peerfor 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.
calibrationisnon_calibreeas 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 forreversal_of, 2 fortransfer_pair. A0.5on a transfer pair (1/2) and a0.5on a reversal (2/4) are not the same strength. Readcorroborationsalongside.presentonly counts signals from the set specific to the type: a refund marker on a transfer pair stays visible inevidencebut does not corroborate. - 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 - 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.
type | Contract definition | symmetric | Possible roles |
|---|---|---|---|
refund_of | The purchase took place, then the money came back: both entries remain. | false | source (the refund), target (the purchase) |
reversal_of | The original entry is erased: nothing happened. | false | source, target (oriented relationship; the contract shows no example) |
transfer_pair | Two legs of a single movement. | true | peer 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"ShellAn 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_hintandcounterparty_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.
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:
valueispresent / possible, rounded to three decimal places;corroborations.presentandcorroborations.possiblerender the numerator and the denominator;calibrationisnon_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"
}JSONRead 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 inrelationships. An earlier version wrongly listed theresolvedandalready_knownoutcomes 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,
typenames the relationship type,outcomeisambiguousandcandidates_at_leastgives a floor, never the exact count, which would be an oracle on other people's transactions; - for the series,
typeisseries,occurrencescounts the occurrences seen andoutcomecarries 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:
relationships | unresolved (relationships) | Reading |
|---|---|---|
| Empty | Empty | No relationship established, no ambiguity met. |
| Empty | One ambiguous entry | Several equally plausible candidates: Noryo abstains. |
| One relationship | Empty | Link 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).