GitHub
PATH REQUEST

SDK reference

Payment requests

Issuing a request, reading one as any wallet, and verifying who signed it.

createRequest(input, idempotencyKey?)

Requires a credential.

const request = await path.createRequest(
  {
    amount: { value: '5000', asset_type: 'fiat', asset_code: 'USD' },
    accepts: [
      {
        rail: 'book',
        asset_type: 'fiat',
        asset_code: 'USD',
        fees: [{ kind: 'operator', amount: { value: '25', asset_type: 'fiat', asset_code: 'USD' }, paid_by: 'payer' }],
      },
    ],
    orderReference: 'ord_10482',
    expiresAt: new Date(Date.now() + 86_400_000).toISOString(),
  },
  `ord_10482`,
);

request.reference; // "7fk2m9pq3vx8"
request.url;       // "https://api.example.com/path/7fk2m9pq3vx8"

Open amounts

await path.createRequest({
  amount: null,
  accepts: [{ rail: 'book', asset_type: 'fiat', asset_code: 'USD' }],
});

Omit amount and the payer chooses. A first-class case — a tip, a donation, an open invoice — not a missing field.

Bound what the payer may enter with limits on each entry, in that entry's asset (PIP-0015):

await path.createRequest({
  amount: null,
  accepts: [
    {
      rail: 'book',
      asset_type: 'fiat',
      asset_code: 'XOF',
      limits: {
        min_single: { value: '1000', asset_type: 'fiat', asset_code: 'XOF' },
        max_single: { value: '50000', asset_type: 'fiat', asset_code: 'XOF' },
      },
    },
  ],
});

Issued against an address, the limits sit inside the standing intent's; a bound left out is copied from it. The payment instruction for an open request carries the same limits.

Display rules

State how the request is shown (PIP-0016). A checkout on it is shown the same way.

await path.createRequest({
  amount: { value: '12500', asset_type: 'fiat', asset_code: 'XOF' },
  accepts,
  display: { primary: 'amount', settlement: 'hidden', equivalence: 'EUR' },
});

On the paying side, resolve the rules for the screen. funding is the asset of the payer's account: it applies the floor.

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

const rules = resolveDisplay(req, { via: chosenEntry, funding: payerAccountAsset });
rules.primary;     // 'amount' | 'quote'
rules.settlement;  // 'hidden' | 'asset' | 'asset_and_chain'
rules.equivalence; // 'EUR' — at the wallet's own rate, labelled indicative

Whatever the rules say, show what leaves the payer's account, fees included, and the payee's verified name.

Idempotency

Use your own order id as the key. On the retry after a timeout, when you do not know whether the first call landed, a replay returns the original request instead of creating a second one.

Fees

Each accepts entry carries its fees. They travel inside the signature, so they cannot differ from what the payer was shown. A price disclosed after the decision is not a disclosure.


readRequest(referenceOrUrl, opts?)

No credential. Accepts a reference or a full URL, so a scanned QR code goes straight in.

const req = await path.readRequest('https://pay.example.com/path/7fk2m9pq3vx8');

req.amount;   // { value: "5000", asset_type: "fiat", asset_code: "USD" } — or null
req.issuer;   // "example-member"
req.accepts;  // capabilities, with fees on each entry
req.status;   // "created"

Verify before displaying

const req = await path.readRequest(url, { verifyWith: issuerPublicKeyHex });
if (!req.verified) throw new Error('Refusing to display an unverified request');

or, fetching the key for you:

import { verifyAgainstIssuer } from '@pathprotocol/sdk';
const { valid, reason } = await verifyAgainstIssuer(req, 'https://api.example.com');
The domain says      WHERE TO FETCH
The signature says   WHO IS RESPONSIBLE

Then show the issuer's verified name, never the raw URL. A payer confronted with an unfamiliar hostname closes the app — which is why a card terminal shows a merchant name rather than an acquirer's.

Expiry and revocation

try {
  const req = await path.readRequest(reference);
} catch (err) {
  if (err instanceof PathApiError) {
    switch (err.code) {
      case 'path.request.expired': // too late — ask for a new link
      case 'path.request.revoked': // the issuer withdrew it
      case 'path.core.not_found':  // wrong reference entirely
    }
  }
}

The three are distinguished on purpose. A payer told "not found" when a link merely expired will retype it, blame themselves, and call support.


revokeRequest(reference)

Requires a credential. Only the issuer may revoke.

await path.revokeRequest('7fk2m9pq3vx8');

Irreversible. A payer opening the link afterwards is told it was revoked, not that it never existed.


markPaid(reference, reason?)

Requires a credential. Separate from issuing the receipt.

A grammar-2 request moves to paid on its own once settled receipts cover it. This call checks the coverage and answers path.request.not_covered when it falls short.

await path.markPaid('7fk2m9pq3vx8');

Checkout — the request plus the return URLs — is its own page.

On this page