SDK reference
Errors
PathApiError, what retryable actually covers, and the codes that look retryable and are not.
PathApiError
Every non-2xx response becomes one.
import { PathApiError } from '@pathprotocol/sdk';
try {
await path.createRequest({ amount: '5000', currency: 'XOF' });
} catch (err) {
if (err instanceof PathApiError) {
err.status; // 429
err.code; // 'path.core.rate_limited'
err.message; // human-readable, for a log — do not parse
err.requestId; // 'req_8f21c4' — quote this in support
err.detail; // structured, optional
err.retryable; // true
}
}Retrying
async function withRetry<T>(fn: () => Promise<T>, attempts = 4): Promise<T> {
for (let i = 0; ; i++) {
try {
return await fn();
} catch (err) {
const retryable = err instanceof PathApiError && err.retryable;
if (!retryable || i >= attempts - 1) throw err;
const backoff = Math.min(2 ** i * 250, 8000);
await new Promise((r) => setTimeout(r, backoff * Math.random()));
}
}
}retryable covers 5xx and path.core.rate_limited. Everything else is a decision, not a hiccup —
retrying an expired request produces the same answer more expensively.
path.finder.budget_exhausted is not retryable, and looks like it is. It arrives as a 429, which
every retry helper in existence treats as "slow down". It is a monthly allowance tied to what your
member contributes to the index. Retrying more slowly changes nothing; look up fewer things, or talk
to the network.
Codes worth handling by name
if (err instanceof PathApiError) {
switch (err.code) {
case 'path.auth.unauthenticated':
// Check clock skew first. It is far more often the timestamp than an attack.
break;
case 'path.auth.credential_revoked':
// Stop. Retrying cannot help. Get a new credential.
break;
case 'path.finder.budget_exhausted':
// Not a rate limit. Reduce lookups.
break;
case 'path.finder.holder_unavailable':
// Someone does hold this key; their endpoint is down. Retryable, and
// worth telling the user "temporarily unreachable" rather than "not found".
break;
case 'path.address.commitment_mismatch':
// Do not send. Treat as a potentially compromised resolver, not a blip.
break;
case 'path.request.expired':
case 'path.request.revoked':
// Distinct from not_found. The payer is too late, not mistaken.
break;
}
}What errors never tell you
Why a lookup found nothing. key_unresolvable is returned identically for a key that does not
exist and for one that exists but is not visible to you. Do not write logic that tries to separate
them — the indistinguishability is deliberate, and a route that leaked it would answer "is this
person a customer of somebody" to anyone patient enough.
Anything about the internals. No stack traces, no query text, no infrastructure detail. If you
need an operator to investigate, give them the request_id.
Logging
logger.error('PATH call failed', {
code: err.code,
status: err.status,
request_id: err.requestId,
});Log the code, not the message. Messages are for humans and may be reworded; codes are part of the protocol and are stable. Dashboards built on message text break on a copy edit.