Callers
Errors and reject codes
What each refusal means, and the one thing to do about it.
A refusal is returned, never thrown. send() resolves with fulfilled: false for anything the network refused: a bad voucher, a route that does not exist, a carriage the route will not accept. Everything the client throws happened before the packet went out, or on chain. A thrown error means you have a configuration or a wallet to fix; a returned refusal means the network answered, and the answer was no.
const answer = await client.send('g.toon.store', { body: 'hello' }); if (!answer.fulfilled) { answer.code; // 'F03', 'F01', 'PAYMENT_REQUIRED', 'TRANSPORT_REQUIRED', … answer.message; // diagnostic text. Never branch on it; branch on the code. answer.refusedBy; // 'destination' | 'path' | 'edge' answer.accumulatedCost; // what the path costs, when the connector stated it }
Who refused
refusedBy | What it means |
|---|---|
destination | The reject came back sealed. Only the terminating connector holds the secret to seal one, so this is proof the destination itself said no. |
path | The reject arrived in plaintext. That identifies nobody: a hop short of the termination refused, or the termination could not open the wrap. |
edge | The connector you are attached to refused before routing at all: a greeting, or a wrong carriage. |
Sealed identifies the destination; unsealed identifies nobody. Do not read a plaintext reject as an accusation against any particular node.
Reject codes
| Code | Means | You do |
|---|---|---|
F00 | Bad request: the envelope's target escaped the route's handler path (an absolute path, a .. segment, a scheme or an authority). | Fix target. It is resolved beneath the handler; '' and '/' both mean the handler itself. |
F01 | A malformed voucher, one that does not verify, or a claim of a retired kind. | If it persists, run claim-state and compare the connector's figure with channel status. A figure ahead of yours usually means a lost channel store. |
F02 | No route to that destination, including "no route over this carriage". | Check the destination against describe(). If it carries requiredTransport, send over that carriage. |
F03 | The voucher did not advance the connector's watermark by the route's price, or its cumulative amount exceeded the channel's deposit, or the declared amount exceeded the price on a forwarded route. | Underpayment: accumulatedCost is the route's price; pay that. Over deposit: top up and send again. Forwarded route: send exactly the price. |
F06 | No voucher was attached, on the BTP carriage. This is the greeting: the route's terms instead of the work. | Open or fund a channel, then send with a voucher. |
T00 | The connector hit an internal error. Temporary. | Retry with backoff. |
T01 | A peer on the path is unreachable. Temporary. | Retry with backoff. The packet is fine; the path currently is not. |
T04 | The packet exceeds the cap a connector will forward to one peer in a single packet. Never carried, never split. | The reject's message states the cap. Send a smaller packet; this is the only way the cap is published. |
T05 | Rate limited. | Back off. On a probe this is the per-channel probe limit. |
R00 | The packet expired before it arrived. | Retry. Persistent expiry means a slow path or a clock well out of step. |
accumulatedCost rides beside the packet, only on a REJECT. It is one figure: every hop's flat fee plus the price of the route that terminated, never a breakdown. It is 0 when nothing was traversed and nothing terminated.
Client-side codes
Two code values do not come from the wire. They are refusals the client names when the connector answered with a greeting rather than a packet:
| Code | Means | You do |
|---|---|---|
PAYMENT_REQUIRED | The connector answered the route's terms instead of the work: HTTP 402, or F06 over BTP. answer.terms carries the price and the x402 channels it accepts. | Open or fund a channel on one of the offered chains. |
TRANSPORT_REQUIRED | The route does not accept the carriage you used. answer.terms.requiredTransport names the one it does. | Resend with transport: 'btp', or leave auto against a node that publishes the pin on the route. |
HTTP statuses
An ILP-level outcome is always HTTP 200, whether the packet fulfilled or was rejected. A non-2xx is a transport-level failure and never carries a packet body.
| Status | Means | You do |
|---|---|---|
400 | The body was not a decodable PREPARE, or was oversized. | A bug in whatever formed the packet, or an encoder that is not TOON's dialect. |
401 | An ILP-Peer-Id was presented and did not authenticate. | Drop the peer id, or fix the credential. Anonymous payers need neither: the voucher identifies you. |
402 | Unpaid request to a priced route, or a request over a carriage the route refuses. The body is the terms document. | Pay, or switch carriage. |
403 | A probe from a sender with no channel the connector recognizes, or over the probe rate limit. | Open and use a channel first. Probing is free traversal, gated on having paid before. |
413 | The body exceeded the 2 MiB limit. | Send less. There is no configuration knob for this on the connector. |
Errors the client throws
All of them extend ToonClientError, which carries a stable code string. Branch on the class or on code, never on the message. ConfigError (CONFIG) is the one you will meet first: a missing required field, no facilitator for a Base network with no default, or no x402 channel offered on the chain asked for. NetworkError is an edge that could not be asked; SponsorRefusedError carries a Solana node's own reason for refusing an open. The full table on GitHub lists every class.
Written from toon-client/docs/errors.md.