INTEROP had two grammars, ten kinds in the URL, and no answer to the only two questions a scanner
actually asks: is this a PATH code, and who stands behind it. Both are now answered, and most
of the surface that existed to support the old design is gone.
One form. https://<host>/path/<reference>?n=<network>. A QR code, an NFC tag, a deeplink and
an in-app handoff carry the same string.
A marker. The first path segment is exactly path. That is the whole of the offline
recognition rule — no network call to tell a payment code from a link to a blog post. /p/ was
rejected as a marker: it is a common prefix for product and post pages, so it produces false
positives in a general-purpose scanner.
The path: scheme is withdrawn. A custom scheme fails silently when no app claims it and fails
worse when several do. Operators keep their own deeplinks for their own products; PATH does not
compete for that slot.
No kind in the URL. What the reference points at is a property of the object, announced by
type: path.address, path.request, path.checkout, path.mandate, path.claim,
path.receipt. Ten URL kinds became six object types.
No terms in the link. amount, currency and memo are rejected by buildLink. Amounts and
memos belong on the signed object.
A network hint with no authority. n=<slug> replaces op=. It is a claim, not a fact — good
for a cache or a waiting state, never for trust, display or routing.
A chain of trust, written down. Host → discovery → register → object → signature. Step 3 is the
one implementations skip: without checking that the declared network's register lists this
operator, n= and the discovery document are both self-assertions.
URI signatures are withdrawn. signUri, verifyUri, canonicalUri and the base64url sig
parameter are gone. The object is signed; JSON envelopes keep hex. With no signature and no terms
in the URL, a complete code is around forty characters, and the old ~300-character budget has no
cause left.
One parser. parseLink replaces parseUri and parsePayload. unknown_kind is gone from the
parse stage — an unfamiliar object is discovered after the fetch, on type, where an upgrade
prompt is the right answer.
Discovery declares hosts[] and interop_types. Several hosts for one operator are aliases,
not several operators. A host not on that list is not that issuer.
One reference namespace. A checkout session now has a public reference from the same generator
as a request, and GET /path/<reference> serves requests, sessions, addresses and receipts alike.
A session readable only at its own endpoint was not reachable by a scanned code at all.
Receipts replay the stored envelope. The bytes signed at issuance are the bytes served on
read. Amounts are not reconstructed from typed columns.
Attestations are real objects: issued, stored as signed, read at execution. Expiry is
mandatory. Revoked or expired answers 410, not 404. Claims are closed (kyc_level, kyb_level,
aml, sanctions, pep, risk_tier, identity_level 0–5).
Ownership bindings record that a subject controls an identifier, a wallet or an account. The
clear value is hashed and is not stored.
Receive targets can be created, listed and revoked. The resolver ignores revoked ones.
account_ref never leaves the member. Kinds now include viban, iban, ach, fps.
PATH-ID.Attestations, PATH-ID.Ownership and PATH-ADDR.Targets are declared in discovery.
PATH-ID.Revocation (a published register) is still not claimed.
PIP-0005 specifies the only shape receipts.route may take. The field stays null.
Checkout is documented as live. Partial payments, concurrent attempts and refunds remain reserved.
Discovery declares PATH-CONNECT.Core and PATH-CONNECT.Delegation: connection objects with an
explicit capability set, binds_to tokens for a third party, live counters, a usage log and
revocation. A token is bound to capability, amount, currency and destination. Consume requires an
exact match. Spending updates the parent connection in the same write.
The consented terms are stored as signed. Status and counters move, so the object served is
signed at read; consent inside it is the envelope from the moment the subject agreed.
Connections are not served at /path/{reference}. A request is public. A connection names a
grantor, a grantee and a subject. Revocation is checked by the grantor when the token is used.
Idempotency keys are scoped to the issuing member.
POST /sonar/confirm takes the signed SONAR answer. The caller is taken from that envelope.
Cross-network confirmation does not require a credential on this member.
Batch lookups accept signed: true. That returns one envelope per key, which /sonar/confirm
accepts. Unsigned remains the default. The batch is not signed as a single envelope.
SONAR answers include identifier_type and identifier_hash in the signed payload.
Member authentication covers the request body as received.
Payer-facing checkout routes require a credential. The member is taken from that credential,
not from the body.
Checkout sessions can be completed. POST /checkout/sessions/{reference}/complete closes the
session. It does not mark the request paid.
Payer-facing session routes take the public reference.
Request status changes are conditional writes.
Amounts are normalised on the way out to the decimal string the payer and the signature share.
Reachability.proofs.sonar is SonarAnswer | null.
Finder does not send identifiers to a resolver. Without an address, reachability stops after
step 1.
The digest is produced when standing intent is written, stored with that version, and served at
GET /commitments/{address} — the same value for every caller. The resolver quotes it and does not
recompute it. The SDK compares the two with checkCommitment.
Addresses with no standing intent answer commitment: null.
A commitment is a public record of terms. It is not a defence against a fully compromised operator.
Routes that act on this member's own data require this member's credential
(path.auth.not_local_member otherwise). Cross-member routes — completing a checkout, consuming a
connection — still accept any active member credential.
Withdrawal from the directory is scoped to this network and this member.
PATH-ID.Revocation, PATH-ADDR.Inbound and PATH-SETTLE.Reconciliation are profiles of their
own. This implementation does not declare them. Standing intent is live. Receive targets are
createable, listable and revocable (PATH-ADDR.Targets). Targets carry asset, chain and rail;
currency is a term of the standing intent.
POST /receipts takes request_reference and signs that public reference. It requires
source_tx_hash or source_reference. Issued receipts include route: null.
path.markPaid() is in the SDK.
Every code issued under 0.1.x. Re-issue them as https://<host>/path/<reference>.
POST /sonar/confirm takes { sonar_answer } instead of loose fields, and GET /checkout/sessions/{id} requires the issuer's credential.
POST /checkout/sessions/{id}/attempt is now {reference}/attempt, authenticated, with no body.
finder({ identifier }) alone returns accepts: [] — pass address for step 2.
GET /p/:reference is gone; it is GET /path/:reference. request.url and the new
session.url already return the correct form.
kind is type on the wire: payment_request → path.request, settlement_receipt →
path.receipt. Readers switching on kind need updating.
Callers importing buildUri, parseUri, parsePayload, signUri, verifyUri, issuerOrigin,
canonicalUri, httpsFormLength, INTEROP_KINDS, SIGNED_STATIC_PAY_BUDGET, InteropKind or
InteropUri.
POST /receipts takes request_reference, not request_id, and refuses a body with no
source_tx_hash and no source_reference. Issued receipts gain route: null, so the signed payload
differs — a stored envelope still verifies, since the bytes are replayed as signed.
Ring-3 routes — subjects, identifiers, addresses, standing intent, issuing requests and receipts,
creating checkout sessions and connections — now answer path.auth.not_local_member to a credential
belonging to another member of the network.
ResolverAnswer.commitment is string | null and gains commitment_version. Readers treating it as
always-present need updating, and a payer that wants the guarantee must now fetch
GET /commitments/{address} as a second call.
Receipts issued before this release cannot be served as proofs — their envelope was never stored,
and no amount of reconstruction recovers signed_at. They answer path.settlement.receipt_unverifiable
rather than pretending.
parseLink at the scanner, then discoveryAt(link.origin), then check the register, then
readObject. buildLink({ host, reference, network }) for anything printed or tapped. Switch on
object.type and treat an unknown one as an upgrade prompt.