ContoError
SDK failures throwContoError. ContoAdmin throws ContoAdminError, which has the same code, status, requestId, retryAfter, and retryGuidance fields:
message is generic text for the HTTP status, not the server’s error text. Branch on code, show reasons to the user, and log requestId. For validation failures, fieldErrors names the fields to check. When a response carries no code, code falls back to a status value: INVALID_REQUEST, AUTH_FAILED, FORBIDDEN, NOT_FOUND, CONFLICT, RATE_LIMITED or SERVER_ERROR.
Error codes
Authentication errors
Payment errors
A repeated
execute() never sends a payment twice. If an approver executed the payment by
approving it in Conto, or an earlier call succeeded, execute() returns the existing receipt with
alreadyExecuted: true. While another execution is in flight it returns 409 PAYMENT_PROCESSING.
After a timeout, still read the original request before deciding on another write.
External wallet errors
payments.approve() and payments.confirm() add these codes.
Payment reasons
reasons on a payment, an approval, or a payment error uses this fixed set. External-wallet
approvals (/api/sdk/payments/approve) also return the matching reasonCodes.
Reasons never include rule values, policy names, or text the agent supplied. Your organization’s
dashboard shows the full evaluation.
Validation errors
System errors
Status
0 means the SDK raised the error before or without an HTTP response.
Handle errors
Decision and error boundaries
payments.request() returns policy outcomes as a status (APPROVED, DENIED,
REQUIRES_APPROVAL or PROCESSING), not as errors. It throws ContoError only for transport,
authentication, validation, rate limit and idempotency failures, 403 PAYMENT_DENIED when an
approved budget cannot cover the payment, and
503 COUNTERPARTY_REVIEW_UNAVAILABLE. pay() turns DENIED and REQUIRES_APPROVAL into errors.
execute() throws on every execution failure. Use separate request() and execute() calls to
keep the requestId and handle each status. See
Track and recover a payment.
Handle timeouts
ATIMEOUT from request() with a stable idempotencyKey is safe to retry. A TIMEOUT from
execute() means the payment may have been sent. Read its status before any other write:
Set CONTO_PAYMENT_REQUEST_ID to the original request ID saved before execution. Use the same
agent’s SDK key with transactions:read. This recovery example only reads that request:
requestId before execution so recovery can continue after a process restart.
If the status read also times out, retain that ID and retry the read later with bounded backoff.
A status response without a transaction does not by itself prove that another execution is safe;
do not create a new payment to resolve an uncertain outcome.
Never wrap pay() in a retry loop. Each pay() call without an idempotencyKey creates a new
payment request, so a retry after a lost response can pay twice. Retry only request(), with a
stable key you store with your job.
Retry strategy
The SDK retries transient failures only when repeating the request cannot create an additional side effect.- Retries read-only requests up to 3 times on network failures,
429, and5xx - Retries payment authorization requests with the same stable
idempotencyKey - Retries x402 and MPP pre-authorization. An approved pre-authorization issues a grant and
reserves spend, so pass a stable
paymentId(x402) orchallengeIdorcredentialId(MPP). A retry with the same value returns the same live grant instead of reserving again - Respects
Retry-Afterheaders from the server, up to 30 seconds - Exponential backoff: 1s, 2s, 4s (capped at 10s)
- Never automatically retries payment execution, protocol record calls, or
ContoAdminmutations - Does not retry client errors (
4xxexcept429) or auth failures
ContoError.retryAfter may carry a
server-supplied delay in seconds. Otherwise, back off on your own schedule.