Docs
Errors
One error envelope, a namespaced registry of codes, and what a caller should actually do with each.
One envelope
{
"error": {
"code": "path.request.expired",
"message": "This request has expired",
"detail": { "expired_at": "2026-09-08T10:00:00Z" },
"request_id": "req_8f21c4"
}
}Every error, everywhere. A caller writes one handler.
code is stable and machine-readable. message is for a developer reading a log and MUST NOT
be parsed. detail is optional and structured. request_id is echoed in the Path-Request-Id
header and is what you quote in a support conversation.
Codes are part of the protocol
Namespaced path.<area>.<condition>, so the code alone tells you which part of the specification to
open.
Adding a code is additive. Changing what one means is breaking, and requires a major protocol bump.
The registry
Envelope and versioning
| Code | Status | What it means | What to do |
|---|---|---|---|
path.core.unsupported_version | 400 | Unknown API version | Pin a version the operator lists in discovery |
path.core.malformed_request | 400 | The body failed validation | Read detail; fix the caller |
path.core.not_found | 404 | No such object | Not retryable |
path.core.rate_limited | 429 | Too many requests | Back off, then retry |
path.core.internal | 500 | Unexpected failure | Retry with backoff; quote request_id |
Authentication and membership
| Code | Status | What it means | What to do |
|---|---|---|---|
path.auth.unauthenticated | 401 | Missing, stale or invalid signature | Check clock skew first — it is usually the timestamp window |
path.auth.credential_revoked | 401 | The credential was revoked | Stop retrying. Get a new one |
path.auth.member_not_active | 403 | The member is suspended or excluded | A membership matter, not a technical one |
Reachability
| Code | Status | What it means | What to do |
|---|---|---|---|
path.finder.budget_exhausted | 429 | Reciprocity budget spent | Not a rate limit — do not simply retry slower. Reduce lookups or discuss the budget |
path.finder.key_unresolvable | 404 | Nothing reachable at this key | Indistinguishable from "not visible to you", by design |
path.finder.holder_unavailable | 503 | The holder is listed but not answering | Retryable. Distinct from not found — someone does hold this |
Identity
| Code | Status | What it means | What to do |
|---|---|---|---|
path.id.attestation_revoked | 410 | The claim was withdrawn | Ask the issuer for a current one. Not the same as unknown |
path.id.attestation_expired | 410 | Past its expiry | Same — do not reuse it |
path.id.binding_revoked | 410 | The ownership binding was withdrawn | Re-prove control |
path.id.binding_expired | 410 | The binding has expired | Re-prove control |
These are execution-time answers. A 410 here means the object existed and is no longer usable; a 404 means it never did. Collapsing them would hide a revocation.
Address
| Code | Status | What it means | What to do |
|---|---|---|---|
path.address.revoked | 410 | The address was revoked | Ask the recipient for a current one. Not the same as unknown |
path.address.commitment_mismatch | 409 | A resolver answer diverges from the commitment published for that address | Do not send. Treat as a potentially compromised resolver |
Raised client-side, by checkCommitment, not returned by an endpoint: the divergence is between two
documents the payer fetched, and only the payer holds both.
Request
| Code | Status | What it means | What to do |
|---|---|---|---|
path.request.expired | 410 | Past its expiry | Ask the issuer for a new one |
path.request.already_paid | 409 | Already paid | Not an error to retry — check your own state |
path.request.revoked | 410 | Revoked by the issuer | Stop |
path.request.amount_required | 400 | Open amount, and none supplied | Prompt the payer |
Connect
| Code | Status | What it means | What to do |
|---|---|---|---|
path.connect.delegation_out_of_scope | 403 | Outside the granted capabilities | Request a wider grant, explicitly |
path.connect.delegation_limit_reached | 403 | The counter is spent | Surface it to the user; do not retry |
Settlement
| Code | Status | What it means | What to do |
|---|---|---|---|
path.settlement.transition_invalid | 409 | The receipt cannot move from the state it is in to the one asked for — failed and reversed are terminal, and reversed follows only settled | Read the receipt; if it is already where you wanted it, you lost a race and there is nothing to do |
path.settlement.receipt_unverifiable | 500 | The issuer stored this receipt without its signed envelope and cannot reproduce the bytes it signed | Nothing the caller can retry. The receipt exists and its proof does not; ask the issuer |
A signature that simply fails to verify is not this code. POST /receipts/verify answers
{ "valid": false, "reason": … } with a 200 — the verification ran and came back negative, which is
an answer, not a failure of the request.
What errors never contain
No stack traces. Ever, in any environment reachable from outside.
No hint about 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 the caller.
Retrying
if (err instanceof PathApiError && err.retryable) {
// 5xx and rate limits: exponential backoff, full jitter
}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 deserves special mention: it looks like a rate limit and is not one.
Retrying more slowly does not help, because the allowance is monthly and tied to what you contribute
to the index. The fix is to look up fewer things, or to talk to the network.