Skip to main content

ContoError

SDK failures throw ContoError. 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

A TIMEOUT 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:
Persist the original 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, and 5xx
  • 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) or challengeId or credentialId (MPP). A retry with the same value returns the same live grant instead of reserving again
  • Respects Retry-After headers from the server, up to 30 seconds
  • Exponential backoff: 1s, 2s, 4s (capped at 10s)
  • Never automatically retries payment execution, protocol record calls, or ContoAdmin mutations
  • Does not retry client errors (4xx except 429) or auth failures
If a call is still rate limited after these retries, ContoError.retryAfter may carry a server-supplied delay in seconds. Otherwise, back off on your own schedule.