Docs
PATH SETTLEMENT
One signed receipt format across every rail, so that finality becomes a verifiable state instead of a promise.
DRAFT — receipts, the full lifecycle and
signed transitions are LIVE. Reconciliation
stays RESERVED, and route stays null.
Path-Version: 2026-10-04.genesis writes via as a capability, sent and
received, typed rail_references, receipts issued by either side with issued_by, and a link to
the payment instruction (PIP-0011). A caller pinned to 2026-09-09.genesis writes
the 0.3 bodies, signed protocol_version 0.1.0, with amount a decimal string and status,
reason_code and supersedes inside the signature. Signed 0.3 receipts are replayed as signed.
The governing idea
Finality is a verifiable state, not a promise.
"It's paid" means different things on-chain, on an instant rail and in a mobile-money ledger. Each has its own proof, its own format and its own horizon, and none of them reconcile with the others. So reconciliation gets done by hand, in spreadsheets, at month end.
A signed receipt over a mobile-money transfer is worth exactly as much as an on-chain proof — provided the format is the same and the reconciliation agrees. That equivalence is what lets a protocol span rails that have nothing else in common.
The receipt
{
"type": "path.receipt",
"reference": "rcpt_9a41c8f2b731",
"issuer": "member-a",
"issued_by": "payee",
"payer": { "member": "member-b" },
"payee": { "member": "member-a", "address": "path:4a91c2f7e8d3" },
"request": "7fk2m9pq3vx8",
"instruction": "in_3k91…",
"via": { "rail": "book", "asset_type": "fiat", "asset_code": "USD" },
"sent": null,
"received": { "value": "5000", "asset_type": "fiat", "asset_code": "USD" },
"fx": null,
"fees": [],
"rail_references": [{ "type": "book_entry", "value": "LE-8891-2231" }],
"attestations": [],
"status": "settled",
"reason_code": null,
"supersedes": null,
"route": null,
"grammar": 2,
"protocol_version": "0.2.0",
"signed_at": "2026-09-09T10:04:12Z",
"kid": "op_example_2026_01",
"signature": "…"
}via is the capability used. rail_references are typed by that rail: book_entry on book,
tx_hash on blockchain. A settled receipt carries at least one.
request links back to the payment request when there was one. Spontaneous
payments have none, and that is normal rather than an omission.
status, reason_code and supersedes are what make the lifecycle below real rather
than declared. They are inside the signature, not beside it.
Publicly readable
Like a payment request, and for the same reason. A receipt is only useful if a third party can check it — a supplier, an auditor, a customs officer, the payer's own institution. Putting it behind a credential means the only people who can verify a settlement are the two who already agree about it.
const { valid, reason } = await verifyAgainstIssuer(receipt, 'https://api.other-member.com');The verifier fetches the issuer's key from the issuer's discovery document. Not from whoever served the receipt, and not from a central authority: an operator vouching for another operator's key rebuilds the hierarchy the protocol avoids.
Lifecycle
LIVEA receipt is issued pending or settled, and moves once.
reversed follows settled only. A failure never moved value; a reversal moved it and moved it
back. An auditor, a dispute and a ledger read the two states as different events.
failed and reversed are terminal. Re-stating the state a receipt already holds is not an error
— a duplicate rail callback is ordinary — but it appends nothing.
Issuing pending
A pending receipt is the only way to end up with a signed statement that a payment failed.
Issue straight to settled and the only document you can ever produce is one asserting success.
It is also the only receipt exempt from the rail-reference rule: on most rails the transaction is
not named until it has gone through, so rail_references arrive with the transition to settled.
Transitions mint a new envelope
POST /receipts/{reference}/transitions does not update the receipt you already handed out.
Re-signing in place would move signed_at, and every copy an auditor is holding would stop matching
the one served afterwards. So each transition signs a new envelope carrying the new status and
supersedes — the digest of the envelope it replaces. Both stay readable, both stay valid.
{
"type": "path.receipt",
"reference": "rcpt_9a41c8f2b731",
"status": "failed",
"reason_code": "rail_unavailable",
"supersedes": "4c1e77b9…",
"via": { "rail": "pix", "asset_type": "fiat", "asset_code": "BRL" },
"received": { "value": "5000", "asset_type": "fiat", "asset_code": "BRL" },
"rail_references": [],
"route": null,
"grammar": 2,
"protocol_version": "0.2.0",
"signed_at": "2026-09-21T10:06:44Z",
"kid": "op_example_2026_01",
"signature": "…"
}supersedes is the SHA-256 digest of the canonical JSON of the previous envelope, not its reference.
Altering either statement changes the digest.
GET /receipts/{reference}/transitions returns the whole chain, oldest first, and is public for the
same reason the receipt is. A reader holding only the current envelope can verify a signature and
still not know whether the money ever moved, or moved and came back.
The status is inside the signature
status, reason_code and supersedes are part of the signed payload. A valid signature is proof
of arrival only when status is settled.
Envelopes issued before these fields entered the payload still verify: stored bytes are replayed as signed and never rebuilt. A verifier that reconstructs the payload adds the three fields.
Why a failure needs a reason, from a closed list
reason_code is required on failed, and the list is closed — a typo is a 400, not a new reason.
| Code | Means |
|---|---|
rail_unavailable | The rail could not be reached at all |
rail_rejected | The rail took the instruction and refused it |
destination_unknown | |
destination_closed | |
limit_exceeded | |
compliance_blocked | |
insufficient_funds | |
expired |
The list is closed because this is what makes a declared rail an undertaking rather than a decoration. Nothing in the protocol can verify that a member is really a PIX participant — that claim is signed, not proven. What the protocol can do is make the contradiction accumulate:
the member declares accepts: [{ "rail": "pix", "asset_type": "fiat", "asset_code": "BRL" }] signed
the payment fails reason_code: "rail_unavailable" signed
│
└─► repeated, on a rail it declared,
under its own key — and a network
rulebook has something to act onSee Conformance and Networks.
Reconciliation
RESERVED — specified, and nothing writes it.
See PATH-SETTLE.Reconciliation, a profile this implementation does not claim.
Three views of the same event, which must agree:
| Source | What it says |
|---|---|
onchain | The chain's own record |
provider | The rail operator's ledger |
statement | The account statement |
{
"receipt": "rcpt_9a41c8f2b731",
"observations": [
{ "source": "provider", "observed": "5000", "observed_at": "2026-09-09T10:04:12Z" },
{ "source": "statement", "observed": "4975", "observed_at": "2026-09-10T02:00:00Z", "discrepancy": "25" }
]
}Discrepancies are recorded, not silently corrected. A system that quietly reconciles differences away is a system where nobody notices the fee that was never disclosed, or the rail that rounds in one direction.
The reserved field
route is present on every receipt. PIP-0005 defines its shape (legs, providers,
eta_seconds). Implementations MUST emit null until a later PIP names who may fill it.
"route": nullA non-empty route in this version is a non-conformity.
Presenting a receipt
A receipt can be carried as a receipt URI, so it can be shown as a QR code and
verified by someone with no API access at all.
That is what makes settlement tangible rather than theoretical: a supplier scans a code and knows they were paid, without an account anywhere and without trusting the person holding the phone.
What is out of scope
No settlement token, no escrow, no collateral, no guarantee of funds. Those are regulated activities and a protocol that specified them would need a licence in every jurisdiction it touched.
PATH specifies the proof, not the rail — and the proof is the part that has to be portable.