Cookbook
Verify a receipt
Check a settlement receipt issued by someone else — online, offline, and in bulk.
A receipt is only worth something if a party that was not involved can check it. No credential required.
The straightforward case
import { verifyAgainstIssuer } from '@pathprotocol/sdk';
export async function checkReceipt(envelope: SettlementReceipt, issuerBaseUrl: string) {
const { valid, reason } = await verifyAgainstIssuer(envelope, issuerBaseUrl);
if (!valid) {
return { ok: false, reason };
}
return {
ok: true,
amount: envelope.amount,
currency: envelope.currency,
rail: envelope.rail,
settledAt: envelope.signed_at,
payee: envelope.payee_member,
};
}Handling the reasons properly
const { valid, reason } = await verifyAgainstIssuer(receipt, issuerUrl);
if (!valid) {
if (reason?.includes('publishes no key with kid')) {
// Almost always a key rotation you have not picked up. Refetch and retry once
// before concluding anything — this is not an attack.
keyCache.invalidate(issuerUrl);
return checkReceipt(receipt, issuerUrl);
}
// A real failure: the envelope does not match a key the issuer publishes.
return { ok: false, reason };
}Conflating "unknown key id" with "bad signature" turns a routine rotation into a support incident.
Offline
Once you hold the key, verification needs no network at all.
import { verifyEnvelope } from '@pathprotocol/sdk';
const keys = await loadCachedKeys(issuerUrl); // fetched earlier
const key = keys.find((k) => k.kid === receipt.kid);
const valid = key ? verifyEnvelope(receipt, key.public_key_hex) : false;This is what lets a receipt be shown as a QR code and checked by a supplier with no connectivity and no account anywhere — the concrete form of finality is a verifiable state, not a promise.
In bulk
export async function verifyStatement(receipts: SettlementReceipt[], issuerUrl: string) {
const { keys } = await fetch(`${issuerUrl}/.well-known/path-keys`).then((r) => r.json());
const byKid = new Map(keys.map((k) => [k.kid, k.public_key_hex]));
return receipts.map((receipt) => {
const key = byKid.get(receipt.kid);
return {
reference: receipt.reference,
valid: key ? verifyEnvelope(receipt, key) : false,
reason: key ? undefined : `unknown kid ${receipt.kid}`,
};
});
}One key fetch, many verifications. Useful at month end, when reconciling a statement against what counterparties claim they paid.
Reconciling against your own records
export function reconcile(receipt: SettlementReceipt, ourRecord: LedgerEntry) {
const discrepancies: string[] = [];
if (receipt.amount !== ourRecord.amount) {
discrepancies.push(`amount: receipt ${receipt.amount}, ledger ${ourRecord.amount}`);
}
if (receipt.currency !== ourRecord.currency) {
discrepancies.push(`currency: ${receipt.currency} vs ${ourRecord.currency}`);
}
if (receipt.request && receipt.request !== ourRecord.requestReference) {
discrepancies.push('request reference does not match');
}
return { matched: discrepancies.length === 0, discrepancies };
}Record discrepancies; do not silently correct them. A system that quietly reconciles differences away is one where nobody notices the fee that was never disclosed, or the rail that rounds in one direction every time.
Where to get the key
From the issuer's own discovery document, always.
Not from whoever handed you the receipt, and not from a central registry. An operator vouching for another operator's key rebuilds the hierarchy the protocol avoids — and it means a compromise of one operator becomes a compromise of statements about others.
What a valid signature does and does not tell you
It tells you: this envelope was produced by the holder of that key, and has not been altered since.
It does not tell you: that the issuer is honest, solvent, or that the funds truly moved. A signature proves authorship, not truth.
For the second question you need reconciliation against an independent view — the rail, a statement,
a chain — which is why receipts carry source_tx_hash and source_reference. The signature makes
the claim attributable; the reconciliation makes it credible.