# Evidence Record schema

The sixteen core fields, the six-field B2B addendum, the enruta/0.2 and enruta/0.3 additions and the linked-record shape.

Status: Live · Updated: 2026-09-14
Source: https://enruta.ai/docs/schema

Sixteen core fields, six addendum fields, the optional fields of `enruta/0.2` and `enruta/0.3`, and a second shape for linked records. Records sealed today carry `schema: "enruta/0.3"`; `enruta/0` and `enruta/0.2` records keep their schema and verify exactly as before.

> Field names are stable within a schema version; additions are optional so an older record keeps its hash and its meaning. Both shapes are published as one JSON Schema (`oneOf`) with `$id` `https://enruta.ai/schemas/evidence-record-0.3.json`, shipped in every export and in the `enruta-verify` package.

(Interactive field explorer on the web page; the tables below list every field.)

## Core

| # | Field | Contents |
|---|---|---|
| 1 | `record` | `id` (ULID with `rec_` prefix), `schema`, `created_at`, `sequence` within the principal’s chain |
| 2 | `principal` | Organization that owns the funds and the policy; `org_` id, display name and, in 0.2, `login_id` (`halvern/dana.reyes@halvernrobotics.com`) |
| 3 | `agent` | `agt_` id, the identity attestation reference (KYA) and the host it ran in |
| 4 | `policy` | `pol_` id, `version`, and the hash of the signed policy document |
| 5 | `request` | Payee, amount, currency, purpose, checkout reference, evidence references with digests, `idempotency_key`, `hash`; in 0.2 the three intent layers |
| 6 | `decision` | `allow` / `step_up` / `deny` / `observe`, reason codes, budget state before and after, checks run, `decided_at`; in 0.2 `shadow_result` and the signed `risk` assessment; in 0.3 `judgement`, `retryable` and `retry_after` |
| 7 | `approval` | `state` (`approved`, `declined`, `expired`), actor, method (`passkey` / `link`), the bound request hash, `approved_at`; `null` when no person was required |
| 8 | `mandate` | `mnd_` id, scope (amount, currency, payee, expiry), `single_use`, `consumed_state`, the bound request hash; in 0.2 `mode`, `refill_policy`, `finality_class`, `deviation`, `policy` and the `ledger` snapshot; in 0.3 `retry_policy`, `visibility_scope` and `hidden_constraints_digest` |
| 9 | `protocol` | The upstream object mapped in: type (`ap2_mandate`, `acp_allowance`, `ucp_consent`, `x402_offer`, `mpp_receipt`, `card_authorization`, `export`) and its digest |
| 10 | `execution` | Rail, the instruction or credential reference (token, never a card number), `executed_at` |
| 11 | `receipt` | Merchant or rail receipt digest, order or transaction identifier |
| 12 | `outcome` | `authorized` / `declined` / `refunded` / `disputed` / `pending` / `completed` / `handed_off` / `fulfilled` / `voided` / `cancelled` / `expired`, amounts, `posted_at`; a hand-off adds `continue_url` and `handed_off_at` |
| 13 | `strength` | `third_party_signed` / `customer_attested` / `self_asserted`, per claim group |
| 14 | `seal` | Canonicalization (`rfc8785`), hash algorithm, hash, `prev` record hash |
| 15 | `keys` | References to the snapshotted verification keys used at capture, each with a provenance tag |
| 16 | `attestations` | Pointer to the companion bundle: RFC 3161 tokens, daily-root inclusion proof, renewals |

## B2B addendum

| Field | Contents |
|---|---|
| `settlement_rail` | `issuing_card`, `ramp`, `mercury`, `bill_com`, `ach`, `wire`, `x402`, `mpp`, and since `enruta/0.3` `stripe_spt` and `link`; the account or program it ran on |
| `rail_authorization_record` | The rail’s own authorization identifier and decision, and the time Enruta answered |
| `account_validation` | How the payee’s account was validated and when |
| `rfp_context` | Request-for-payment or invoice context: bill, purchase order, match status |
| `card_control_state` | The controls compiled onto the card or limit at issuance: amount, merchant, MCC, expiry |
| `dispute_afterlife` | Anything that happened after posting: retries, refunds, disputes, with references; patched only in the bundle, never in the sealed bytes |

## Added in enruta/0.2

All optional on a payment record; absent means the v0 meaning.

| Field | Type | Meaning |
|---|---|---|
| `kind` | `"payment"` | Default when absent; linked records carry their own kind |
| `chain` | `{ relation: "correction_of" \| null, of, of_hash? }` | A payment record may only be a correction of another |
| `principal.login_id` | string | `<org slug>/<email>`, plain text, stable, recognizable by the cardholder; the Visa CE3.0 login id |
| `request.intent_text` | string | What the person said, verbatim (the acquirer field `consumerPrompt`) |
| `request.constraints` | `{ max_amount?, categories?[], substitutions_allowed? }` | The structured constraints the agent derived |
| `request.intent_mapping` | `{ model, at, confidence? }` | Which model turned the words into constraints, and when |
| `decision.result` | adds `"observe"` | Shadow mode; `decision.shadow_result: { result, reasons }` is present exactly then |
| `decision.risk` | `RiskAssessment` | The [risk assessment](https://enruta.ai/docs/risk-assessment) made with the decision, signed inside the bundle (`signatures.risk`) |
| `mandate.mode`, `refill_policy`, `finality_class`, `deviation`, `policy` | see [Mandate](https://enruta.ai/docs/mandate) | Mandate fields the platform signs into the JWS |
| `mandate.ledger` | `{ ceiling, reserved, authorized, captured, refilled, consumed, remaining }` | Snapshot of the mandate account at seal time |
| `deviations[]` | `{ field, requested, delivered, flag }` | What the merchant delivered against what was requested |
| `catalog_snapshot_hash`, `policy_snapshot_hash` | `sha256:` digests | The catalog the agent saw and the policy document at decision time |
| `failure_class` | `captcha` · `anti_bot_script` · `cdn_block` · `waf_block` · `3ds_challenge` · `payment_declined` | Why an attempt did not reach the rail |

## Added in enruta/0.3

They record the [trust model](https://enruta.ai/docs/trust-model): how the intent was judged, and who the two parties are. Every payment record sealed as `enruta/0.3` carries `agent.identity` (assurance `A0` when the agent has no anchors) and `decision.judgement`; the other fields appear when they apply. Records sealed earlier keep their schema.

| Field | Type | Meaning |
|---|---|---|
| `request.intent` | `{ text, slots, coverage: { required, filled, missing }, clarifications[], mapping }` | The person’s words, the slots, what the policy needed, and each question with its answer, `answered_by` (`user` or `agent`) and times ([intent and clarification](https://enruta.ai/docs/intent-and-clarification)). Supersedes 0.2 `intent_text`, `constraints` and `intent_mapping`, which 0.2 records keep |
| `IntentSlots` | `{ category?, items?[{ description, quantity?, unit_max? }], quantity?, max_total?, currency?, payee?, deliver_by?, substitutions_allowed?, recurrence?, notes? }` | The slot names are the policy’s vocabulary |
| `decision.result` | adds `"clarify"` | The request lacked a slot the policy requires; sealed only when the questions expire unanswered (`outcome.status: expired`) |
| `decision.judgement` | `{ rules[{ name, result, detail? }], model: null \| { provider, model, instruction_hash, input_hash, output_hash, verdict, rationale, confidence, at } }` | Rules first; the model only for what rules cannot decide; the hashes are `sha256:` over the JCS-canonical input and output |
| `agent.identity` | `{ assurance: "A0" \| "A1" \| "A2" \| "A3", anchors[] }` | Each anchor: `{ attestation_id, type, party?, issuer, status, verified_at, expires_at, evidence_hash }` ([identity anchors](https://enruta.ai/docs/identity-anchors)) |
| `payee.identity` | `{ status: "verified" \| "unverified" \| "mismatch", checks[], seller_org, checked_at }` | The merchant check made before paying, on a top-level `payee` object rather than inside `request.payee`: `domain`, `ucp_keys`, `checkout_signature`, `seller_org`, `conformance` |
| `economics_ref` | `{ fee_schedule_as_of }` | Present when a [money split](https://enruta.ai/docs/economics) estimate was shown at decision time; the estimate itself is not sealed |
| `chain.relation` | adds `"occurrence_of"` | A payment made under a [recurrence](https://enruta.ai/docs/recurrence); valid next to `correction_of` |
| `execution.rail`, `settlement_rail.rail` | adds `"stripe_spt"` and `"link"` | A payment on a Stripe shared payment token or a Link spend request ([rails](https://enruta.ai/docs/rails)); a record sealed earlier keeps the label it was sealed with |
| `outcome` of a rail hand-off | `{ status: "handed_off", handed_off_at, rail, reason, message, next_step, continue_url? }` | The rail could not issue a credential for an allowed payment: which rail, its reason, what it said and what a person does next; `continue_url` only when the rail named its own page |
| `decision.retryable`, `decision.retry_after` | boolean; timestamp or `null`, present together | Whether the same payment may be requested again, and from when: `true` only when every reason that ended the request is remediable, and `retry_after` set only then (`over_budget`: the next budget window) ([retrying](https://enruta.ai/docs/decision#retrying)) |
| `mandate.retry_policy` | `{ max_attempts, window_seconds, same_amount_only }` | How a failed payment may be tried again, as signed into the mandate |
| `mandate.visibility_scope` | `{ merchant[], issuer_only[] }` | Which constraints the merchant’s mandate carried; present when the policy declared a scope ([mandate](https://enruta.ai/docs/mandate#retry-policy-and-what-the-merchant-sees)) |
| `mandate.hidden_constraints_digest` | `sha256:` digest | The JWS claim of the same name: a commitment to the constraints kept from the merchant, whose values and salt are in the bundle as `artifacts.hidden_constraints` |

Reason codes added: `needs_clarification`, `clarification_expired`, `identity_insufficient`, `payee_unverified`, `payee_identity_mismatch`, `intent_rule_failed`, `model_judgement_deny`, `model_unavailable`, `collateral_insufficient`, `retry_limit_reached`, `rail_unavailable`, `rail_error`. Checks added: `intent_coverage`, `intent_rules`, `intent_model`, `agent_identity`, `payee_identity`, `collateral`, `retry_policy`. The bundle gains `artifacts.identity[]`, each anchor’s raw evidence, `artifacts.judgement_io`, the model’s input and output, and `artifacts.hidden_constraints`, the constraints a mandate kept from the merchant with the salt of their digest.

## Linked records

Every supporting object is its own record on the principal’s chain: same `sequence` and `seal.prev`, same daily checkpoint, same verifier. `subject` holds the object; `subject.type` equals `kind`.

```json
{
  "record":  { "id": "rec_01M2G8Z0Q4W7T2K9N5B3D6H1XC", "schema": "enruta/0.2", "created_at": "2026-09-13T15:04:11Z", "sequence": 13 },
  "kind":    "reversal",            // reversal | dispute_packet | dispute_outcome | settlement_record | reconciliation_line |
                                     // reconciliation_report | consent_record | kya_attestation | agent_pause | agent_order | recurrence
  "principal": { "id": "org_halvern", "login_id": "halvern/dana.reyes@halvernrobotics.com" },
  "agent":   { "id": "agt_procurement_02", "host": "claude" },       // null only for reconciliation_report
  "chain":   { "relation": "reversal_of", "of": "rec_01M25129GZMJXTNQ47TMZRDGHN", "of_hash": "7e2a…c41d" },
  "policy":  { "id": "pol_office_supplies", "version": 4, "hash": "sha256:…" },
  "subject": { "type": "reversal", "id": "rev_01M2G8YV5R3T7W2Q9K4N8B6DXP", "kind": "refund", "amount": { "value": "9.99", "currency": "USD" }, … },
  "decision": { "result": "allow", "reasons": [], "checks": [{ "name": "original_amount", "ok": true }, …] },
  "approval": null,
  "outcome":  { "status": "refunded", "amount": "9.99", "currency": "USD", "posted_at": "2026-09-13T15:04:02Z" },
  "strength": { "subject": "third_party_signed", "policy": "customer_attested" },
  "seal":    { "canon": "rfc8785", "alg": "sha256", "hash": "c19b…8e02", "prev": "7e2a…c41d" },
  "keys":    ["key_org_halvern_2026-09", "key_enruta_seal_2026-09", "key_merchant_store_2026-09"],
  "attestations": { "bundle": "rec_01M2G8Z0Q4W7T2K9N5B3D6H1XC.bundle.json" }
}
```

| `kind` | `chain.relation` | `subject.id` prefix | Carries |
|---|---|---|---|
| `reversal` | `reversal_of` | `rev_` | A real `decision`; an `approval` when the refund stepped up; `outcome` refunded / voided / cancelled |
| `dispute_packet` | `dispute_of` | `dsp_` | The packet without its exports; the mandate JWS sits in `artifacts.mandate` |
| `dispute_outcome` | `dispute_of` (the packet record) | `dsp_` | won / lost / withdrawn, the note, where it was submitted |
| `settlement_record` | `settlement_of` | `stl_` | Stage, amounts, finality; the last rail object in `artifacts.rail_event` |
| `reconciliation_line` | `reconciliation_of` | `rcl_` | The five legs, match state, exceptions |
| `reconciliation_report` | `report_for` (`of: null`, `agent: null`) | `rcr_` | Counts, coverage, root hash, the line record ids; the signature of the person who approved it as `approval` |
| `consent_record` | `consent_for` | `cns_` | The consent object; a revocation is a later record chained to the first, with `subject.action` `schedule_revocation` while it waits for its `effective_at` and `revoke` when it takes effect |
| `kya_attestation` | `attestation_for` | `kya_` | Registry, tier, method, `evidence_hash` |
| `agent_pause` | `pause_of` (`of: null`) | `agt_` | pause / resume, by whom, why, when |
| `agent_order` | `merchant_view_of` (the buyer record, with `of_hash`) | `mor_` | The seller’s view: labels, risk signal, line items by id and total |
| `recurrence` | `recurrence_for` (its consent record, or `of: null`) | `rcu_` | The recurrence at creation and at every change: payee, caps, schedule, status |

## Strength of a linked record

`strength.subject` is `third_party_signed` when the bundle carries `artifacts.subject` plus a detached JWS by a merchant key over it, or an object from a rail (`artifacts.rail_event`); `customer_attested` when Enruta signed the subject hash with the organization’s link key (`signatures.subject`); `self_asserted` otherwise. The verifier applies the same never-upgrade rule as for payment records.

## The bundle

`<record id>.bundle.json` sits next to the record and holds everything that changes after sealing: time-stamp tokens, the inclusion proof in the daily root, the root’s signed checkpoint, renewals, the key snapshots, and since 0.2 `artifacts.subject`, `artifacts.subject_signature`, `artifacts.rail_event`, `signatures.subject` and `signatures.risk`, and since 0.3 `artifacts.identity[]`, `artifacts.judgement_io` and `artifacts.hidden_constraints` (the constraints a mandate kept from the merchant, with the salt of their digest; never served to a merchant). The record’s hash never depends on the bundle.
