Errors and retries
Understand SDK and Merchant API errors, and retry without repeating paid work.
Keep every payment header exactly as recuut returned it; x402 payment instructions and retry explains each one. The tables below cover everything else that can fail.
SDK errors
| Error | Meaning | Typical response |
|---|---|---|
RecuutConfigurationError | An app setting is missing or wrong | Fix the setting. Do not retry the request. |
RecuutTransportError | The network failed before recuut replied | Retry only when it is safe to run the paid work again. |
RecuutResponseError | recuut returned an error response | Read code and message. |
RecuutUnexpectedResponseError | recuut replied outside the generated contract | Inspect the retained status, body, and headers; update the SDK before depending on new fields. |
PaymentRequiredError | Access requires payment | Return content and payment_required to the payer unchanged. |
PaymentRateLimitedError | The Merchant API rate-limited the request | Wait for retry_after, then retry within a bound. |
Import generic SDK errors from recuut. Import generated Merchant response errors and enums from recuut.merchant_api. For a known Merchant API error, read code and message.
Advanced error details
Raw content bytes and headers remain available when you need to return a payment request exactly or inspect logs. A future status, undocumented error code, or malformed payload raises RecuutUnexpectedResponseError with the original status, body, and headers.
import osfrom recuut import Recuutfrom recuut.merchant_api import ( AccessRequest, AccessResource, PaymentRequiredError,)def relay_challenge( *, body: bytes, payment_required: str) -> tuple[bytes, dict[str, str]]: return body, {"PAYMENT-REQUIRED": payment_required}prompt = "Summarize the quarterly report"input_tokens = len(prompt.split())try: with Recuut() as client: client.payments.access( access_request=AccessRequest( inputs={"input_tokens": str(input_tokens)}, revision_key=os.environ["RECUUT_REVISION_KEY"], resource=AccessResource( url="https://merchant.example/generate", description="Text generation API", mime_type="application/json", ), ) )except PaymentRequiredError as error: if error.payment_required is not None: response_body, response_headers = relay_challenge( body=error.content, payment_required=error.payment_required, ) # Return this body and these headers to the payer with HTTP 402. else: print(error.status_code, error.code, error.message)Retry discipline
| Response | Meaning | Safe action |
|---|---|---|
Access 402 | Payment is required | Relay the body and PAYMENT-REQUIRED; let the payer retry. |
Completion 202 | Settlement is pending | Keep the result private; honor Retry-After and use Get a transaction. |
400, 401, 403, 404, or 422 | Request, key, permission, ID, or contract error | Fix the input or configuration; do not retry unchanged. |
409 | Stored payment state conflicts with this request | Read the error code and transaction before deciding whether to resume. |
PaymentRateLimitedError (429) | Rate limited | Wait for error.retry_after, then retry within a bound. |
500, 502, or 503 | recuut or the payment provider failed temporarily | Retry only when protected side effects are idempotent. |
Do not retry every failure automatically. When recuut says payment is required, return its 402 response to the payer unchanged. If Retry-After is present, wait that long. For a temporary network or server error, retry a limited number of times with increasing waits.
Keep the paid result private until completion succeeds. A network interruption can cause a retry. Make important side effects safe to run more than once when possible. A successful transaction and receipt are the saved payment record. An app timeout alone does not prove payment failed.
Use the transaction's payment_request_id to match the original attempt in your logs.