Docs
PATH INTEROP
One URI, three transports — HTTPS on the printed code, path: as an app-to-app alias, and a reading algorithm that does not become an attack surface.
LIVE — the HTTPS grammar, the path: alias,
parsePayload, and URI signatures are implemented. claim, present and receipt kinds are
DRAFT as product flows; their rules are
already in this page.
One grammar, three transports
A QR is not a format. A deeplink is not a format. An NFC tag is not a format. They are three ways to carry the same object.
Printed and clickable form — HTTPS:
https://<host>/p/<kind>/<reference>?<params>A custom path: scheme fails silently when no app claims it. HTTPS always degrades: it opens a
page. With Universal Links and App Links it opens the app when one is installed.
App-to-app / NFC alias — never the printed form:
path:<kind>/<reference>?<params>op= is allowed only on this alias. The HTTPS host carries routing. Removing the routing hint
does not make the object unresolvable for a party that already knows the holder.
The reference is opaque [A-Za-z0-9_.-]+. A colon is not allowed, so path:pay/path:4a91c2f7e8d3
cannot nest. The address path:4a91c2f7e8d3 (PATH ADDRESS) is not copied into the URI as the
reference — the URI carries the opaque id only (4a91c2f7e8d3).
parseUri stays strict: it accepts path: and nothing else. Readers that may see either form call
parsePayload.
What HTTPS costs (and why that does not put path: back on the sticker)
A printed HTTPS URI is fetched, cached, and logged. That is a privacy cost, and it is written here rather than discovered later:
| Surface | What leaks |
|---|---|
| GET on the hosted page | The operator sees that this sticker was scanned, from which IP, at which time |
| Camera / OCR | The full URI sits in screenshots, gallery backups, and accessibility trees |
| CDN / edge | Cache keys and access logs see the path and often the query |
| Referer | A click-through from a wallet page can send the URI to a third party |
This cost is accepted. It is the price of a code that opens when no PATH app is installed. It does
not put path: back on a printed sticker — a custom scheme that nobody handles is a dead
sticker, not a private one.
A host that is not listed on the issuer's discovery hosts[] is not that issuer. Several hosts for
one operator are aliases, not several operators. There is no resolveIssuerBase: the HTTPS host
is the routing index; issuerOrigin() returns it.
Kinds
Closed allowlist. None is ten characters — that reservation belongs to inherited one-segment
payment links (/p/<code>), which are not PATH URIs.
| Kind | Meaning | Object belongs to |
|---|---|---|
pay | Pay this destination | ADDRESS |
request | A specific demand for payment | REQUEST |
checkout | A merchant checkout session | REQUEST |
subscribe | A recurring mandate | CONNECT |
connect | A connection request | CONNECT |
fconnect | A financial connection request | CONNECT |
mandate | Consent to a delegation | CONNECT |
claim | A code presented by the payer | ADDRESS |
present | A request to present identity | ID |
receipt | A verifiable settlement receipt | SETTLEMENT |
The right-hand column is the rule, not an annotation:
INTEROP carries the envelope. The pillar owns the object.
Two rows repay attention. pay points at a destination while request carries a claim —
they look alike in a QR code and behave differently everywhere else. And subscribe looks exactly
like a payment link while creating a standing permission, which is why it is CONNECT; file it
under REQUEST and you ship without revocation or counters.
Modes
A reader MUST determine the mode without a network call. presented is not a third grammar —
it is the CPM rules on kind claim.
| Mode | Who displays | Content | Network to read | Usage |
|---|---|---|---|---|
static | The receiver, once | Terms are in the URI | No | Printed sticker, invoice, profile |
dynamic | The receiver, per transaction | A short reference; the object lives at the issuer | Yes | Till, order, PR, SR, CR, FCR |
presented | The payer (kind=claim) | Bearer token, one use, short life | Yes (acquirer side) | POS: the terminal scans the customer |
Dynamic is unreadable offline. That is not a limitation to discover in the field: the URI carries a reference, not the terms. Without the issuer, a reader can recognise the kind and refuse politely. It cannot invent an amount.
claim is DRAFT as a product flow. Its
rules are not:
- one use, invalidated on first presentation;
- 60–180 seconds, rotating on screen;
- bound to the presenter — never reused for another amount or another acquirer;
- never an amount inside the code (the acquirer proposes, the presenter confirms);
- never left in a shareable screenshot.
The reading algorithm
Normative. Implementations MUST follow it in order.
- Trim leading and trailing whitespace. Nothing else.
- Dispatch the form.
https:→ parse the HTTPS grammar.path:→ parse the alias. Anything else isnot_path. An input that merely contains a PATH URI is rejected — never searched for a match.http:is not HTTPS. - HTTPS path. Exactly two segments after
/p/:/p/<kind>/<reference>.- One segment (
/p/<code>) is not a PATH URI (not_path) — an inherited payment link. - Two segments, kind not in the allowlist →
unknown_kind. Never treat an unknown kind as a code.
- One segment (
- Match the whole string. Partial matches are rejected.
parseUriadditionally requires the input to start withpath:and nothing else. - Resolve the kind. Unknown kinds fail with the kind named, so the error reads "upgrade your reader" rather than "malformed".
- Parse parameters as a query string. Unknown parameters are preserved, never dropped.
op=on HTTPS ismalformed.op=onpath:is a routing hint, not a term. - Verify the signature where one is present, before acting.
- Fetch state for dynamic kinds. The URI carries a reference; the current terms come from the issuer. Offline, a dynamic URI is unreadable.
Steps 2 and 3 are the security-relevant ones. A reader that scans for a pattern and takes the first
match can be redirected by appending a crafted suffix to an innocuous string. Anchor at both ends.
A single segment after /p/ is a different object — do not promote it to PATH by guessing a kind.
Signed URIs
Static codes that carry their own terms are signed. The signature is Ed25519, base64url
(URI sig is not the hex used on JSON envelopes).
Canonical form: kind + reference + every parameter except sig and op, keys sorted.
Unknown parameters are covered. Host is not — routing is not identity, so the same object
verifies as HTTPS and as a path: alias.
An attacker who edits the amount, or who drops a parameter they do not understand, breaks the signature.
Size is measured on the HTTPS form. A signed static pay SHOULD stay under 300 characters
on https://<host>/p/pay/…. That prefix costs twenty to forty characters more than path:pay/,
depending on the host. Above 300, error correction drops and the sticker stops working in exactly
the conditions it was printed for.
What a QR SHOULD NOT carry
A printed code SHOULD NOT put a chain address in the clear. The reference is an opaque PATH
id; resolution returns a capability, not a wallet. Publishing 0x… on a shop window correlates
the merchant's history for anyone with a camera — the failure the directory was built to avoid.
op= is not the Goldrail qr_institution_slug. The slug stays where it is (compact QR,
onboarding). PATH does not reuse it as a routing index. The host (HTTPS) or a declared hosts[]
entry is the routing index; op= is only a hint on the path: alias.
Hosted short links
For dynamic kinds the URI is still PATH — two segments — and it stays short because the terms live behind the reference:
The reference is random, not sequential. Fetching it returns the signed object.
The domain says where to fetch; the signature says who is responsible. Verify the kid, then
display the issuer's verified name rather than the URL.
A one-segment https://pay.example.com/p/7fk2m9pq3vx8 is an inherited payment link, not Fig. 03.
Mapping to existing standards
PATH does not replace what already works at the counter. Where a rail has a native format, implementations SHOULD map rather than compete. One table — informative, not a second copy elsewhere:
| Standard | Mapping |
|---|---|
| EMVCo MPM | kind=pay, merchant-presented, mode static / dynamic |
| EMVCo CPM | kind=claim, consumer-presented, mode presented |
| EPC069-12 | kind=pay, rail sepa |
| BIP-21 | kind=pay, chain-native |
| EIP-681 | kind=pay, chain-native |
A reader that understands PATH and one native format covers most of what it will meet in the field.