# Errors and retries (/guides/operate/errors)

Keep every payment header exactly as recuut returned it; [x402 payment instructions and retry](/guides/concepts/x402-payment-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.

```python title="Relay a payment challenge"
import os

from recuut import Recuut
from 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](/api-reference/transactions/payments.transactions.get/). |
| `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`](/guides/operate/transactions/#pending-transactions) to match the original attempt in your logs.

[Inspect exact responses and schemas →](/api-reference/) · [OpenAPI JSON →](/openapi.json)
