Errors
There are two families, and they are raised in different places: HTTP errors an agent
returns over the wire, and RiparError codes the SDK throws in your own process.
Both carry a stable code. Match on that, never on message.
{
"error": {
"code": "invalid_input",
"message": "text is required.",
"field": "text",
"schema": { "type": "object", "required": ["text"] }
}
}HTTP errors from an agent
| Code | HTTP | Meaning | Do this |
|---|---|---|---|
invalid_input | 400 | Body failed the published input schema | Fix the request against the schema echoed back |
rate_limited | 429 | Over the agent's perMinute for this payer or IP | Back off per Retry-After |
idempotency_in_progress | 409 | A request with this Idempotency-Key is still running | Retry shortly |
idempotency_key_reuse | 409 | Same key, different body | Use a new key |
handler_error | 500 | The handler threw | Nothing settled. Check the agent's logs. |
handler_timeout | 504 | Exceeded the endpoint's timeout | Nothing settled. Raise timeout or make the work smaller. |
shutting_down | 503 | The agent got SIGTERM and is draining | Retry in a few seconds, per Retry-After |
not_found | 404 | No such endpoint on this agent | Check the manifest for real endpoint names |
A 402 carries no code — the quote is in the PAYMENT-REQUIRED header and the body is
{}. See Payments & settlement.
The payment middleware buffers the response and cancels settlement on a 4xx or 5xx,
so every error above leaves the caller uncharged. There is no refund to chase.
RiparError codes
Every failure the SDK raises is a RiparError, so one catch covers definition, serving
and calling:
import { RiparError } from "@ripar/sdk";
try {
await ripar.call(url, body);
} catch (err) {
if (err instanceof RiparError && err.code === "price_above_max") {
// the quote moved; re-quote or skip the call
}
throw err;
}| Property | Type | Notes |
|---|---|---|
code | string | Stable identifier — branch on this |
status | number | undefined | HTTP status, when the failure came from a response |
detail | unknown | Response body or payment requirements, when there were any |
At definition time
These throw at module load, not at request time, so a bad definition fails the build rather than the first paying customer.
| Code | Cause |
|---|---|
invalid_name | Endpoint name is not lowercase [a-z0-9-/] — it becomes the URL path |
invalid_price | Not a positive USD amount like "$0.01" |
invalid_timeout | Outside 1,000–300,000 ms |
invalid_endpoint | No handler function |
invalid_agent | The agent declares no endpoints |
invalid_handle | Handle is not 3–40 lowercase characters, digits or hyphens |
invalid_address | payTo is not 58 base32 characters |
duplicate_endpoint | Two endpoints share a name, so they would share a URL |
While calling
| Code | Cause | Do this |
|---|---|---|
no_signer | The client was built without a mnemonic or secretKey | It can quote, but not pay. Give it a key. |
price_above_max | The quote exceeded your maxPrice | Raise the cap deliberately, or pick another endpoint |
daily_cap_reached | maxPerDay is spent for this rolling 24h | Wait, or raise it deliberately |
unreadable_quote | A cap is set and the 402 could not be decoded | The client refuses to pay blind. Check the endpoint's headers. |
invalid_max_price | maxPrice or maxPerDay is not a positive number | Fix the client options |
network_error | The request never reached the server | Safe to retry; nothing settled |
call_failed | Non-2xx response; status and body are on the error | Depends on the status — see the table above |
not_a_subscription | subscribe() paid, but no key came back | The endpoint is priced per call. Use call(). |
no_manifest | discover() found no /.well-known/ripar.json | Check the host and that the path is unpaid |
Retrying safely
RiparClient retries on 5xx and transport failures only — never on a 4xx, which is the
server saying the request itself is wrong. Three attempts by default:
new RiparClient({ mnemonic, retry: { attempts: 5 } });
new RiparClient({ mnemonic, retry: false }); // never retryAnything that already ran a handler should only be retried with the same idempotency key, or you will pay twice.