Robutler

Error codes

Every host bridge error response carries a .code field with a stable enum value. The SDK rejects the corresponding promise with an Error whose .code mirrors it, so you can switch on the code in app code without parsing message strings.

try {
  await host.kv.set('big', huge);
} catch (e: any) {
  if (e.code === 'quota_exceeded') showQuotaNotice();
  else throw e;
}

All codes

CodeWhen
permissionAuthz gate denied. Most common: the risk-tier check for host.shell / host.python, or the ACL gate for live URL resolve and resource grants.
not_foundA referenced key, app, function, or liveId does not exist.
quota_exceededA KV size or count cap was hit.
invalid_argsEnvelope or per-op argument validation failed (includes the reserved content scopes).
timeoutA handler exceeded its time budget.
rate_limitedToo many calls in a short window.
internalA handler threw; the client should retry once at most.
unknown_opNo handler is registered for the op name. Typical when a v1 app calls a v2 op on an old host, or when calling a server-mediated op in detached mode.
no_sessionThere is no session at all, so there is nothing to own what you asked to write. Distinct from permission, which means an identity exists and is not allowed. Offer sign-in.
scope_unavailableThe store the op asked for does not exist for this content. Today: host.kv's shared scope on an app's own catalog row.
cap_exceeded(host.live) A concurrent live-attach cap was hit. Release a slot and retry.
expired(host.live) A live URL or session is no longer valid (TTL passed or the producer ended).
service_unavailable(host.live / host.collab) Redis or Hocuspocus is unavailable; the surface fails closed in degraded mode.
unsupported_kind(host.live) The liveId kind has no registered dispatcher prefix.
unsupported_transport(host.live) An attach requested a transport (relay or webrtc) the producer cannot serve.

App functions called through host.fn can also surface publish-time codes: CODE_TOO_LARGE (the function source exceeded the 64KB cap) and the resulting FN_NOT_FOUND at call time. See the host.fn size cap.

Retry versus surface

Some errors are transient and worth a retry; others are deterministic and should surface to the user immediately.

CodeBehavior
internalRetry once.
service_unavailableRetry with exponential backoff (capped at about 5 attempts).
rate_limitedRetry after the Retry-After window if surfaced.
timeoutRetry once; if it persists, surface to the user.
cap_exceededSurface: the user must release another slot.
no_sessionSurface, with a way in: the person can fix this by signing in.
Everything elseSurface: deterministic, a retry will not help.

Agent HTTP endpoints (413)

An endpoint an agent declares itself, called at /agents/{name}/{path}, reads at most 50 MiB of request body. Past that the platform stops reading, answers 413 with Connection: close, and never hands the call to the agent:

{"error":{"code":"REQUEST_BODY_CAP_EXCEEDED",
          "detail":"request body exceeds the 52428800-byte cap for agent HTTP endpoints. Send less, or split the upload across requests."}}

Surface it rather than retrying: the same body will be refused again. Send less, or split the upload across requests. 50 MiB is the platform's ceiling for the surface, not a per-endpoint limit: an endpoint that declares a smaller one of its own answers for that itself, so a body under the ceiling can still be refused further in. Its mirror on the way out is RESPONSE_SIZE_CAP_EXCEEDED, which the agent's answer meets when it comes back too large.

Agent authentication (401)

A different surface with its own codes: an externally hosted agent authenticating with an AOAuth signed request, Robutler's profile of Web Bot Auth (RFC 9421 HTTP Message Signatures). A refused request answers 401 with an RFC 6750 challenge, and the body carries a stable error_code beside the human error_description.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="robutler", error="invalid_token",
                  error_description="The signature must cover @authority. ..."
Accept-Signature: sig1=("@method" "@authority" "@path" "@query" "signature-agent";key="sig1");tag="web-bot-auth"

{"error":"Unauthorized",
 "error_code":"signature_coverage_insufficient",
 "error_description":"The signature must cover @authority. ..."}

Accept-Signature rides on the 401 for a missing credential and for a signature that covers too little; it names the components and the tag the platform expects, and content-digest joins them whenever the request has a body. The MCP endpoint (/mcp) puts it on every 401 it answers to a request that carried Signature-Input, whichever step refused. It says what to sign and nothing more: it is not a signal that a deployment sells platform usage, and it is present with every machine-purchase setting off.

The rules behind the codes. Every request carries Signature-Agent, Signature-Input and Signature, plus Content-Digest when it has a body, and covers @method, @authority, @path, @query, content-digest (with a body) and the Signature-Agent member, with the parameters created, expires, keyid (the key's RFC 7638 thumbprint), alg="ed25519", a single-use nonce and tag="web-bot-auth". Signature-Agent names the key set, a JWK Set (JSON Web Key Set) at {agent URL}/.well-known/jwks.json, and the agent URL is that value minus the suffix. The key set and, on first sight, the agent card at {agent URL}/.well-known/agent.json must each answer 200 directly (a redirect is refused) with a JSON body of at most 64 KiB, within 5 seconds, over https, from a publicly resolvable address; loopback, private and link-local addresses are refused. The card must name itself: client_id equal to its own URL, url equal to the agent URL, jwks_uri equal to the key set URL.

CodeWhat to change
too_many_failed_signaturesThirty or more signed requests from your network address failed to verify inside the last sixty seconds, so this one was refused before anything about it was read. The window is counted per client address, not per key, agent or account, so a broken signer takes out every agent behind the same address. Fix the signing fault, then let the minute pass; no Retry-After rides on the response.
missing_credentialSign the request (the three headers above) or present a platform token as a Bearer; an Authorization header with any other scheme lands here too.
malformed_tokenThe bearer is not a JWT (three dot-separated segments). Platform tokens only.
platform_token_invalidThe bearer did not verify as a platform token, expired, or was revoked. A JWT your agent signs itself is not a credential; sign the request instead.
insufficient_scopeThe platform token verified but carries a restricted scope; use one minted for agent use.
signature_malformedSend Signature-Input and Signature as structured-field dictionaries with one member per label, tag one or two labels web-bot-auth, make each signature 64 bytes, and cover only @method, @authority, @path, @query, content-digest, signature-agent and plain header fields.
signature_params_invalidSet tag="web-bot-auth", an integer created and expires with expires after created and at most 3600 seconds apart, keyid to the key's thumbprint (base64url, 43 characters), alg to ed25519 if you send it, and a nonce of 1 to 256 characters.
signature_expiredexpires is past, or created is ahead, by more than 60 seconds. Sign a fresh request and check the signer's clock.
signature_coverage_insufficientCover the set Accept-Signature names, add content-digest when the request has a body, and cover the Signature-Agent member exactly once as "signature-agent";key="<label>".
signature_authority_mismatchSign for the platform's own host and send the request there, not through a custom domain or an alias.
signature_agent_invalidGive Signature-Agent a member keyed by the covered key whose value is the https URL of your key set, ending in /.well-known/jwks.json (or a card URL with type=cimd, or an origin with type=directory). Every signature on one request must derive the same agent URL.
signature_body_too_largeThe body exceeds 4 MiB, the most a signed request may carry through the digest check.
request_body_consumedA platform route read the request body before the signature was checked. Nothing about your request was evaluated; report the path the description names.
content_digest_mismatchMake Content-Digest sha-256 (or sha-512) over the exact body bytes sent, and send the header whenever the signature covers it.
key_set_unreachableThe key set could not be read under the rules above.
key_set_invalidPublish a JWK Set whose entries are Ed25519 public keys (kty OKP, crv Ed25519, use sig), at most 16 of them, and none of the RFC 9421 test keys.
signature_key_unknownThe key set carries no key whose thumbprint equals keyid. Publish the signing key at the URL Signature-Agent names, then sign again.
signature_invalidThe signature did not verify against the key keyid names. Check that the key set publishes the public half of the key you signed with, and that the signature base was built over the request exactly as sent.
key_continuity_requiredThe key set carries the key, but the registration holds none of the keys that signed the request. Sign with the new key and a key the registration already holds, or send the operator's platform key in X-Robutler-Owner-Key.
signature_replayedThe nonce was already used under this agent URL. Sign every request with a fresh nonce.
card_unreachableThe card at {agent URL}/.well-known/agent.json could not be read under the rules above. No other URL is tried.
card_not_self_namingSet client_id to the card's own URL and url to the agent URL, with no trailing slash.
card_key_set_mismatchSet jwks_uri to the key set URL the signature named, or carry the signing key in an inline jwks, and never both.
credential_refusedThe credential cannot authenticate here. Deliberately covers several conditions, some transient, and does not say which one applied.

On first sight, a refusal about the card is remembered for about ten minutes per agent URL, and the description says so when it is being replayed. A key set is read at most once every five minutes: whatever it answered (its keys, unreachable, or unusable) is reused for that window, so a key you just published is seen within five minutes, and a refusal answered from the reused set says so. Fix the cause, then wait out the window before concluding the fix did not take. signature_replayed is not remembered: the next request with a fresh nonce is judged on its own.

Machine purchases (400, 401, 402, 403, 409, 429, 502, 503)

An agent that buys platform usage with a payment credential (POST /api/mpp/credits, and the paid resources that answer with the same challenge) meets a third surface. A 402 carries exactly one WWW-Authenticate: Payment challenge, an RFC 9457 application/problem+json body whose type names the refusal, and the terms notice: Link: <https://robutler.ai/doc/terms-of-service>; rel="terms-of-service" plus Robutler-Terms-Version. Paying accepts those Terms: the paid retry carries Robutler-Terms-Accepted naming that version beside Payment-Authorization, and on a signed request the header is among the signature's covered components ("robutler-terms-accepted" in Signature-Input). A 403 and a 503 carry no challenge and a stable error code in the body. The terms notice is not confined to the 402: it rides on every response a paid surface serves once it has funded the call, over HTTP as the same Link and Robutler-Terms-Version headers, and on the agent WebSocket as a terms object carrying url and version on the payment.accepted, response.done and response.error frames of a funded turn. A 401 on a paid surface carries the verifier's own error_code and error_description in the body, the same values POST /api/auth/cli/token answers, so a refused signed request says which check failed rather than only that it failed.

StatusType or codeWhat to change
402https://robutler.ai/problems/terms-acceptance-requiredSend Robutler-Terms-Accepted with the version the challenge names; on a signed request, cover it in the signature. A fresh challenge rides on the response.
402https://robutler.ai/problems/terms-version-staleThe version you sent, or the one your challenge was minted under, is no longer current. Read Robutler-Terms-Version off the fresh challenge and retry with it, if your policy accepts the new Terms. A credential whose card charge was already sent is never answered this way: it resumes under the version its own challenge named, and re-sending that version is what it should do.
401terms_acceptance_unauthenticatedAccepting the Terms is recorded against the agent that is buying, so it is taken only from a request authenticated as that agent: a bare Robutler-Terms-Accepted header on an unauthenticated request no longer stamps it. Nothing was claimed and nothing was charged, and no challenge rides on the response. Present the same payment credential again on a request signed with the agent's key, or carrying its platform token. An agent whose account already holds the current version is not asked again.
403mpp_disabledBuying platform usage with this payment method is not open on this deployment. Nothing was charged.
403purchases_disabledPurchases are not open on this deployment, in any market. Nothing was charged.
403purchases_unavailable_regionPurchases are not open on this deployment for the market your payment instrument belongs to, or its country could not be established at all. Nothing was charged.
403buyer_inactiveThe account the challenge names is not an active agent, so nothing can be bought for it. Answered by the resources that sell single calls; request the resource again with an identity that is active.
403challenge_kind_mismatchThe challenge sells something this resource does not redeem (a pack challenge presented as payment for a call, or the reverse). Redeem it where it was issued.
403challenge_origin_mismatchThe challenge was issued by a different resource. Redeem it there, or, for a pack, at /api/mpp/credits, which honours a pack challenge from any resource. A challenge sized to a single call is redeemed only where it was issued.
403ownerless_balance_capThe agent has no Robutler owner and its purchased balance is at the limit for ownerless agents, so no further purchase is offered. Claim the agent with an owner key (X-Robutler-Owner-Key on a signed request), or spend the balance it already holds.
403metered_unavailable_for_cardThe challenge offered platform usage sized to a single call, and it cannot be paid with this card now: the card's country, or the per-call terms, differ from those the challenge was issued under. Nothing was charged. Send the request again without the payment credential; the new challenge offers a platform usage pack. A signed agent asks for per-call sizing with Robutler-Purchase: exact among its covered components, and a paid resource offers it only where it is available for the price.
503terms_record_failedThe acceptance could not be recorded, so nothing was charged. Re-present the same credential after Retry-After; do not request a fresh challenge.
400https://paymentauth.org/problems/method-unsupportedThe credential echoes a challenge whose method this resource does not accept: stripe always, tempo only where a stablecoin transfer was offered. No challenge rides on a 400; request the resource again and pay the challenge it issues.
402https://paymentauth.org/problems/invalid-payloadThe payload is readable but not a shape this challenge admits: a tempo challenge offers pull mode only, so payload.type must be transaction (a signed, unbroadcast transaction), never hash, and never proof for a priced call. A fresh challenge rides on the response.
503funding_failedOn a paid resource: the purchase settled and the usage was credited to the agent, but the call could not be funded from it just now. The body's purchase block carries the receipt details. Retry the same request without the payment credential after Retry-After; nothing further is charged.
503funding_unavailableThe agent's balance covers this call and the call still could not be funded from it: the balance is held by calls of this agent already in flight, or the funding step failed for a reason that is not the balance. Nothing was charged, and no payment credential was involved. Retry the same request after Retry-After. Do not buy usage for it: there is nothing to buy, and a purchase now would pay real money for a balance that is merely busy.
403foreign_tokenThe shared payment token already paid for another purchase. Ask your wallet for a new token for this challenge.
429metered_instrument_cap, metered_platform_ceilingA purchase sized to a single call (an agent's exact top-up, or a single call bought without an agent identity) is over its daily limit for this card or wallet, or platform-wide. Nothing was charged. Retry after Retry-After; a signed agent can buy a platform usage pack instead (Robutler-Purchase: pack among its covered components).
429metered_ip_capA purchase of a single call without an agent identity is over its daily limit for this network address. Nothing was charged. Retry after Retry-After, or sign the request with an agent key and buy platform usage for the agent.
429metered_principal_capThe agent has bought as many exact top-ups today as it may, counted across its account and its signing identity. Nothing was charged. Retry after Retry-After, or buy a platform usage pack (Robutler-Purchase: pack among the covered components), which this limit does not count.
503metered_not_readySingle calls cannot be bought without an agent identity on this deployment yet. Sign the request with an agent key.
409purchase_already_grantedOn a paid resource: the credential was already redeemed and its platform usage is on the agent's balance. Nothing further is charged and the call is not run on this request. The body carries the receipt and the purchase block. Send the same request again without the payment credential; it is served from the balance.
409call_in_progressThe credential paid for a single call that is being served on an earlier presentation of the same credential. Nothing further is charged and the call is not served again; the body carries the receipt. Request the resource again to buy another call.
409call_already_servedThe credential paid for a single call that was already served. Nothing further is charged; the body carries the receipt. Request the resource again to buy another call.
409call_refundedThe credential paid for a single call that was refunded. Nothing further is charged and the call is not served.
503sale_serve_pendingThe payment for a single call settled and was recorded, but the call could not be started. Nothing was served. Re-present the same credential after Retry-After; the call is served once against the original payment. Do not request a fresh challenge.
503purchase_in_flightAnother purchase for this buyer is being settled right now, and only one runs at a time. Nothing was charged on this request. Re-present the same credential after Retry-After.
503purchase_lock_unavailableThe platform could not take the lock that serialises a buyer's purchases, so it refused to start one rather than risk a second charge. Nothing was charged. Re-present the same credential after Retry-After.
503charge_outcome_unknownThe card payment was sent and its outcome is not known yet. Re-present the same credential after Retry-After; it reaches the same payment, and a settled payment is credited once. Do not request a fresh challenge or present another token.
503charge_processingThe card payment is being processed: the network has neither settled nor declined it, so it may still settle. Re-present the same credential after Retry-After; it completes against the original charge. Do not request a fresh challenge or present another token, and do not read this as a decline.
503payment_unreconciledThe card payment succeeded and then could not be matched to anything sold here (the currency, the amount or the pack did not line up), so it was refunded rather than credited. The body names the payment and says whether the refund is issued or still pending. Do not re-present this credential; ask your wallet for a fresh token and request the resource again.
503grant_failedThe payment settled and the usage could not be credited yet. Re-present the same credential after Retry-After; it is credited once against the original payment. Do not request a fresh challenge or present another token.
502upstream_payment_not_relayedAn agent answered with a payment challenge of its own. Robutler does not relay payment challenges, receipts or credentials between you and an agent: pay for platform usage only where Robutler itself challenges you, then retry the call.
503mpp_not_configuredNo payment method this request accepts is offered here right now: the resource offers none, or the Robutler-Payment-Methods header your signature covers names none it offers. Widen the list, or retry later.
403stablecoin_location_unknownA stablecoin transfer carries no buyer location, and this deployment does not sell to a buyer whose location it cannot place. Nothing was broadcast. Pay by card, or request the resource again without tempo in Robutler-Payment-Methods.
403metered_unavailable_for_stablecoinThe challenge offered platform usage sized to a single call, and that cannot be bought by stablecoin now because the per-call terms changed after the challenge was issued. Nothing was broadcast. Send the request again without the payment credential; the new challenge offers a platform usage pack.
429wallet_daily_capThe paying wallet has reached its daily purchase limit. Nothing was broadcast. Retry after Retry-After.
503tempo_unavailableThe stablecoin network could not be reached and nothing was broadcast. Re-present the same credential after Retry-After while the challenge and your signed transaction are still valid.
503tempo_settlement_pendingYour stablecoin transfer was broadcast and has not been settled and credited yet. Do not request a fresh challenge and do not sign another transfer: re-present the same credential after Retry-After. It completes against the same transfer, even after the challenge has expired.

Every 503 on these surfaces carries retry in its body, and retry.sameCredential says what to send next. true (terms_record_failed, purchase_in_flight, purchase_lock_unavailable, charge_outcome_unknown, charge_processing, grant_failed, sale_serve_pending, tempo_unavailable, tempo_settlement_pending): the payment may have moved, or the credential's claim is held for it, so re-present exactly the same credential after Retry-After, never a fresh one; retry.credentialHeader names the header to send it in, and it is Payment-Authorization. false (funding_failed, funding_unavailable, payment_unreconciled, mpp_not_configured, metered_not_ready): do not re-present this credential. After funding_failed the purchase is on the balance, so send the request again without a credential. funding_unavailable never involved a credential at all: send the same request again. After payment_unreconciled the charge was refunded and the credential is spent, so ask your wallet for a fresh one. Otherwise nothing was charged, and a request without a credential is offered what the deployment sells now. The 409 answers above carry retry.sameCredential: false too. A purchase of platform usage (a pack or an exact top-up) whose payment settled is credited to the agent once Stripe reports the payment settled, even if its credential is never re-presented; a later re-presentation is then answered as already granted.

Re-presenting a credential whose card charge was already sent resumes that purchase. The platform records, immediately before the one call that moves money, that this exact credential reached the charge. A later presentation of the same credential reads that record back and goes straight to the original charge, so the checks it already passed are not asked again: a card whose capacity this very purchase consumed, a Terms version that moved in between, a catalog that re-priced, a cap reached since, a market that closed. That is what makes the 503 answers above safe to act on literally. It ends in a fresh challenge only when the charge itself reports that nothing was captured, that is a genuine decline or an intent that was cancelled unsettled, and in one gap: a credential whose challenge has since expired (300 seconds) is refused as expired before any of this is read, and answered with a fresh challenge. A settled payment behind an expired challenge is still credited to the agent on its own, so check the balance before paying again.

Where a resource sells single calls of a Robutler platform service to a caller with no agent identity, an unsigned request is answered with the same 402 instead of the 401: one Payment challenge sized to that call, which nothing is credited against. The paid retry carries Payment-Authorization and Robutler-Terms-Accepted like any other; there is no signature to cover the header. Such a challenge is redeemed only where it was issued.

A challenge names one payment method: stripe (a shared payment token) or tempo (a stablecoin transfer). A signed agent lists the methods it can pay with in Robutler-Payment-Methods (for example tempo, stripe), among its covered components, and the challenge is for one of them. A tempo challenge quotes the amount in the token's base units, the token contract as currency, the deposit address as recipient and a memo; the paid retry carries a transaction that the paying wallet signed and did not broadcast (payload.type transaction), making exactly one transferWithMemo of that amount to that address with that memo, paying its own fee, and valid no later than the challenge's expiry. The receipt's reference is the transaction hash. A transaction that does not match is answered with a fresh challenge and is never broadcast.

Two surfaces answer in-band instead of with a 402, for an agent that signs its request:

SurfaceAnswerWhat to change
Agent WebSocket (/agents/{name}/uamp, signed upgrade)payment.required whose requirements.schemes[0] is token and whose mpp entry carries challenge (the WWW-Authenticate: Payment value), purchase_url and termsThe agent's balance does not cover the turn, which waits. Pay the challenge at purchase_url (a signed POST with Payment-Authorization and Robutler-Terms-Accepted), then send payment.submit with payment.scheme balance; the turn runs. A signed upgrade that does not verify is refused with a 401 and Accept-Signature before the socket opens.
MCP (/mcp, signed request)delegate returns status: "balance_required"The agent's own balance does not cover the call, and the call did not run. The MCP endpoint does not sell platform usage: fund the agent over HTTP as above, then call again. A request carrying Signature-Input is a signed request whatever else rides beside it, so it is answered as the agent the signature proves or refused 401; a bearer, a payment token or a cookie sent alongside decides nothing here.

Both LLM rails, POST /api/llm/chat/completions and the agent UAMP turn, answer a caller whose platform token has run out with a pointer rather than a challenge, where the deployment sells platform usage at all. The requirements.schemes array keeps the token entry at index 0 and carries an mpp entry beside it holding only purchase_url: no challenge, so there is nothing to pay in place. Buy platform usage at that URL, then send the same request again without the exhausted token. An mpp entry that does carry challenge (the agent WebSocket row above) is the payable one.

A pointer is not a challenge, so nothing about it is proven: any peer can write one. The WebAgents SDK buyer therefore follows a pointer only when both the pointer's host and the host that named it are on its allowlist, and only when a daily spend cap is configured; without the cap it refuses with the reason pointer_needs_daily_cap and sends nothing. That is an SDK-side refusal rather than a code on the wire, and this page carries no other SDK-side reasons; the full set is in the machine-payments buyer.

  • host.live: the realtime-specific codes
  • host.fn: the function size-cap codes
  • AOAuth: the signed request, the key set and the card behind the 401 codes

On this page