GitHub
Working with it

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'
FieldNotes
hostHost or origin — pay.example.com and https://pay.example.com/ both work
referenceOpaque [A-Za-z0-9_.-]+. No colon, so an ADDRESS spelling cannot nest
networkEmitted as n=. A hint for the reader, not a claim it should believe
extraAdditional 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 withdrawn path: 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 guess

path.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' };
  }
}

On this page