---
title: "Errors"
description: "Every failure raises. Two classes, and a request id on every API error."
url: "https://openemail.uk/docs/python/errors"
area: "Python"
category: "Getting started"
---

# Errors

Every failure raises. Two classes, and a request id on every API error.

## Catching one

**catch_errors.py**

```
from openemail import OpenEmailApiError, OpenEmailNetworkError, openemail

try:
    openemail.emails.send({'from': sender, 'to': recipient, 'subject': subject, 'text': text})
except OpenEmailApiError as error:
    if error.is_validation:
        print(error.code, error.param, error.message)

    if error.is_permission:
        print(openemail.addresses.list())

    if error.is_rate_limited:
        print('try again in', error.retry_after_seconds, 'seconds')

    print(error.status, error.request_id)

    raise
except OpenEmailNetworkError as error:
    if error.is_timeout:
        print('no answer in time')

    raise
```

A `permission_error` on a send is usually the KEY’s send scope, a domain or an address it was not given, rather than the workspace, which is why the sample prints what `addresses.list()` says this key may send as.

## The classes

| Class | When |
| --- | --- |
| `OpenEmailApiError` | The API answered, and not with a success. Carries `message`, `status`, `type`, `code`, `param`, `doc_url`, `request_id`, `retry_after_seconds`, `fields` and `body`. |
| `OpenEmailNetworkError` | No response arrived: DNS, TLS, a dropped connection or the timeout. Carries `cause`, the `httpx` exception underneath, and `is_timeout` is `True` when the timeout was the reason. |
| `OpenEmailError` | The base of both, so one `except` catches every failure the API or the network caused. `WebhookVerificationError`, which `verify_webhook_signature` raises, inherits it too. |
| `ValueError` | Raised before anything is sent: a missing or malformed key, an unusable `base_url`, a credential bound for plain `http`, an empty id. A wrong `http_client`, or a body JSON cannot carry, raises `TypeError` instead. |

> `fields` lists each answer a sign-up form refused, as `key` and `error`, and is `None` on every other error. `body` keeps the JSON the API sent, or `None` when the body was not JSON.

| Property | True when |
| --- | --- |
| `is_auth` | `type` is `authentication_error`, a 401: no key, the wrong kind of credential, or a key we did not issue. |
| `is_permission` | `permission_error`, a 403: a real key without the scope or the From address it needs. |
| `is_scope_missing` | `code` is `insufficient_scope`, the 403 that names a missing scope. |
| `is_invalid_request` | `invalid_request_error`, a 400: a request that could not be understood. A message over the size ceiling comes back as a 422 `message_too_large`, so `is_validation` is the property that catches it. |
| `is_validation` | `validation_error`, a 422: the schema refused it, and `param` names the field. |
| `is_not_found` | `not_found_error`, a 404: no such resource. |
| `is_conflict` | `conflict_error`, a 409: the resource is past the point where this could be done to it. |
| `is_rate_limited` | `rate_limit_error`, a 429. `retry_after_seconds` holds the wait when the server named one. |
| `is_server_error` | `status` is 500 or above. Quote `request_id` if you contact support. |
| `is_retryable` | `status` is 408, 429, 500, 502, 503 or 504. |
| `is_step_up_required` | `code` is `step_up_required`, the 403 an OAuth access token gets before a sensitive change until the person has verified a code. |

Most properties read `type`, the frozen half of the envelope. `code` stays a `str`, because the API guarantees it is open and additive, so treat one you do not recognise as its `type`. A closed `Literal` would make an SDK upgrade the price of reading a new failure mode.

> A body that is not the API’s error envelope still becomes an `OpenEmailApiError`, with `type` inferred from the status and `code` set to `unrecognised_response`. A success whose body is not JSON raises one too.

> Cancelling an `AsyncOpenEmail` call raises no `OpenEmailError`. The cancellation itself propagates, whether it lands during the request or during the wait before a retry, and nothing is retried after it.

## request_id

Every `OpenEmailApiError` carries the request id the server sent, from the error body or the `x-request-id` header, and it is the only thing tying your failure to a line in the server's log. A success returns the parsed body alone, so there is no request id to read on one.

`str(error)` ends with the status, the code and the request id, so a log line that prints the exception keeps all three. An `OpenEmailApiError` also survives pickling with every field, so one raised in a worker process reaches the parent intact.
