GitHub

SDK reference

URIs

parsePayload, parseUri, signUri, verifyUri — HTTPS first, path: as an alias, and the parsing rule that keeps a QR reader from becoming an attack surface.

buildUri(input)

import { buildUri } from '@pathprotocol/sdk';

buildUri({
  kind: 'request',
  reference: '7fk2m9pq3vx8',
  host: 'pay.example.com',
  amount: '5000',
  currency: 'XOF',
});
// 'https://pay.example.com/p/request/7fk2m9pq3vx8?amount=5000&currency=XOF'

buildUri({
  kind: 'request',
  reference: '7fk2m9pq3vx8',
  amount: '5000',
  currency: 'XOF',
  extra: { op: 'burok' },
});
// 'path:request/7fk2m9pq3vx8?amount=5000&currency=XOF&op=burok'
FieldNotes
kindOne of the ten interop kinds
referenceOpaque [A-Za-z0-9_.-]+ — no colon, no path: prefix
hostWhen set, emit HTTPS. When omitted, emit the path: alias
amount, currency, memoOptional
acceptsSerialised compactly as asset:chain:rail
extraAdditional parameters. op= is rejected when host is set

Keep a signed static pay URI under roughly 300 characters on the HTTPS form. That is the budget that matters for a printed sticker.


parsePayload(input)

The reading algorithm's step 2. Dispatches https: then path:.

import { parsePayload } from '@pathprotocol/sdk';

const parsed = parsePayload(
  'https://pay.example.com/p/request/7fk2m9pq3vx8?amount=5000&currency=XOF',
);

parsed.form;      // 'https'
parsed.host;      // 'pay.example.com'
parsed.kind;      // 'request'
parsed.reference; // '7fk2m9pq3vx8'
parsed.params;    // { amount: '5000', currency: 'XOF' }
  • Two segments after /p/, kind in the allowlist → PATH URI.
  • One segment after /p/not_path (inherited payment link).
  • Two segments, unknown kind → unknown_kind (never treated as a code).
  • op= on HTTPS → malformed.
  • http:not_path.

parseUri(input)

Strict path: parser. Anchored at both ends. Does not accept HTTPS — that is parsePayload.

import { parseUri } from '@pathprotocol/sdk';

const parsed = parseUri('path:request/7fk2m9pq3vx8?amount=5000&currency=XOF');

parsed.kind;      // 'request'
parsed.reference; // '7fk2m9pq3vx8'
parsed.form;      // 'path'
parsed.params;    // { amount: '5000', currency: 'XOF' }

It is anchored at both ends

parseUri('harmless text path:pay/abc123 trailing');
// throws InteropParseError { reason: 'not_path' }

This is the security-relevant behaviour. A reader that scans for a pattern anywhere in the input and takes the first match can be redirected by appending a crafted suffix to an innocuous string.

Unknown kinds are named

parseUri('path:teleport/abc123');
// throws InteropParseError { reason: 'unknown_kind' }

Unknown parameters survive

const parsed = parseUri('path:pay/abc123?amount=10&future_field=keepme');
parsed.params.future_field; // 'keepme'

Errors

import { InteropParseError } from '@pathprotocol/sdk';

try {
  parsePayload(scanned);
} catch (err) {
  if (err instanceof InteropParseError) {
    switch (err.reason) {
      case 'not_path':      // not a PATH URI — try another format
      case 'unknown_kind':  // PATH, but newer than this reader
      case 'malformed':     // PATH, and broken
    }
  }
}

not_path is the common case in a general-purpose scanner and should not surface as an error to the user.


signUri(input, privateKey) / verifyUri(uri, publicKey)

Ed25519, sig in base64url. Canonicalisation: kind + reference + every parameter except sig and op, keys sorted. Host is not covered, so the same signature verifies on HTTPS and on the path: alias.

import { signUri, verifyUri } from '@pathprotocol/sdk';

const uri = signUri(
  {
    kind: 'pay',
    reference: '4a91c2f7e8d3',
    host: 'pay.example.com',
    kid: 'op_example_2026_01',
    params: { amount: '5000', currency: 'XOF' },
  },
  privateKeyHex,
);

verifyUri(uri, publicKeyHex); // true

JSON envelopes still sign with hex (verifyEnvelope). Do not mix the two encodings.


issuerOrigin(uri) / pillarFor(kind)

import { issuerOrigin, pillarFor } from '@pathprotocol/sdk';

issuerOrigin(parsed);     // 'https://pay.example.com' or null on the path: alias
pillarFor('request');     // 'REQUEST'
pillarFor('pay');         // 'ADDRESS'
pillarFor('subscribe');   // 'CONNECT'

There is no resolveIssuerBase. On HTTPS the host is the routing index. On the alias, the caller already knows the holder — or looks up a declared hosts[] entry. A host not on that list is not that issuer.

subscribe returning CONNECT is the one to notice. It looks exactly like a payment link and creates a standing permission. Handle it as a payment request and you ship without revocation and without counters.

On this page