---
title: "Tratamento de erros no SDK Python da NFE.io"
description: "A hierarquia de exceções do SDK Python, os atributos de cada erro, outcome_unknown e a receita de reconciliação por external_id que evita nota duplicada."
source_url: https://nfe.io/docs/desenvolvedores/bibliotecas/python/tratamento-de-erros/
product: bibliotecas
last_updated: 2026-10-14
tags: ["python", "referencia", "erro", "nfse", "emissao"]
integration: ["sdk"]
---

# Tratamento de erros

Todas as exceções do SDK derivam de `nfeio.NfeError` e podem ser importadas direto de `nfeio` (ou
de `nfeio.errors`). Nenhuma delas carrega a chave de API, em mensagem, atributo ou `repr`.

## Hierarquia

```text
NfeError
├── ConfigurationError            chave ausente, opção inválida, TLS inseguro (também ValueError)
├── InvalidParameterError         parâmetro recusado localmente, antes do HTTP (também ValueError)
├── APIError                      a API respondeu com status de erro
│   ├── InvalidRequestError       400, 405, 410, 415, 422 e demais 4xx
│   │   └── DuplicateExternalIdError   externalId repetido na emissão
│   ├── AuthenticationError       401
│   ├── PermissionDeniedError     403 (a mensagem diz qual chave a família usa)
│   ├── NotFoundError             404
│   ├── ConflictError             409
│   ├── RateLimitError            429 (retry_after)
│   └── ServerError               408 e 5xx
├── APIConnectionError            falha de rede (phase)
│   └── APITimeoutError           prazo de conexão, leitura ou total esgotado
├── ResponseTooLargeError         resposta acima de max_response_bytes
├── UnexpectedResponseError       resposta de sucesso que o SDK não consegue interpretar
├── InvoiceProcessingError        a nota terminou em IssueFailed, CancelFailed ou Error
├── PollingTimeoutError           o prazo de espera acabou antes de um estado final
└── SignatureVerificationError    assinatura de webhook inválida
```

## Atributos

| Exceção | Atributos |
|---|---|
| `APIError` e subclasses | `status_code`, `message`, `error_code`, `request_id`, `trace_id`, `body` (bytes), `json_body`, `headers`, `outcome_unknown`, `external_id` |
| `RateLimitError` | os de `APIError` + `retry_after` (segundos, ou `None`) |
| `APIConnectionError`, `APITimeoutError` | `message`, `phase` (`FailurePhase.NOT_ESTABLISHED` ou `MAYBE_SENT`), `outcome_unknown`, `external_id` |
| `InvalidParameterError` | `message`, `param` (nome do parâmetro recusado) |
| `InvoiceProcessingError` | `invoice`, `flow_status`, `flow_message` (a explicação da prefeitura) |
| `PollingTimeoutError` | `invoice` (a última observada, ou `None`), `flow_status`, `elapsed` |
| `UnexpectedResponseError` | `status_code`, `body`, `headers`, `request_id` |

`request_id` é o `x-request-id` da resposta: informe esse valor ao suporte. A mensagem já vem limpa
(sem caracteres de controle, até 1.000 caracteres), extraída dos formatos de erro que a API usa.

```python
from decimal import Decimal

from nfeio import APIError, InvalidRequestError, NfeClient

client = NfeClient()

try:
    client.service_invoices.create(
        "ID_DA_EMPRESA",
        {"cityServiceCode": "10677", "description": "Teste", "servicesAmount": Decimal("10.00")},
        external_id="pedido-3001",
    )
except InvalidRequestError as erro:
    # Validação da API: corrija o payload antes de tentar de novo.
    print(erro.status_code, erro.message, erro.request_id)
    print(erro.json_body)  # corpo decodificado, quando é JSON
except APIError as erro:
    print(type(erro).__name__, erro)  # status, código e request_id já no str()
```

## A regra: nunca reenviar uma emissão às cegas

A API pode **criar a nota e mesmo assim responder `500` ou `504`**, ou a conexão pode cair depois
que o pedido chegou. Reenviar nesse caso emite a nota duas vezes. Por isso o SDK:

- **nunca repete um `POST`** depois de timeout de leitura, conexão interrompida, `408`, `429` ou
  `5xx`. Só repete quando a falha aconteceu antes de conectar, porque aí nada saiu da máquina;
- marca esses erros com **`outcome_unknown=True`** e o `external_id` usado;
- devolve `DuplicateExternalIdError` quando a API recusa um `externalId` já usado. A primeira
  tentativa existe.

`outcome_unknown=True` quer dizer "não sabemos se a nota foi criada". Não é falha nem sucesso: é
um estado a reconciliar. `Idempotency-Key` não resolve, porque a API ignora esse cabeçalho hoje.

### Receita de reconciliação

Sempre envie `external_id` e, diante de resultado incerto, procure a nota antes de tentar de novo:

```python
from nfeio import (
    APIConnectionError,
    APIError,
    DuplicateExternalIdError,
    NfeClient,
    ServiceInvoice,
)
from nfeio.types import ServiceInvoiceCreateParams


class EmissaoNaoRegistrada(Exception):
    """A API não registrou a emissão. Tente de novo mais tarde, com o mesmo external_id."""


def emitir(
    client: NfeClient,
    company_id: str,
    external_id: str,
    params: ServiceInvoiceCreateParams,
) -> ServiceInvoice:
    try:
        return client.service_invoices.create(company_id, params, external_id=external_id)
    except DuplicateExternalIdError:
        pass  # uma tentativa anterior foi registrada: busque a nota
    except (APIError, APIConnectionError) as erro:
        if not erro.outcome_unknown:
            raise  # validação, chave inválida etc.: corrija antes de tentar de novo
    nota = client.service_invoices.find_by_external_id(company_id, external_id, wait=30)
    if nota is None:
        raise EmissaoNaoRegistrada(external_id)
    return nota
```

Como a receita funciona:

1. `DuplicateExternalIdError` vem antes de `APIError` porque é subclasse dele.
2. Erros com `outcome_unknown=False` (validação, `401`, `403`, `404`) não criaram nota. Corrija a
   causa antes de tentar de novo.
3. `find_by_external_id(..., wait=30)` repete a busca com backoff por até 30 segundos, cobrindo o
   atraso de indexação logo após o `202`. Devolve a nota ou `None`. Uma resposta que não seja a
   lista vazia explícita da API levanta `UnexpectedResponseError`: o SDK prefere falhar a concluir
   "pode emitir de novo" por engano.
4. Com `None`, a emissão não foi registrada. Tente de novo mais tarde **com o mesmo
   `external_id`**: se a primeira tentativa aparecer depois, a API recusa com
   `DuplicateExternalIdError` e a receita reconcilia, sem nota duplicada.

:::warning `external_id` é a sua garantia
Sem `external_id`, não há como reconciliar: a única informação sobre a nota criada é a que se
perdeu com a resposta. Use um valor estável e único por venda (o id do pedido, da fatura ou da
cobrança no seu sistema), nunca um UUID gerado a cada tentativa.
:::

:::note `429` em emissão
`RateLimitError` num `POST` também vem com `outcome_unknown=True`: não há garantia de que o
limitador recusou o pedido antes de processá-lo. Reconcilie antes de reenviar.
:::

## Erros de rede

`APIConnectionError.phase` diz se o pedido pode ter saído:

| `phase` | Significado | `outcome_unknown` num `POST` |
|---|---|---|
| `FailurePhase.NOT_ESTABLISHED` | DNS, conexão recusada ou handshake TLS: nada saiu da máquina | `False` (o SDK já repetiu) |
| `FailurePhase.MAYBE_SENT` | timeout de leitura, conexão interrompida: o pedido pode ter chegado | `True` |

Em `GET`, `PUT` e `DELETE`, que são seguros de repetir, o SDK tenta de novo sozinho até
`max_retries` antes de levantar o erro.

## Erros de espera

`wait`, `create_and_wait` e `cancel_and_wait` têm dois erros próprios:

- `InvoiceProcessingError`: a nota terminou em `IssueFailed`, `CancelFailed` ou `Error`. O motivo da
  prefeitura está em `flow_message`. A nota existe, com status de falha, e o `external_id` usado
  ficou registrado nela. Corrija a causa e emita uma nota nova, com outro `external_id`.
- `PollingTimeoutError`: o prazo acabou antes de um estado final. A nota existe e continua sendo
  processada. **Não emita de novo**: consulte depois com `retrieve` ou `wait`.

## Erros locais

`InvalidParameterError` e `ConfigurationError` são levantados antes de qualquer requisição, e também
são `ValueError`:

```python
from nfeio import ConfigurationError, InvalidParameterError, NfeClient, Timeout

try:
    Timeout(connect=10, read=60, total=0)
except ConfigurationError as erro:
    print(erro)  # timeout.total must be a finite number of seconds greater than zero

client = NfeClient(api_key="chave-de-exemplo", data_api_key="chave-de-dados-de-exemplo")
try:
    client.lookups.cnpj("11.222.333/0001-00")  # dígito verificador errado
except InvalidParameterError as erro:
    print(erro.param)  # cnpj
    print(erro)        # cnpj is not a valid CNPJ: '11.222.333/0001-00'
```

O SDK valida localmente ids, `external_id`, CNPJ (inclusive o alfanumérico), CPF, CEP, UF e datas.
As consultas podem ser cobradas, então um documento inválido nunca chega à API.

## Chave errada para o host

Um `403` quase sempre é a chave da família errada (a principal numa consulta de CEP, ou a de dados
na emissão). `PermissionDeniedError` diz qual parâmetro a família usa:

```text
permission denied for this API family (address); it uses data_api_key — check that the right key was configured
```

## Veja também

- [Configuração](./configuration.md#retry): a tabela de retry por método.
- [Síncrono, assíncrono e polling](./async-and-polling.md): os estados da nota.
- [Mensagens de erro da API de NFS-e](../../../documentacao/nota-fiscal-servico-eletronica/duvidas/erros-e-status/mensagens-de-erro-da-api-nfse.md).
