npm packages are not installable yet: [email protected] can be published after 2026-09-16 10:30 UTC. Details
Tollstile

Errors

Machine-readable denials — a stable code, whether retrying helps, and what to do next.

Public Beta · early access

Core denials use a common body shape, so an agent can recover without reading prose. Configuration errors and unexpected exceptions are separate TollstileError failures; adapters may rethrow them to the framework.

{
  "error": {
    "code": "insufficient_authorization",
    "retryable": true,
    "action": "pay",
    "message": "The payment authorizes $0.03; this request costs $0.04.",
    "detail": null
  },
  "resource": "POST /v1/translate",
  "required": "$0.04",
  "authorized": "$0.03"
}

A payment challenge carries price, variable, quote, nonce, expiresAt, and accepts. A requirement denial with status 402 has the error envelope but no payment offers. A settlement rejection after the handler consumed the body can also lack a fresh quote; request a new challenge before constructing a new proof.

FieldUse
codeStable. Branch on it.
retryableWhether the same logical request can still succeed
actionpay: pay using accepts in this response. retry_later: send the same request with the same payment and idempotency key after Retry-After. fix_request: change the request. stop: do not retry.
messageFor humans; may change
detailThe rail's or requirement's own reason, such as transfer_mismatch. Log it; do not branch on it.

Codes

codeStatusactionWhen
payment_required402payNo payment
quote_required402payThe price is computed per request and the payment carried no quote
quote_invalid402payThe quote is forged, expired, or for another resource
quote_mismatch402payThe request differs from the one the quote priced
quote_offer_missing402payThe quote has no offer for the rail used
proof_invalid402payThe rail rejected the payment; detail says why
insufficient_authorization402payThe payment authorizes less than the price; required, authorized
authorization_expired402payThe payment authorization expired
payment_rejected402payThe provider rejected an upfront payment
settlement_rejected402payThe provider rejected settlement after the handler; the output was withheld
requirement_failed402 · 403 · 429pay · stop · retry_laterA requirement refused; requirement, detail
access_denied403stopNo access policy admits the caller
proof_already_used409stopA single-use payment was already used by another request
request_in_progress409retry_laterSame idempotency key; the first attempt is still running
already_paid409stopSame idempotency key; the request was already paid; chargeId, settlement, result
idempotency_key_reused422fix_requestSame idempotency key, different request
invalid_request400fix_requestMalformed idempotency key or MCP envelope
payment_unavailable503retry_laterA provider needed to verify or challenge is down
payment_outcome_unknown503retry_laterSettlement outcome unknown; retry with the same payment and key
requirement_unavailable503retry_laterA requirement cannot check its evidence right now

Denials include Cache-Control: no-store. Only action: "retry_later" includes Retry-After: 5 (including retryable 429). already_paid and proof_already_used have action: "stop" and no retry header. On MCP, tool-result denials carry the body in _meta["tollstile/payment-required"]; MPP challenge errors use the protocol-specific JSON-RPC shape described in MCP.

An agent's loop

const response = await fetch(url, { headers });
if (response.ok) return response;

const { error, accepts } = await response.json();
switch (error.action) {
  case "pay":         return payAndRetry(accepts);            // new payment, same Idempotency-Key
  case "retry_later": return retryAfter(response.headers.get("retry-after"));
  case "fix_request":
  case "stop":        throw new Error(`${error.code}: ${error.message}`);
}

On this page