> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reconifyhq.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Reconify's API reference is read-only for customer data. Do not invent endpoints or authentication behavior beyond the OpenAPI contract.
> The public OpenAPI document contains only the documented external /v2 contract.

# Python errors and retries

> Handle failed requests, rejected events, and automatic retries.

## Two kinds of failure

A batch can fail as a whole, or one event inside it can fail on its own. These
need different handling.

| Failure                                       | How you see it                                                  |
| --------------------------------------------- | --------------------------------------------------------------- |
| The request failed                            | The call raises a `ReconifyError` subclass.                     |
| The request succeeded, one event was rejected | The call returns, and a `results` item has `status` `rejected`. |

## Exception types

Every exception inherits from `ReconifyError`, so one `except ReconifyError`
catches all of them. Catch a subclass when the response needs a different
action.

| Exception                         | Raised for                                             |
| --------------------------------- | ------------------------------------------------------ |
| `ReconifyValidationError`         | Invalid arguments, rejected before any request is sent |
| `ReconifyRequestError`            | `400` and `422`                                        |
| `ReconifyAuthenticationError`     | `401`                                                  |
| `ReconifyPermissionError`         | `403`                                                  |
| `ReconifyNotFoundError`           | `404`                                                  |
| `ReconifyConflictError`           | `409`                                                  |
| `ReconifyRateLimitError`          | `429`                                                  |
| `ReconifyServiceUnavailableError` | `503`                                                  |
| `ReconifyServerError`             | Any other `5xx`                                        |

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from reconify import Reconify
from reconify.errors import (
    ReconifyError,
    ReconifyNotFoundError,
    ReconifyPermissionError,
)

with Reconify() as client:
    try:
        issue = client.issues.get_issue(
            "00000000-0000-7000-8000-000000000099"
        )
    except ReconifyNotFoundError:
        issue = None
    except ReconifyPermissionError as exc:
        raise RuntimeError(f"key lacks the required scope: {exc.code}") from exc
    except ReconifyError as exc:
        logger.error(
            "reconify request failed",
            extra={
                "status_code": exc.status_code,
                "code": exc.code,
                "request_id": exc.request_id,
            },
        )
        raise
```

Every exception carries `status_code`, `title`, `detail`, `code`,
`validation_errors`, `request_id`, `response_headers`, and `response_metadata`.

<Info>
  Exceptions never include your API key or the request body. Log `request_id` and
  include it in support requests.
</Info>

### Which status to act on

Each status implies one action:

| Status       | Do this                                                                              |
| ------------ | ------------------------------------------------------------------------------------ |
| `400`, `422` | Fix the request. Do not retry it unchanged.                                          |
| `401`        | Check the key value and that it is still active.                                     |
| `403`        | Use a key with the required scope. `write` for `POST` and `PATCH`, `read` for `GET`. |
| `404`        | Confirm the identifier belongs to your organization.                                 |
| `409`        | An `idempotency_key` was reused with a different note body.                          |
| `503`        | Retry with backoff. The SDK already does this for reads.                             |

## Rejected events inside a batch

The batch endpoint returns `202` when at least one event is accepted or
duplicated, and `422` when every event is rejected. Always read `results`.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
result = client.ingestion.ingest_monitoring_events(batch)

for item in result.results:
    if item.status == "rejected":
        logger.error(
            "event rejected",
            extra={
                "index": item.index,
                "code": item.code,
                "field": item.field,
                "message": item.message,
            },
        )
```

`item.index` matches the position in the list you sent. Use it to locate the
producer-side record without writing the event or customer reference to logs.

| `code`                 | Meaning                                                                    |
| ---------------------- | -------------------------------------------------------------------------- |
| `invalid_event`        | A field failed validation. Read `field`.                                   |
| `unknown_field`        | The event carried a field the contract does not define.                    |
| `idempotency_conflict` | The `id` was reused with different content. Investigate rather than retry. |
| `duplicate`            | Reconify already stored this event. Treat it as delivered.                 |
| `malformed_request`    | The request body could not be parsed.                                      |

## Automatic retries

Retry behavior is configured on the client:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from reconify import Reconify
from reconify.transport import RetryConfig

client = Reconify(
    retry=RetryConfig(
        max_retries=2,
        base_delay=0.25,
        max_delay=8.0,
        jitter=0.25,
        retry_unsafe_methods=False,
    )
)
```

| Setting                | Default | Meaning                                |
| ---------------------- | ------- | -------------------------------------- |
| `max_retries`          | `2`     | Retries after the first attempt        |
| `base_delay`           | `0.25`  | Seconds before the first retry         |
| `max_delay`            | `8.0`   | Ceiling for any single wait            |
| `jitter`               | `0.25`  | Random seconds added to spread retries |
| `retry_unsafe_methods` | `False` | Whether to retry `POST` and `PATCH`    |

Retries apply to `429` and `503` responses and to transport errors. A
`Retry-After` header overrides the computed backoff, capped at `max_delay`.

By default only `GET`, `HEAD`, and `OPTIONS` are retried, so event ingestion is
**not** retried automatically.

Turn on `retry_unsafe_methods` only when every event in the batch has a stable
`id`. Reconify then returns `duplicate` for events it already stored instead of
recording them twice.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
client = Reconify(retry=RetryConfig(max_retries=2, retry_unsafe_methods=True))
```

<Warning>
  An event without `id` receives a generated ID on every attempt. Retrying a batch
  that contains such events records them more than once.
</Warning>

## Next

Put this together in [Send events with Python](/sdks/python/send-events).
