SDK reference
Links
buildLink, parseLink, pillarFor — one HTTPS grammar, a marker that identifies it offline, and the parsing rule that keeps a scanner from becoming an attack surface.
One form, every transport. A QR code, an NFC tag, a deeplink and an in-app handoff carry the same string.
https://<host>/path/<reference>?n=<network>buildLink(input)
import { buildLink } from '@pathprotocol/sdk';
buildLink({ host: 'pay.example.com', reference: '7fk2m9pq3vx8' });
// 'https://pay.example.com/path/7fk2m9pq3vx8'
buildLink({
host: 'pay.example.com',
reference: '7fk2m9pq3vx8',
network: 'openconnect',
});
// 'https://pay.example.com/path/7fk2m9pq3vx8?n=openconnect'| Field | Notes |
|---|---|
host | Host or origin — pay.example.com and https://pay.example.com/ both work |
reference | Opaque [A-Za-z0-9_.-]+. No colon, so an ADDRESS spelling cannot nest |
network | Emitted as n=. A hint for the reader, not a claim it should believe |
extra | Additional parameters. amount, currency and memo are rejected |
amount, currency and memo throw. Terms live in the signed object, never in the link: a number
in a query string is editable with a text editor, and a wallet that renders it before the fetch
resolves has shown the payer a figure the issuer never stated.
parseLink(input)
Steps 1 to 6 of the reading algorithm. Anchored at both ends.
import { parseLink } from '@pathprotocol/sdk';
const link = parseLink('https://pay.example.com/path/7fk2m9pq3vx8?n=openconnect');
link.origin; // 'https://pay.example.com' — the routing index
link.host; // 'pay.example.com'
link.reference; // '7fk2m9pq3vx8'
link.network; // 'openconnect' — UNVERIFIED
link.params; // { n: 'openconnect' }- First path segment exactly
path, then exactly one segment → PATH link. - Anything else, including
https://host/p/abc→not_path. http:→not_path. The withdrawnpath:scheme →not_path.- A reference outside
[A-Za-z0-9_.-]+→malformed. - Unknown parameters are preserved.
network is a claim, not a fact
Anyone can print a sticker carrying ?n=openconnect. Use it to warm a cache or render a waiting
state. Never to decide trust, display, or routing — the authority is the discovery document at
origin.
It is anchored at both ends
parseLink('Pay at Oak Street Books https://pay.example.com/path/7fk2m9');
// throws InteropParseError { reason: 'not_path' }The whole string must be a PATH link. A substring match is not enough.
Errors
import { InteropParseError } from '@pathprotocol/sdk';
try {
parseLink(scanned);
} catch (err) {
if (err instanceof InteropParseError) {
switch (err.reason) {
case 'not_path': // something else — try EMVCo, BIP-21, an operator's own format
case 'malformed': // PATH-shaped, and broken
}
}
}not_path is the common case in a general-purpose scanner and should not surface to the user as an
error. There is no unknown_kind: the URL no longer names a kind, so an unfamiliar object is
discovered after the fetch, on type.
discoveryUrl(origin)
Step 2 of the chain of trust — where to ask a host what it is.
import { discoveryUrl, parseLink } from '@pathprotocol/sdk';
discoveryUrl(parseLink(scanned).origin);
// 'https://pay.example.com/.well-known/path-configuration'Works on any origin, not only one that produced a PATH link. An operator with its own link format is PATH-capable if it serves this document, and a wallet may probe for it. The marker is the fast path, not an admission requirement.
pillarFor(type) / isPathObjectType(value)
The type travels in the object, not in the URL.
import { isPathObjectType, pillarFor } from '@pathprotocol/sdk';
pillarFor('path.address'); // 'ADDRESS'
pillarFor('path.request'); // 'REQUEST'
pillarFor('path.checkout'); // 'REQUEST'
pillarFor('path.mandate'); // 'CONNECT'
pillarFor('path.receipt'); // 'SETTLEMENT'
isPathObjectType('path.teleport'); // false → upgrade prompt, never a guesspath.mandate returning CONNECT is the one to notice. It arrives through the same link as a
payment and creates a standing permission. Handle it as a request and you ship without
revocation and without counters.
Reading one end to end
import { InteropParseError, PathClient, parseLink, pillarFor } from '@pathprotocol/sdk';
export async function onScan(payload: string) {
let link;
try {
link = parseLink(payload);
} catch (err) {
if (err instanceof InteropParseError) return tryOtherFormats(payload);
throw err;
}
const wallet = new PathClient({ baseUrl: link.origin });
// Step 2 — the host declares its network. Step 3 (check the register lists it) is yours.
const { network } = await wallet.discoveryAt(link.origin);
// Step 4 and 5 — the object, and who signed it.
const object = await wallet.readObject(payload);
switch (object.type) {
case 'path.address': return { screen: 'enter-amount', accepts: object.accepts, network };
case 'path.request':
case 'path.checkout': return { screen: 'confirm', object, network };
default: return { screen: 'update-required' };
}
}