# API de NF-e para ERPs: comece aqui

Esta API emite **NF-e (modelo 55)** em nome de uma ou várias empresas (CNPJs) e devolve a nota ao seu sistema. Foi desenhada para quem já integrou com serviços como o Focus NFe: você manda os dados da venda em JSON, a plataforma calcula os tributos, assina, envia à SEFAZ e avisa o resultado.

- **Base da API:** `https://srv1839254.hstgr.cloud/api/v2` (troque pelo endereço que você recebeu)
- **Formato:** JSON em UTF-8, datas em ISO 8601, valores em reais (número, ponto decimal)
- **Autenticação:** uma chave de API por conta, em `Authorization: Bearer nfe_...`
- **Contrato OpenAPI 3.1:** [`/api/v2/openapi.json`](/api/v2/openapi.json), público, para importar no Postman, Insomnia ou num gerador de cliente
- **Tudo que fala com a SEFAZ é assíncrono:** você recebe `202` e acompanha por consulta ou por **webhook**

## Em 5 minutos

Os exemplos usam `curl`. Defina a chave e a base uma vez:

```bash
export CHAVE="nfe_SUA_CHAVE_AQUI"
export API="https://srv1839254.hstgr.cloud/api/v2"
```

**1. Confirme que a chave funciona** (deve listar as empresas da sua conta, ou uma lista vazia):

```bash
curl -s "$API/empresas" -H "Authorization: Bearer $CHAVE"
```

**2. Cadastre a empresa que vai emitir** (ela nasce em *homologação*, sem valor fiscal):

```bash
curl -s -X POST "$API/empresas" -H "Authorization: Bearer $CHAVE" -H "Content-Type: application/json" -d '{
  "cnpj": "11.222.333/0001-81", "razao_social": "LOJA EXEMPLO LTDA", "ie": "111111111111", "regime": "simples",
  "uf": "SP", "municipio": "Sao Paulo", "logradouro": "Rua das Flores", "numero": "100",
  "bairro": "Centro", "cep": "01001-000", "email": "fiscal@loja.exemplo.com.br"
}'
```

**3. Envie o certificado A1 da empresa** ([detalhes](03-empresas.md#certificado-digital-a1)):

```bash
curl -s -X PUT "$API/empresas/11222333000181/certificado" -H "Authorization: Bearer $CHAVE" -H "Content-Type: application/json" \
  -d "{\"arquivo_certificado_base64\": \"$(base64 -w0 certificado.pfx)\", \"senha_certificado\": \"SENHA_DO_CERTIFICADO\"}"
```

**4. Emita uma nota de teste.** A `ref` é o identificador **seu** da venda (use o número do pedido): ela garante que repetir a chamada nunca emite duas notas.

```bash
curl -s -X POST "$API/nfe?ref=PEDIDO-1001" -H "Authorization: Bearer $CHAVE" -H "Content-Type: application/json" -d '{
  "nome_destinatario": "Maria da Silva", "cpf_destinatario": "529.982.247-25",
  "logradouro_destinatario": "Av Atlantica", "numero_destinatario": "500", "bairro_destinatario": "Copacabana",
  "municipio_destinatario": "Rio de Janeiro", "codigo_municipio_destinatario": "3304557", "uf_destinatario": "RJ", "cep_destinatario": "22010-000",
  "items": [{ "codigo_produto": "CAMISA-P", "descricao": "Camiseta basica P", "codigo_ncm": "61091000", "icms_origem": 0,
              "quantidade_comercial": 1, "valor_unitario_comercial": 100.00 }],
  "formas_pagamento": [{ "forma_pagamento": "17" }]
}'
```

A resposta é `202` com `"status": "processando_autorizacao"`.

**5. Acompanhe até o estado final** (ou receba por [webhook](08-webhooks.md)):

```bash
curl -s "$API/nfe/PEDIDO-1001" -H "Authorization: Bearer $CHAVE"
```

Quando o `status` for `autorizado`, os campos `caminho_xml_nota_fiscal` e `caminho_danfe` trazem os links do XML e do PDF. Pronto: a nota de teste foi emitida na SEFAZ de homologação.

## Onde está cada assunto

| Página | Para quê |
|---|---|
| [Conceitos](01-conceitos.md) | conta, empresa, ambiente, `ref`, estados da nota, o que é síncrono e o que não é |
| [Autenticação e limites](02-autenticacao.md) | chave de API, formato de erro, limites por minuto, `X-Request-Id`, paginação |
| [Empresas e certificado](03-empresas.md) | cadastrar CNPJs, enviar o certificado A1, trocar para produção |
| [Produtos](04-produtos.md) | catálogo SKU → NCM/origem, que completa os itens da nota |
| [Emitir NF-e](05-emitir-nfe.md) | todos os campos, reenvio da `ref` e exemplos prontos |
| [Consultar e eventos](06-eventos.md) | consulta, XML/DANFE, cancelamento, carta de correção, inutilização |
| [Modo híbrido de tributação](07-modo-hibrido.md) | informar CFOP, situação tributária e alíquotas por item; o que a plataforma preenche |
| [Webhooks](08-webhooks.md) | avisos assinados de cada mudança de estado |
| [Erros](09-erros.md) | todos os códigos, o que fazer em cada um e as rejeições comuns da SEFAZ |
| [Ir para produção](10-producao.md) | checklist de entrada em produção e boas práticas |
| [Exemplos de código](11-exemplos-de-codigo.md) | integração completa em PHP, Node.js e Python |
| [Referência de endpoints](12-referencia.md) | lista de todas as rotas |
| [Novidades](13-novidades.md) | versão e política de compatibilidade |

> **Importante:** a nota de **produção** tem valor fiscal. Faça todos os testes em homologação e siga o [checklist](10-producao.md) antes de trocar o ambiente.

---

# Conceitos

## Conta, chave de API e empresas

| Conceito | O que é |
|---|---|
| **Conta** | o seu sistema (o ERP) como cliente da API. Uma conta tem chaves de API e empresas |
| **Chave de API** | credencial `nfe_...` da conta. Pode haver várias (uma por ambiente, por sistema...) e cada uma pode ser revogada sozinha |
| **Empresa** | um CNPJ emitente, com regime tributário, endereço, inscrição estadual e **certificado digital A1** próprios. Uma conta pode ter várias |

Uma chave só enxerga as empresas da **própria** conta. Se o seu ERP atende várias lojas (ou vários CNPJs), cadastre cada CNPJ como uma empresa e diga qual emite em cada chamada com `cnpj_emitente`. Quem tem **uma única empresa** pode omitir o campo.

## Ambiente: homologação e produção

Cada empresa está em um de dois ambientes:

- **Homologação**: a SEFAZ de teste. As notas **não têm valor fiscal**, têm numeração própria e levam o texto obrigatório de homologação. É onde toda empresa nasce.
- **Produção**: notas reais, com valor jurídico. A troca é por `PUT /empresas/{cnpj}/ambiente` e só libera com certificado válido, inscrição estadual e as tabelas fiscais da plataforma validadas ([detalhes](03-empresas.md#trocar-de-ambiente)).

As notas ficam presas ao ambiente em que foram emitidas. Ao passar para produção você pode **reaproveitar as mesmas `ref`**: na produção elas começam do zero. As consultas olham sempre o ambiente atual da empresa.

## A `ref`: identificador da nota e proteção contra duplicidade

Toda emissão leva uma `ref` na URL (`POST /nfe?ref=PEDIDO-1001`). É um texto **seu**, de 1 a 60 caracteres (letras, números, ponto, hífen e sublinhado), único por empresa e ambiente. Use o identificador da venda no seu sistema.

A `ref` faz três coisas:

1. **Idempotência.** Se a rede cair e você repetir o `POST` com a mesma `ref` e o mesmo conteúdo, recebe o estado da nota que já existe, **sem emitir outra**. É seguro repetir à vontade.
2. **Consulta.** Você acompanha e opera a nota por ela: `GET /nfe/{ref}`, `DELETE /nfe/{ref}`...
3. **Correção.** Uma nota rejeitada pela SEFAZ aceita o reenvio **corrigido na mesma `ref`**, com o mesmo número fiscal.

Detalhes das regras de reenvio em [Emitir NF-e](05-emitir-nfe.md#reenvio-de-uma-ref).

## Estados da nota

O campo `status` da nota é um destes:

| `status` | Significa | É final? |
|---|---|---|
| `processando_autorizacao` | aceita pela API e na fila, ou enviada e aguardando a SEFAZ | não |
| `autorizado` | a SEFAZ autorizou: a nota existe e vale | sim (aceita eventos) |
| `erro_autorizacao` | a SEFAZ **rejeitou**. `status_sefaz`, `mensagem_sefaz` e `mensagem` dizem por quê. Corrija e reenvie na mesma `ref` | não (reenviável) |
| `erro_validacao` | parou por uma pendência de cálculo ou cadastro, antes de chegar à SEFAZ. `mensagem` diz o que corrigir. Reenvie na mesma `ref` | não (reenviável) |
| `denegado` | a SEFAZ **denegou o uso** (problema fiscal do emitente ou do destinatário): o número foi consumido e a nota não vale | sim |
| `cancelado` | o cancelamento foi registrado | sim |
| `inutilizado` | o número da nota foi inutilizado: ela nunca existiu na SEFAZ | sim |

```text
                 ┌─────────────── erro_autorizacao ◄──────┐
                 │               erro_validacao           │ (reenvio corrigido, mesma ref)
 POST /nfe ─► processando_autorizacao ─► autorizado ─► cancelado
                 │                                   (DELETE /nfe/{ref})
                 └─────────────► denegado
```

## O que é síncrono e o que não é

A API valida **na hora** tudo o que dá para validar sem a SEFAZ e responde `422` antes de gastar um número da série: formato, campos que faltam, tributos que o motor não consegue calcular, totais que não fecham, certificado vencido, produção bloqueada. Quando o `POST /nfe` responde `202`, a nota foi aceita e **já tem número reservado**.

Daí em diante tudo o que depende da SEFAZ é **assíncrono**: emitir, cancelar, corrigir e inutilizar respondem `202` e o resultado chega depois. Você escolhe como saber dele:

- **Webhook** (recomendado): a plataforma chama o seu endereço a cada mudança de estado. Veja [Webhooks](08-webhooks.md).
- **Consulta (polling):** `GET /nfe/{ref}` até o `status` ficar final. Comece a cada 2 segundos e aumente o intervalo.
- **Os dois juntos:** webhook para reagir rápido e uma consulta periódica das notas pendentes como rede de segurança. É o que recomendamos em produção.

## O que a plataforma calcula por você

Você informa a venda (destinatário, itens, valores, pagamento). A plataforma:

- calcula **ICMS, DIFAL, FCP, PIS, COFINS, IPI e IBS/CBS** conforme o regime da empresa e a operação, e define o CFOP;
- monta o XML no leiaute oficial, **assina** com o certificado da empresa e valida contra o XSD;
- reserva o número da NF-e em sequência, **sem buracos nem duplicidade**, mesmo com várias lojas emitindo ao mesmo tempo;
- envia, trata as respostas da SEFAZ (inclusive timeouts: antes de reenviar ela **consulta a chave**, para nunca emitir uma nota duplicada) e gera o DANFE.

Se o seu ERP **já calcula** os tributos, você pode informá-los por item (CFOP, situação tributária, alíquotas) e a plataforma passa a usar o que você mandou: é o [modo híbrido](07-modo-hibrido.md). Os *valores* de imposto (base de cálculo, valor do ICMS...) são sempre calculados pela API.

> **Responsabilidade fiscal:** os parâmetros que você informa valem mais que as regras da plataforma, e a responsabilidade por eles é sua. Cenários que o motor ainda não cobre (por exemplo ICMS-ST como substituto) são recusados com `422 validacao_fiscal`, em vez de a plataforma "chutar".

## Vem do Focus NFe?

A API segue o mesmo estilo (chave por HTTP Basic ou Bearer, `ref` na query, `items[]`, nomes como `nome_destinatario` e `codigo_ncm`, `cnpj_emitente`, `DELETE /nfe/{ref}` com `justificativa`, `POST /nfe/inutilizacao`), então boa parte de um cliente já escrito para ele pode ser reaproveitada. Mas **não é um clone**: confira estas diferenças antes.

| Assunto | Aqui |
|---|---|
| **Cancelar e inutilizar** | **assíncronos**: respondem `202` e o estado muda depois (`GET /nfe/{ref}` ou webhook). Não esperam a SEFAZ |
| **Cancelar** | exige `"confirmo": true` (afirma que a mercadoria **não** saiu) |
| **Campo desconhecido** | **recusado** com `422` e o caminho do campo. Nunca ignorado em silêncio |
| **Valores de imposto** (`icms_valor`, `pis_valor`...) | **recusados**: a API calcula os valores a partir dos parâmetros que você informa ([modo híbrido](07-modo-hibrido.md)) |
| **`valor_total`** | opcional, mas se vier a nota só sai se o total calculado fechar com ele |
| **Campos centrais** | conferem com o estilo do Focus. Os demais (pagamentos, intermediador, parâmetros fiscais além da situação tributária) são desta API, no mesmo estilo: use [Emitir NF-e](05-emitir-nfe.md) como contrato, não a documentação de outro serviço |
| **Estados e códigos** | `status` e `codigo` de erro são os desta documentação ([estados](#estados-da-nota), [erros](09-erros.md)) |

---

# Autenticação, erros e limites

## Chave de API

Toda chamada (menos `GET /openapi.json`) leva a chave da conta em um destes formatos:

```http
Authorization: Bearer nfe_SUA_CHAVE_AQUI
```

ou HTTP Basic, com a chave como **usuário** e a senha **vazia** (o mesmo formato do Focus NFe, para quem já tem o cliente HTTP configurado assim):

```bash
curl -u "nfe_SUA_CHAVE_AQUI:" https://srv1839254.hstgr.cloud/api/v2/empresas
```

Envie também `Accept: application/json` e, nos `POST`/`PUT`/`PATCH`/`DELETE` com corpo, `Content-Type: application/json`.

- A chave é entregue **uma única vez**, na criação. A plataforma guarda só um hash dela: se você perder, peça uma nova e revogue a antiga.
- Uma chave pode ter **validade**. Chave revogada, vencida ou inexistente responde sempre o mesmo `401`, para não ensinar nada a quem está adivinhando.
- Conta desativada responde `403 conta_inativa`.
- **Rotação sem parada:** peça uma chave nova, coloque-a no seu sistema e só então revogue a antiga. Várias chaves valem ao mesmo tempo.
- Trate a chave como senha: não a coloque em código-fonte, front-end ou repositório.

## Formato dos erros

Qualquer erro tem o mesmo corpo:

```json
{ "codigo": "requisicao_invalida", "mensagem": "O campo nome destinatario é obrigatório.", "erros": { "nome_destinatario": ["O campo nome destinatario é obrigatório."] } }
```

- `codigo`: estável. **Programe contra ele**, não contra a mensagem.
- `mensagem`: para uma pessoa ler (em português; pode mudar de redação).
- `erros` (só na validação): todos os problemas **de uma vez**, por campo, com o caminho (`items.0.codigo_ncm`, `formas_pagamento.1.valor_pagamento`).

Veja todos os códigos e o que fazer em cada um em [Erros](09-erros.md).

| HTTP | Quando |
|---|---|
| `200` | sucesso; ou reenvio de uma nota que já existe com o mesmo conteúdo, em estado final |
| `201` | criado (empresa, webhook, produto novo) |
| `202` | aceito; o resultado depende da SEFAZ e vem depois |
| `204` | removido |
| `401` | chave ausente, inválida, revogada ou vencida |
| `403` | conta desativada |
| `404` | não existe (ou não é da sua conta) |
| `409` | conflito: a `ref` já pertence a uma nota com outro conteúdo |
| `422` | a requisição é inválida ou o cálculo fiscal recusou |
| `429` | limite de requisições por minuto estourado |
| `5xx` | falha da plataforma: tente de novo (com a mesma `ref`, é seguro) |

## Limites por minuto

Cada **chave** tem o seu balde: uma chave que dispara chamadas demais não gasta a cota das outras.

| Balde | Padrão | O que conta |
|---|---|---|
| geral | 300 por minuto | toda chamada autenticada |
| emissão | 120 por minuto | `POST /nfe` |
| sensível | 10 por minuto | `POST /empresas` e `PUT /empresas/{cnpj}/certificado` |

Os padrões podem ser ajustados por conta. Além disso há um limite **por IP**, antes de a chave ser conferida (1200 por minuto), contra quem fica adivinhando chaves.

Toda resposta autenticada traz `X-RateLimit-Limit` e `X-RateLimit-Remaining`. Ao estourar:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 51
X-RateLimit-Limit: 300

{ "codigo": "limite_de_requisicoes", "mensagem": "Muitas requisições. Tente de novo em 51 segundo(s)." }
```

Espere `Retry-After` segundos e repita. Não faça laço apertado de tentativas: veja o [`backoff` recomendado](10-producao.md#tentativas-e-erros-de-rede).

## `X-Request-Id` e o log de requisições

Toda resposta autenticada traz `X-Request-Id`. **Guarde-o nos seus logs** e cite-o ao pedir suporte: ele identifica a chamada exata do nosso lado.

`GET /requisicoes` mostra o log da sua conta (todas as chaves), das mais recentes para as mais antigas:

```bash
curl -s "https://srv1839254.hstgr.cloud/api/v2/requisicoes?status=4xx&metodo=POST&per_page=20" -H "Authorization: Bearer $CHAVE"
```

Filtros: `status` (código exato como `404`, ou a classe `4xx`/`5xx`), `metodo`, `caminho` (contém), `desde` (data/hora ISO 8601; codifique o `+` do fuso como `%2B`), `per_page` (até 100). Cada item tem `id` (o mesmo `X-Request-Id`), `metodo`, `caminho`, `status`, `duracao_ms`, `chave` (prefixo da chave usada), `ip`, `user_agent` e `criada_em`.

O log guarda **só metadados**: nunca o corpo, a query string nem os cabeçalhos (neles há certificado, senha e dados do destinatário). Fica por 30 dias.

## Paginação

Listas grandes (`/produtos`, `/webhooks/{id}/entregas`, `/requisicoes`) são paginadas: `?per_page=25` (máximo 100) e `?page=2`. A resposta traz:

```json
{ "data": [ ... ], "links": { "first": "...?page=1", "last": "...?page=14", "prev": null, "next": "...?page=2" },
  "meta": { "current_page": 1, "last_page": 14, "per_page": 2, "total": 27 } }
```

Siga `links.next` até ele ser `null`. As listas curtas (`/empresas`, `/webhooks`, `/nfe/inutilizacoes`) vêm inteiras em `data`, sem paginação.

## Convenções de dados

- **Valores monetários:** reais, como número JSON com ponto decimal (`100.00`, `0.3333` no preço unitário). Totais aceitam até 2 casas; preço unitário até 10; quantidade até 4. Nas respostas, `valor_total` é um número em reais.
- **Datas e horas:** ISO 8601 com fuso (`2026-10-04T10:00:00-03:00`).
- **Documentos:** CPF, CNPJ, CEP e telefone aceitam máscara (`529.982.247-25`): a API remove. O CNPJ pode ser **alfanumérico** (formato vigente desde julho de 2026).
- **Códigos com zero à esquerda** (`codigo_ncm`, `forma_pagamento` `01`, CST `00`) podem ser enviados como texto ou como número: `4011000` vira `04011000`.
- **Campos desconhecidos são recusados** com `422` e o caminho do campo. Digitação errada nunca vira dado ignorado em silêncio. As exceções (campos aceitos e ignorados de propósito) estão em [Emitir NF-e](05-emitir-nfe.md#campos-aceitos-e-ignorados).
- **Identificadores:** a empresa é identificada pelo CNPJ na URL e pelo `id` (uuid) nas respostas; o produto pelo SKU; a nota pela `ref`. Os ids numéricos internos não aparecem.

## Compatibilidade

A versão está na URL (`/api/v2`). Dentro da `v2` só adicionamos: campos novos em respostas, endpoints novos, valores novos de enumerações (`status`, `evento`...). **Seu cliente deve ignorar campos que não conhece** e tratar um `status` desconhecido como "não final". Mudanças que quebram contrato só entram em uma versão nova, com aviso prévio. Veja [Novidades](13-novidades.md).

---

# Empresas e certificado

Cada CNPJ que vai emitir é uma **empresa** da sua conta. Você cadastra, atualiza, envia o certificado digital e liga a produção pela API, sem depender de ninguém.

| Método | Rota | Para quê |
|---|---|---|
| `GET` | `/empresas` | listar as empresas da conta |
| `POST` | `/empresas` | cadastrar (nasce em homologação) |
| `GET` | `/empresas/{cnpj}` | detalhar |
| `PATCH` | `/empresas/{cnpj}` | atualizar (parcial) |
| `GET` | `/empresas/{cnpj}/certificado` | situação do certificado |
| `PUT` | `/empresas/{cnpj}/certificado` | enviar o certificado A1 |
| `DELETE` | `/empresas/{cnpj}/certificado` | remover o certificado |
| `PUT` | `/empresas/{cnpj}/ambiente` | trocar homologação ↔ produção |

O `{cnpj}` da URL aceita só os dígitos (ou o formato alfanumérico); pontos, barra e traço são ignorados. O CNPJ de outra conta e um CNPJ que não existe recebem a **mesma** resposta `404 empresa_nao_encontrada`: a API nunca revela quem é cliente da plataforma.

## Cadastrar uma empresa

`POST /empresas`

<!-- exemplo:empresa -->
```json
{
  "cnpj": "11.222.333/0001-81",
  "razao_social": "LOJA EXEMPLO LTDA",
  "nome_fantasia": "Loja Exemplo",
  "ie": "111.111.111.111",
  "regime": "simples",
  "uf": "SP",
  "municipio": "Sao Paulo",
  "logradouro": "Rua das Flores",
  "numero": "100",
  "bairro": "Centro",
  "cep": "01001-000",
  "email": "fiscal@loja.exemplo.com.br",
  "telefone": "(11) 99999-8888"
}
```

Resposta `201`:

```json
{
  "id": "d6a1174d-11f5-487e-9583-d846f870c54d",
  "razao_social": "LOJA EXEMPLO LTDA",
  "nome_fantasia": "Loja Exemplo",
  "cnpj": "11222333000181",
  "ie": "111111111111",
  "im": null,
  "cnae": null,
  "regime": "simples",
  "industrial": false,
  "p_cred_sn": null,
  "endereco": {
    "uf": "SP", "codigo_municipio": "3550308", "municipio": "Sao Paulo", "logradouro": "Rua das Flores",
    "numero": "100", "complemento": null, "bairro": "Centro", "cep": "01001000"
  },
  "telefone": "11999998888",
  "email": "fiscal@loja.exemplo.com.br",
  "ambiente": "homologacao",
  "certificado": { "instalado": false, "valido_ate": null, "dias_restantes": null, "vencido": false },
  "bloqueios_producao": [
    "Envie um certificado A1 válido (e-CNPJ).",
    "As tabelas fiscais ainda não foram validadas por um contador (FISCAL_RATES_VERIFIED)."
  ],
  "ativo": true,
  "criado_em": "2026-10-04T15:50:05+00:00"
}
```

### Campos do cadastro

Campo desconhecido é recusado. Obrigatórios no `POST`: `cnpj`, `razao_social`, `regime`, `uf`, `municipio`, `logradouro`, `numero`, `bairro`, `cep`, `email`.

| Campo | Regra |
|---|---|
| `cnpj` | **só no `POST`**; com dígito verificador, numérico ou alfanumérico; único na plataforma |
| `razao_social` | até 60 caracteres (limite da NF-e) |
| `nome_fantasia` | até 60 |
| `ie` | inscrição estadual, 2 a 14 dígitos. Sem IE habilitada na SEFAZ não há NF-e |
| `im`, `cnae` | inscrição municipal (até 15 dígitos); CNAE com 7 dígitos |
| `regime` | `mei`, `simples`, `simples_excesso`, `presumido` ou `real`. **Informe o verdadeiro**: ele decide como o ICMS é calculado (CSOSN no Simples/MEI, CST nos demais) |
| `industrial` | `true` se for estabelecimento industrial (passa a exigir IPI) |
| `p_cred_sn` | percentual de crédito de ICMS do Simples (CSOSN 101), 0 a 100. **Só** para Simples/MEI |
| `uf` | sigla da UF |
| `codigo_municipio` | código IBGE do município (7 dígitos). Se faltar, a API descobre pelo CEP; se não conseguir, pede que você informe |
| `municipio`, `logradouro`, `numero`, `complemento`, `bairro` | endereço do emitente (até 60 caracteres cada) |
| `cep` | 8 dígitos, com ou sem máscara |
| `telefone`, `email` | contato do emitente |

**Limite de empresas:** cada conta tem um máximo de empresas (padrão 50, ajustável). Passou dele: `422 limite_de_empresas`. Criar empresa e enviar certificado são **operações sensíveis**: no máximo 10 chamadas por minuto por chave.

**CNPJ já cadastrado** na plataforma devolve `422` com erro no campo `cnpj` (a mesma mensagem, seja o CNPJ da sua conta ou de outra).

## Atualizar uma empresa

`PATCH /empresas/{cnpj}`: atualização **parcial**. O que você não enviar fica como está, e o **resultado inteiro** é validado. O CNPJ não muda (o certificado e as notas estão presos a ele).

<!-- exemplo:atualizar-empresa -->
```json
{ "nome_fantasia": "Loja Exemplo Centro", "regime": "presumido" }
```

Se você trocar o `cep` sem informar o município, o código IBGE antigo deixa de valer e a API o descobre de novo. Mande `"ativo": false` para **desativar a emissão** da empresa (as notas e o cadastro continuam); `"ativo": true` reativa.

## Certificado digital A1

Para emitir, a SEFAZ exige o **certificado A1** (arquivo `.pfx` ou `.p12`) **e-CNPJ ICP-Brasil** da empresa, dentro da validade. Certificado de pessoa física (e-CPF), A3 (token ou cartão) e autoassinado **não servem**.

`PUT /empresas/{cnpj}/certificado` aceita dois formatos.

**JSON, com o arquivo em base64:**

```bash
curl -s -X PUT "$API/empresas/11222333000181/certificado" -H "Authorization: Bearer $CHAVE" -H "Content-Type: application/json" \
  -d "{\"arquivo_certificado_base64\": \"$(base64 -w0 certificado.pfx)\", \"senha_certificado\": \"SENHA\"}"
```

**Multipart, com o arquivo:**

```bash
curl -s -X PUT "$API/empresas/11222333000181/certificado" -H "Authorization: Bearer $CHAVE" \
  -F "certificado=@certificado.pfx" -F "senha_certificado=SENHA"
```

A API confere que o arquivo abre com a senha, que não está vencido e que o CNPJ do certificado é o da empresa (mesma raiz de 8 dígitos: matriz e filiais compartilham o certificado do grupo). Resposta `200`:

```json
{ "empresa": "LOJA EXEMPLO LTDA:11222333000181", "cnpj": "11222333000181", "valido_ate": "2027-10-04", "dias_restantes": 364 }
```

- Arquivo de até **200 KB**.
- A mensagem de erro para arquivo inválido e senha errada é **a mesma** de propósito (não ajudar quem está sondando): confira o arquivo e a senha.
- O arquivo e a senha ficam **criptografados** e **nunca** voltam em nenhuma resposta nem entram no log.
- `GET /empresas/{cnpj}/certificado` devolve só a situação: `{ "instalado": true, "valido_ate": "2027-10-04", "dias_restantes": 364, "vencido": false }`. **Monitore `dias_restantes`**: certificado vencido bloqueia toda a emissão da empresa.
- `DELETE /empresas/{cnpj}/certificado` remove o certificado; sem ele não há emissão e a empresa **volta para homologação**.

### Cadastrar já com o certificado (tudo ou nada)

O `POST /empresas` aceita `arquivo_certificado_base64` e `senha_certificado` junto dos dados: se o certificado for recusado, **a empresa também não é criada**. É o jeito mais simples de fazer o *onboarding* de um cliente em uma só chamada.

## Trocar de ambiente

`PUT /empresas/{cnpj}/ambiente` com `{ "ambiente": "producao" }` ou `{ "ambiente": "homologacao" }`.

Produção só libera quando **nada impede**. O que falta vem em `erros.ambiente` (e, a qualquer momento, em `bloqueios_producao` da empresa):

```json
{ "codigo": "requisicao_invalida", "mensagem": "As tabelas fiscais ainda não foram validadas por um contador (FISCAL_RATES_VERIFIED).",
  "erros": { "ambiente": ["As tabelas fiscais ainda não foram validadas por um contador (FISCAL_RATES_VERIFIED)."] } }
```

Os bloqueios possíveis são: certificado A1 válido ausente ou vencido, inscrição estadual não informada e as tabelas fiscais da plataforma ainda não validadas. Antes de trocar, siga o [checklist de produção](10-producao.md).

## Numeração da NF-e (séries e próximo número)

Cada empresa numera as notas em sequência, por **série** e por **ambiente** (homologação e produção têm sequências próprias). Sem configurar nada, a nota sai na **série 1** a partir do **número 1**.

Quem já emitia por outro sistema (Bling, Tiny...) precisa **continuar de onde parou**: a SEFAZ recusa número repetido na mesma série. Antes da primeira nota em produção, informe o próximo número:

```bash
curl -s -X PUT "$API/empresas/11222333000181/series/1" -H "Authorization: Bearer $CHAVE" -H "Content-Type: application/json" \
  -d '{ "proximo_numero": 1500 }'
```

Resposta `200`:

```json
{ "cnpj": "11222333000181", "ambiente": "producao", "serie": 1, "ultimo_numero": 1499, "proximo_numero": 1500 }
```

- A série vai de `0` a `889` (890 em diante são do fisco e da contingência). Série que não existe é criada na hora.
- A numeração **só anda para a frente**: número menor ou igual ao maior já usado na série responde `422` `numero_retroativo`, com o `minimo` aceito. Número já usado nunca é reaproveitado.
- O ajuste vale para o **ambiente atual** da empresa. Configure a numeração de produção depois de [trocar de ambiente](#trocar-de-ambiente).
- Pular números sem nota (por exemplo, ir de 10 para 1500 sem ter emitido 11 a 1499 em outro sistema) obriga a [inutilizar](06-eventos.md#inutilizar-numeracao) a faixa pulada.

`GET /empresas/{cnpj}/series` lista as séries do ambiente atual com `ultimo_numero` e `proximo_numero` (sem nenhuma nota, a série 1 aparece com próximo número 1). Para emitir em outra série, mande `serie` no corpo da nota ([Emitir NF-e](05-emitir-nfe.md#nota)).

## Resposta da empresa

Campos que você vai usar mais: `ambiente` (`homologacao` ou `producao`), `certificado.dias_restantes` e `certificado.vencido`, `bloqueios_producao` (lista do que impede a produção; vazia quando pode ligar) e `ativo`.

---

# Produtos (catálogo da empresa)

Cada empresa tem um **catálogo** de produtos: SKU → NCM, origem, GTIN e unidade. O catálogo é **opcional**, mas poupa trabalho: na emissão, o item que não informa `codigo_ncm`, `icms_origem` (e os demais dados do produto) usa o produto do catálogo com o mesmo `codigo_produto`. O que o item informa **sempre vence** o catálogo.

Se o item não informa e o catálogo também não tem, a API responde `422` apontando o campo (por exemplo `items.1.codigo_ncm`: "Informe codigo_ncm (ou cadastre o produto BONE-U no catálogo da empresa)").

| Método | Rota | Para quê |
|---|---|---|
| `GET` | `/empresas/{cnpj}/produtos` | listar (paginado; `?q=` busca SKU, descrição ou início do NCM) |
| `GET` | `/empresas/{cnpj}/produtos/{sku}` | detalhar |
| `PUT` | `/empresas/{cnpj}/produtos/{sku}` | criar ou substituir o produto do SKU |
| `DELETE` | `/empresas/{cnpj}/produtos/{sku}` | remover (`204`) |
| `POST` | `/empresas/{cnpj}/produtos/importacao` | importar vários de uma vez |

## Criar ou atualizar um produto

`PUT /empresas/{cnpj}/produtos/CAMISA-P` **cria** (`201`) ou **substitui** (`200`) o produto do SKU da URL. É idempotente: o seu ERP pode repetir o mesmo `PUT` à vontade, o que o torna o jeito natural de **sincronizar** o catálogo.

<!-- exemplo:produto sku=CAMISA-P -->
```json
{
  "descricao": "Camiseta basica algodao P",
  "ncm": "6109.10.00",
  "origem": 0,
  "unidade": "UN",
  "gtin": "7891000100103"
}
```

Resposta (o SKU identifica o produto; não há id numérico):

```json
{ "sku": "CAMISA-P", "descricao": "Camiseta basica algodao P", "ncm": "61091000", "cest": null, "origem": 0, "unidade": "UN", "gtin": "7891000100103", "producao_propria": false }
```

| Campo | Regra |
|---|---|
| `descricao` | obrigatória, até 120 caracteres |
| `ncm` | obrigatório, 8 dígitos (a máscara é removida) |
| `origem` | origem da mercadoria, 0 a 8. Padrão 0 (nacional). **Importados (1, 2, 3, 8...) mudam a alíquota interestadual**: informe certo |
| `unidade` | unidade comercial. Padrão `UN` |
| `cest` | 7 dígitos, para mercadorias sujeitas a substituição tributária |
| `gtin` | GTIN-8/12/13/14 com **dígito verificador válido** (a SEFAZ rejeita o inválido). Sem código de barras, omita |
| `producao_propria` | `true` se você fabrica o produto (CFOP x101); padrão `false` = revenda (CFOP x102) |
| `sku` | se vier no corpo, tem de ser igual ao da URL |

Campo desconhecido é recusado. O mesmo SKU pode existir em empresas diferentes, com dados diferentes.

> SKU com barra (`/`) não cabe na URL: cadastre-o pela importação.

## Importar vários de uma vez

`POST /empresas/{cnpj}/produtos/importacao` grava a lista inteira **ou nada**: se **uma** linha for inválida, nenhum produto é gravado e o erro aponta a linha, para você corrigir e reenviar. SKU que já existe é **atualizado**. Até **2000** produtos por chamada.

<!-- exemplo:importacao -->
```json
{
  "produtos": [
    { "sku": "CAMISA-M", "descricao": "Camiseta basica algodao M", "ncm": "61091000" },
    { "sku": "BONE-U", "descricao": "Bone aba curva", "ncm": "65050090", "origem": 0 }
  ]
}
```

Resposta `200`: `{ "criados": 2, "atualizados": 0 }`.

Com linha inválida, `422` e nada gravado (a "linha" é a posição na lista, começando em 1):

```json
{ "codigo": "requisicao_invalida", "mensagem": "Linha 2: O campo NCM deve ter 8 dígitos.",
  "erros": { "produtos": ["Linha 2: O campo NCM deve ter 8 dígitos."] } }
```

Você também pode enviar um **arquivo CSV** (até 1 MB) em `arquivo`, em `multipart/form-data`, com cabeçalho e colunas `sku;descricao;ncm;cest;origem;unidade;gtin;producao_propria` (aceita `;` ou `,`; as colunas `sku` e `ncm` são obrigatórias):

```bash
curl -s -X POST "$API/empresas/11222333000181/produtos/importacao" -H "Authorization: Bearer $CHAVE" -F "arquivo=@produtos.csv"
```

## Listar

`GET /empresas/{cnpj}/produtos?q=camiseta&per_page=50` devolve a lista paginada (ver [Paginação](02-autenticacao.md#paginacao)), ordenada por SKU. `q` procura no SKU, na descrição e no início do NCM.

---

# Emitir NF-e

`POST /nfe?ref=SUA-REF`: emite uma NF-e. A resposta é **assíncrona**: `202` significa que a nota foi aceita, tem número reservado e está a caminho da SEFAZ.

| | |
|---|---|
| **Rota** | `POST /api/v2/nfe?ref={ref}` |
| **Corpo** | JSON com a venda (destinatário, itens, pagamentos...) |
| **Qual empresa emite** | `cnpj_emitente` no corpo (ou na query). Conta com **uma só** empresa dispensa o campo |
| **Limite** | 120 emissões por minuto por chave (ajustável por conta) |

| Resposta | Quando |
|---|---|
| `202` | aceita e na fila (ou a nota da `ref` já estava em andamento com este mesmo conteúdo) |
| `200` | a `ref` já tinha uma nota **com este mesmo conteúdo**, em estado final: devolve o estado atual |
| `404` | a empresa não é da sua conta (`empresa_nao_encontrada`) |
| `409` | a `ref` já pertence a uma nota com **outro** conteúdo (`ref_ja_utilizada`) |
| `422` | requisição inválida (`requisicao_invalida`), cálculo fiscal recusado (`validacao_fiscal`) ou empresa não informada (`cnpj_emitente_obrigatorio`) |

Tudo o que dá para recusar sem a SEFAZ volta como `422` **aqui, antes de gastar um número da série**: nota recusada na validação não deixa buraco na numeração.

## O exemplo mínimo

Venda para um consumidor final (pessoa física) em outro estado, paga por PIX. É o menor corpo que emite uma nota completa:

<!-- exemplo:emitir -->
```json
{
  "nome_destinatario": "Maria da Silva",
  "cpf_destinatario": "529.982.247-25",
  "logradouro_destinatario": "Av Atlantica",
  "numero_destinatario": "500",
  "bairro_destinatario": "Copacabana",
  "municipio_destinatario": "Rio de Janeiro",
  "codigo_municipio_destinatario": "3304557",
  "uf_destinatario": "RJ",
  "cep_destinatario": "22010-000",
  "items": [
    {
      "codigo_produto": "CAMISA-P",
      "descricao": "Camiseta basica algodao P",
      "codigo_ncm": "61091000",
      "icms_origem": 0,
      "quantidade_comercial": 1,
      "valor_unitario_comercial": 100.00
    }
  ],
  "formas_pagamento": [ { "forma_pagamento": "17" } ]
}
```

Resposta `202`:

```json
{
  "cnpj_emitente": "11222333000181",
  "ref": "PED-1001",
  "ambiente": "homologacao",
  "status": "processando_autorizacao",
  "numero": "1",
  "serie": "1",
  "chave_nfe": null,
  "protocolo_sefaz": null,
  "status_sefaz": null,
  "mensagem_sefaz": null,
  "mensagem": null,
  "valor_total": 100,
  "autorizada_em": null,
  "caminho_xml_nota_fiscal": null,
  "caminho_danfe": null,
  "eventos": []
}
```

Note que **o ICMS, o DIFAL, o PIS/COFINS e o CFOP não foram informados**: a plataforma calcula pelo regime da empresa e pela operação. Aqui, por ser uma venda interestadual a consumidor final não contribuinte, ela calcula também o DIFAL.

Segundos depois, `GET /nfe/PED-1001` devolve a nota autorizada:

```json
{
  "cnpj_emitente": "11222333000181",
  "ref": "PED-1001",
  "ambiente": "homologacao",
  "status": "autorizado",
  "numero": "1",
  "serie": "1",
  "chave_nfe": "35261011222333000181550010000000011219954665",
  "protocolo_sefaz": "135260000000001",
  "status_sefaz": "100",
  "mensagem_sefaz": "Autorizado o uso da NF-e",
  "mensagem": null,
  "valor_total": 100,
  "autorizada_em": "2026-10-04T15:50:10+00:00",
  "caminho_xml_nota_fiscal": "https://srv1839254.hstgr.cloud/api/v2/nfe/PED-1001/xml?cnpj_emitente=11222333000181",
  "caminho_danfe": "https://srv1839254.hstgr.cloud/api/v2/nfe/PED-1001/danfe?cnpj_emitente=11222333000181",
  "eventos": []
}
```

## A resposta da nota

| Campo | Significado |
|---|---|
| `cnpj_emitente`, `ref`, `ambiente` | quem emitiu, o seu identificador e `homologacao` ou `producao` |
| `status` | estado atual: veja [os estados](01-conceitos.md#estados-da-nota) |
| `numero`, `serie` | número fiscal da NF-e e série (texto). Reservados na aceitação; **não mudam** se a nota for rejeitada e reenviada |
| `chave_nfe` | chave de acesso de 44 dígitos (existe depois que a nota é montada e assinada) |
| `protocolo_sefaz` | protocolo de autorização |
| `status_sefaz`, `mensagem_sefaz` | código (`cStat`) e texto da última resposta da SEFAZ. **Sempre o original** |
| `mensagem` | por que a nota está parada ou foi recusada, numa frase (rejeição da SEFAZ ou pendência de validação) |
| `valor_total` | valor da nota em reais |
| `autorizada_em` | quando a SEFAZ autorizou |
| `caminho_xml_nota_fiscal` | link do XML autorizado. Continua disponível **depois do cancelamento** (guarda obrigatória de 5 anos) |
| `caminho_danfe` | link do DANFE (PDF), enquanto a nota vale |
| `eventos` | cancelamentos e cartas de correção, com o estado de cada um (veja [Consultar e eventos](06-eventos.md)) |

Os links exigem a chave de API, como qualquer chamada. **Guarde o XML autorizado:** é o documento fiscal, e você precisa mantê-lo por 5 anos.

## Referência dos campos

Campo desconhecido é recusado (`422`, com o caminho do campo). Valores em reais (número). Documentos aceitam máscara.

### Nota

| Campo | Regra |
|---|---|
| `cnpj_emitente` | a empresa que emite. Dispensável se a conta tem uma só |
| `serie` | série em que a nota sai, `0` a `889`. Padrão `1`. O próximo número de cada série se configura em [Numeração](03-empresas.md#numeracao-da-nf-e-series-e-proximo-numero) |
| `natureza_operacao` | texto, até 60. Padrão `VENDA DE MERCADORIA` |
| `presenca_comprador` | `0` não se aplica, `1` presencial, `2` internet, `3` teleatendimento, `4` entrega a domicílio, `5` presencial fora do estabelecimento, `9` outros. Padrão `2` |
| `modalidade_frete` | `0` por conta do remetente, `1` do destinatário, `2` de terceiros, `3` transporte próprio do remetente, `4` do destinatário, `9` sem frete. Padrão `9` |
| `consumidor_final` | `0` ou `1`. Padrão: `1` se o destinatário **não é contribuinte** de ICMS e `0` se é |
| `valor_frete`, `valor_seguro`, `valor_desconto`, `valor_outras_despesas` | valores **da nota**, que a API rateia entre os itens ([como](#frete-desconto-e-rateio)) |
| `valor_total` | opcional: o total que **você** calculou, com IPI (o `vNF`). A nota só sai se o total calculado pela API fechar com ele |
| `valor_produtos` | opcional: soma dos itens, só para conferência |
| `informacoes_adicionais_contribuinte` | texto livre (até 2000) que vai nas informações complementares da nota |
| `cnpj_intermediador`, `identificador_intermediador` | venda por marketplace ([abaixo](#venda-por-marketplace-com-cartao)) |
| `tipo_documento` | só `1` (saída) |
| `finalidade_emissao` | só `1` (normal). Devolução, complementar e ajuste ainda não |
| `local_destino` | `1` interna, `2` interestadual. Precisa **bater com as UFs** (emitente e destinatário) |

### Destinatário

Informe **um** documento: `cpf_destinatario` ou `cnpj_destinatario` (com dígito verificador válido; o CNPJ pode ser alfanumérico).

| Campo | Regra |
|---|---|
| `nome_destinatario` | obrigatório |
| `cpf_destinatario` / `cnpj_destinatario` | exatamente um dos dois |
| `inscricao_estadual_destinatario` | dígitos, ou o texto `ISENTO` |
| `indicador_inscricao_estadual_destinatario` | `1` contribuinte, `2` isento, `9` não contribuinte. Padrão: CNPJ com IE → `1`; `ISENTO` → `2`; o resto → `9`. Com `1` a IE é obrigatória |
| `logradouro_destinatario`, `bairro_destinatario`, `municipio_destinatario` | obrigatórios |
| `numero_destinatario` | padrão `S/N` |
| `complemento_destinatario`, `telefone_destinatario`, `email_destinatario` | opcionais |
| `uf_destinatario` | obrigatório; UF válida |
| `cep_destinatario` | obrigatório, 8 dígitos |
| `codigo_municipio_destinatario` | código IBGE do município (7 dígitos). **Informe sempre que puder**: se faltar, a API tenta descobrir pelo CEP (consulta externa) e, se não conseguir, devolve `422` pedindo o código |
| `pais_destinatario` | só Brasil (`Brasil` ou `1058`). Destinatário no exterior não é suportado |

> Em **homologação** a SEFAZ exige o texto `NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL` no nome do destinatário e na descrição do item 1: a plataforma coloca sozinha. Você manda os dados reais normalmente.

### Itens (`items`)

De 1 a 990 itens, **na ordem**.

| Campo | Regra |
|---|---|
| `codigo_produto` | obrigatório (até 60). É o SKU e a chave do [catálogo](04-produtos.md) |
| `descricao` | descrição do item (até 120 saem na nota). Se faltar, vem do catálogo |
| `codigo_ncm` | NCM, 8 dígitos. Se faltar, vem do catálogo |
| `icms_origem` | origem da mercadoria, 0 a 8. Se faltar, vem do catálogo |
| `quantidade_comercial` | obrigatória, maior que zero, até 4 casas |
| `valor_unitario_comercial` | obrigatório, até 10 casas (`0.3333` vale) |
| `valor_bruto` | opcional: quantidade × preço. Se vier, precisa bater com a conta (tolerância de R$ 0,01, como a SEFAZ) |
| `unidade_comercial` | padrão `UN` (ou a do catálogo) |
| `codigo_barras_comercial` | GTIN válido, ou `SEM GTIN`/omitido |
| `codigo_cest` | 7 dígitos |
| `producao_propria` | `true` para o que você fabrica (CFOP x101). Padrão: revenda (x102) |
| `valor_desconto`, `valor_frete`, `valor_seguro`, `valor_outras_despesas` | valores **do item**, somados ao rateio da nota |
| `numero_item` | opcional; se vier, tem de ser a posição do item (1, 2, 3...) |
| `unidade_tributavel`, `quantidade_tributavel`, `valor_unitario_tributavel` | só aceitos se **iguais** aos comerciais (unidade tributável diferente ainda não é suportada) |
| `inclui_no_total` | só `1` |
| `cfop`, `icms_situacao_tributaria`, `icms_aliquota`, `icms_reducao_base_calculo`, `fcp_aliquota`, `icms_difal`, `pis_situacao_tributaria`, `pis_aliquota_porcentual`, `cofins_situacao_tributaria`, `cofins_aliquota_porcentual`, `ipi_situacao_tributaria`, `ipi_aliquota` | **parâmetros fiscais** do [modo híbrido](07-modo-hibrido.md). Opcionais: o que você não informa a plataforma calcula |

Valores de imposto (`icms_valor`, `icms_base_calculo`, `pis_valor`, `ipi_valor`...) são **recusados**, com uma mensagem que explica: a API calcula os valores a partir dos parâmetros.

### Pagamentos (`formas_pagamento`)

Pelo menos uma forma de pagamento.

| Campo | Regra |
|---|---|
| `forma_pagamento` | obrigatório. Código da NF-e: `01` dinheiro, `02` cheque, `03` cartão de crédito, `04` cartão de débito, `05` crédito loja, `10` a `13` vales (alimentação, refeição, presente, combustível), `14` duplicata mercantil, `15` boleto, `16` depósito, `17` PIX, `90` sem pagamento, `99` outros. `18` a `22` são os demais meios da tabela de formas de pagamento do leiaute da NF-e (`tPag`) |
| `valor_pagamento` | opcional com **uma** forma (paga a nota inteira); **obrigatório** com duas ou mais, e a soma tem de fechar com o valor da nota |
| `descricao_pagamento` | obrigatória na forma `99` |
| `indicador_pagamento` | `0` à vista, `1` a prazo. Padrão `1` para `03`, `05` e `14`; `0` para as demais |
| `tipo_integracao_pagamento` | cartão (`03` e `04`): `1` integrado ao sistema de pagamento, `2` não integrado. **Obrigatório** no cartão. PIX dinâmico (`17`) também leva o grupo de pagamento eletrônico na nota: se você não informar, sai como `2` (não integrado) — a SEFAZ rejeita (391) o PIX sem esse grupo |
| `cnpj_credenciadora`, `bandeira_operadora`, `numero_autorizacao` | cartão **integrado** (`tipo_integracao_pagamento` = 1): obrigatórios. Bandeira: `01` Visa, `02` Mastercard, `03` Amex, `04` Sorocred, `05` Diners, `06` Elo, `07` Hipercard, `08` Aura, `09` Cabal, `99` outras |

## Frete, desconto e rateio

Você pode informar frete, seguro, desconto e outras despesas **na nota** e/ou **em cada item**. Os valores da nota são **rateados entre os itens, proporcionalmente ao valor de cada um**, e a soma dos itens bate exatamente com o que você informou (a diferença de arredondamento vai para os maiores restos). Os valores de cada item entram **além** do rateio.

Exemplo: dois itens de R$ 100,00 e R$ 30,00, frete de R$ 10,00 na nota e desconto de R$ 5,00. O frete se divide em R$ 7,69 e R$ 2,31; o desconto em R$ 3,85 e R$ 1,15. A nota fecha em `100 + 30 + 10 − 5 = 135,00`.

O desconto de um item não pode passar do valor dele.

## Total e pagamentos precisam fechar

Antes de emitir, a API calcula o total da nota e confere:

- se você informou `valor_total`, ele tem de bater com o total calculado **(incluindo IPI)**. Se não bater: `422 validacao_fiscal` com a mensagem `Total calculado (11500) difere do total informado (9990).` (valores em centavos). É a rede de segurança para um erro de cálculo do seu lado não virar uma nota errada;
- com duas ou mais formas de pagamento, a soma de `valor_pagamento` tem de fechar com o valor da nota.

## Campos aceitos e ignorados

Para facilitar a migração de quem vem de outro emissor, estes campos são **aceitos e ignorados de propósito**:

- `data_emissao`, `data_entrada_saida`: a nota sai com o horário real da emissão, o da plataforma;
- `cpf_emitente`, `nome_emitente`, `nome_fantasia_emitente`, `logradouro_emitente`, `numero_emitente`, `bairro_emitente`, `municipio_emitente`, `uf_emitente`, `cep_emitente`, `inscricao_estadual_emitente`, `regime_tributario_emitente`: os dados do emitente vêm do **cadastro da empresa**.

Qualquer outro campo desconhecido é recusado.

## Reenvio de uma `ref`

O que acontece ao enviar de novo uma `ref` que já existe depende do estado da nota:

| Estado da nota | Mesmo conteúdo | Conteúdo diferente |
|---|---|---|
| `processando_autorizacao` | `202`, devolve o estado (repetir por falha de rede é seguro) | `409 ref_ja_utilizada` |
| `autorizado`, `cancelado`, `denegado`, `inutilizado` | `200`, devolve o estado | `409 ref_ja_utilizada`: use uma `ref` nova |
| `erro_autorizacao` ou `erro_validacao` | reenfileira (é o "tentar de novo") | aceita o conteúdo corrigido e reenfileira, com o **mesmo número** |

Em resumo: **nota rejeitada se corrige reenviando na mesma `ref`**; nota cancelada, denegada ou inutilizada nunca reaproveita a `ref` para outro conteúdo (assim, uma repetição por falha de rede jamais emite uma segunda nota). Para uma venda nova, use sempre uma `ref` nova.

O corpo do `409`:

```json
{ "codigo": "ref_ja_utilizada", "mensagem": "A ref PED-1001 já pertence a uma nota (autorizado) com outro conteúdo. Use uma ref nova para uma nota nova.", "status_atual": "autorizado" }
```

## Exemplos prontos

### Venda B2B com frete, desconto e pagamento dividido

Empresa do Simples Nacional vendendo para uma empresa contribuinte de outro estado. Dois itens, frete de R$ 10,00 e desconto de R$ 5,00 na nota, total conferido, pago em PIX e boleto a prazo:

<!-- exemplo:emitir -->
```json
{
  "natureza_operacao": "VENDA DE MERCADORIA ADQUIRIDA",
  "modalidade_frete": 1,
  "valor_frete": 10.00,
  "valor_desconto": 5.00,
  "valor_total": 135.00,
  "informacoes_adicionais_contribuinte": "Pedido de compra 4471.",
  "nome_destinatario": "COMERCIAL RIO LTDA",
  "cnpj_destinatario": "11.444.777/0001-61",
  "inscricao_estadual_destinatario": "12345678",
  "logradouro_destinatario": "Av Atlantica",
  "numero_destinatario": "500",
  "bairro_destinatario": "Copacabana",
  "municipio_destinatario": "Rio de Janeiro",
  "codigo_municipio_destinatario": "3304557",
  "uf_destinatario": "RJ",
  "cep_destinatario": "22010-000",
  "items": [
    { "codigo_produto": "CAMISA-P", "descricao": "Camiseta basica algodao P", "codigo_ncm": "61091000", "icms_origem": 0,
      "quantidade_comercial": 2, "valor_unitario_comercial": 50.00, "codigo_barras_comercial": "7891000100103" },
    { "codigo_produto": "BONE-U", "descricao": "Bone aba curva", "codigo_ncm": "65050090", "icms_origem": 0,
      "quantidade_comercial": 1, "valor_unitario_comercial": 30.00 }
  ],
  "formas_pagamento": [
    { "forma_pagamento": "17", "valor_pagamento": 85.00 },
    { "forma_pagamento": "15", "valor_pagamento": 50.00, "indicador_pagamento": 1 }
  ]
}
```

### Venda por marketplace com cartão

Quando a venda foi intermediada por um marketplace, informe o CNPJ do intermediador (e o seu identificador de vendedor lá). O pagamento por cartão **integrado** exige credenciadora, bandeira e número de autorização:

<!-- exemplo:emitir -->
```json
{
  "cnpj_intermediador": "03.007.331/0001-41",
  "identificador_intermediador": "123456789",
  "nome_destinatario": "Maria da Silva",
  "cpf_destinatario": "529.982.247-25",
  "logradouro_destinatario": "Av Atlantica",
  "numero_destinatario": "500",
  "bairro_destinatario": "Copacabana",
  "municipio_destinatario": "Rio de Janeiro",
  "codigo_municipio_destinatario": "3304557",
  "uf_destinatario": "RJ",
  "cep_destinatario": "22010-000",
  "items": [
    { "codigo_produto": "CAMISA-P", "descricao": "Camiseta basica algodao P", "codigo_ncm": "61091000", "icms_origem": 0,
      "quantidade_comercial": 1, "valor_unitario_comercial": 100.00 }
  ],
  "formas_pagamento": [
    {
      "forma_pagamento": "03",
      "tipo_integracao_pagamento": 1,
      "cnpj_credenciadora": "03.007.331/0001-41",
      "bandeira_operadora": "01",
      "numero_autorizacao": "A12345"
    }
  ]
}
```

Com `cnpj_intermediador` a nota sai com o indicador de intermediador ligado. O `identificador_intermediador` sozinho (sem o CNPJ) é recusado.

### Empresa do regime normal, venda interna

Empresa do Lucro Presumido vendendo dentro do próprio estado. O ICMS, o PIS, a COFINS e o IBS/CBS são calculados pelo regime, sem você informar nada:

<!-- exemplo:emitir regime=presumido -->
```json
{
  "nome_destinatario": "COMERCIAL PAULISTA LTDA",
  "cnpj_destinatario": "11.444.777/0001-61",
  "inscricao_estadual_destinatario": "110042490114",
  "logradouro_destinatario": "Rua Augusta",
  "numero_destinatario": "1000",
  "bairro_destinatario": "Consolacao",
  "municipio_destinatario": "Sao Paulo",
  "codigo_municipio_destinatario": "3550308",
  "uf_destinatario": "SP",
  "cep_destinatario": "01304-001",
  "items": [
    { "codigo_produto": "CAMISA-P", "descricao": "Camiseta basica algodao P", "codigo_ncm": "61091000", "icms_origem": 0,
      "quantidade_comercial": 2, "valor_unitario_comercial": 50.00 }
  ],
  "formas_pagamento": [ { "forma_pagamento": "15", "indicador_pagamento": 1 } ]
}
```

### Item só com o SKU, usando o catálogo

Se o produto está no [catálogo](04-produtos.md), o item pode ir só com o essencial: o NCM, a origem, a descrição e a unidade vêm do cadastro.

<!-- exemplo:emitir -->
```json
{
  "nome_destinatario": "Maria da Silva",
  "cpf_destinatario": "529.982.247-25",
  "logradouro_destinatario": "Av Atlantica",
  "numero_destinatario": "500",
  "bairro_destinatario": "Copacabana",
  "municipio_destinatario": "Rio de Janeiro",
  "codigo_municipio_destinatario": "3304557",
  "uf_destinatario": "RJ",
  "cep_destinatario": "22010-000",
  "items": [ { "codigo_produto": "CAMISA-P", "quantidade_comercial": 3, "valor_unitario_comercial": 100.00 } ],
  "formas_pagamento": [ { "forma_pagamento": "17" } ]
}
```

## Quando a emissão é recusada

Exemplos reais do que a API devolve na hora, antes de gastar um número:

**Campos inválidos** (`422 requisicao_invalida`, com **todos** os problemas de uma vez):

```json
{
  "codigo": "requisicao_invalida",
  "mensagem": "O campo nome destinatario é obrigatório.",
  "erros": {
    "nome_destinatario": ["O campo nome destinatario é obrigatório."],
    "uf_destinatario": ["O valor informado em uf destinatario é inválido."],
    "items.0.codigo_ncm": ["O campo items.0.codigo_ncm deve ter 8 dígitos."],
    "items.0.icms_valor": ["A API calcula icms_valor a partir da situação tributária e da alíquota informadas; não envie este valor."]
  }
}
```

**Recusa fiscal** (`422 validacao_fiscal`): o cálculo ou a empresa não permitem emitir. Exemplos: total que não fecha, cenário tributário que o motor não suporta, certificado vencido, empresa inativa, produção bloqueada.

```json
{ "codigo": "validacao_fiscal", "mensagem": "Total calculado (11500) difere do total informado (9990)." }
```

**`ref` já usada com outro conteúdo** (`409`): veja [Reenvio de uma `ref`](#reenvio-de-uma-ref).

Se a nota passou pela validação e a SEFAZ a **rejeitar**, o `status` fica `erro_autorizacao` e a nota traz o motivo original:

```json
{
  "ref": "PED-1005", "status": "erro_autorizacao", "numero": "3", "chave_nfe": "35261011222333000181550010000000031948157050",
  "status_sefaz": "225", "mensagem_sefaz": "Rejeicao: Falha no Schema XML da NFe", "mensagem": "Rejeição 225: Rejeicao: Falha no Schema XML da NFe"
}
```

Corrija o problema e reenvie na **mesma `ref`**: o número fiscal é o mesmo (rejeição não consome número). Veja os motivos mais comuns em [Erros](09-erros.md#rejeicoes-comuns-da-sefaz).

---

# Consultar a nota e eventos fiscais

Depois de emitir, tudo se faz pela `ref`. Os eventos fiscais (cancelamento, carta de correção, inutilização) dependem da SEFAZ e por isso são **assíncronos**: respondem `202` e o resultado aparece em `eventos` da nota (e chega por [webhook](08-webhooks.md)).

| Método | Rota | Para quê |
|---|---|---|
| `GET` | `/nfe/{ref}` | estado atual da nota, com chave, protocolo, links e eventos |
| `GET` | `/nfe/{ref}/xml` | XML autorizado (`nfeProc`) |
| `GET` | `/nfe/{ref}/danfe` | DANFE em PDF |
| `DELETE` | `/nfe/{ref}` | cancelar |
| `POST` | `/nfe/{ref}/carta_correcao` | carta de correção (CC-e) |
| `GET` | `/nfe/{ref}/eventos/{id}/xml` | XML de um evento registrado |
| `POST` | `/nfe/inutilizacao` | inutilizar uma faixa de numeração |
| `GET` | `/nfe/inutilizacoes` | histórico de inutilizações |
| `GET` | `/nfe/inutilizacoes/{id}/xml` | XML de uma inutilização registrada |

Em todas as rotas de `/nfe/{ref}`, quem tem **mais de uma empresa** informa `?cnpj_emitente=` (na query; no `POST`/`DELETE` também vale no corpo). Os links devolvidos pela API (`caminho_*`) já levam o `cnpj_emitente`.

## Consultar

`GET /nfe/{ref}` devolve a nota no mesmo formato da emissão (veja [A resposta da nota](05-emitir-nfe.md#a-resposta-da-nota)). `404 nota_nao_encontrada` se não houver nota com essa `ref` **na empresa e no ambiente atual**.

Acompanhando uma emissão por consulta, repita até o `status` ser final (`autorizado`, `cancelado`, `denegado`, `inutilizado`) ou ficar em `erro_autorizacao`/`erro_validacao`, que esperam uma correção sua. Comece a cada 2 segundos e aumente (veja [boas práticas](10-producao.md#acompanhar-a-nota)).

## XML e DANFE

- **XML** (`GET /nfe/{ref}/xml`): o documento fiscal, com o protocolo da SEFAZ. Responde `application/xml`. Disponível enquanto a nota está `autorizado` **e depois de `cancelado`**: a legislação obriga a guardar o XML autorizado e o do cancelamento por 5 anos. Antes da autorização, `404`.
- **DANFE** (`GET /nfe/{ref}/danfe`): o PDF para impressão ou envio ao cliente. Disponível só enquanto a nota está `autorizado`.

Guarde o XML no **seu** sistema assim que a nota for autorizada. Não dependa de buscá-lo só quando alguém pedir.

## Cancelar

`DELETE /nfe/{ref}` pede o cancelamento de uma nota **autorizada**. O corpo:

<!-- exemplo:cancelar -->
```json
{ "justificativa": "Cliente desistiu da compra antes do envio", "confirmo": true }
```

| Campo | Regra |
|---|---|
| `justificativa` | de 15 a 255 caracteres |
| `confirmo` | obrigatório e `true`: você afirma que a **mercadoria não saiu**. Cancelar uma venda já despachada é irregular (o caminho certo é devolução ou estorno, com o contador) |

Resposta `202`: a nota **continua `autorizado`** até a SEFAZ registrar; o pedido aparece em `eventos` com `status: "processando"`:

```json
{
  "ref": "PED-1002", "status": "autorizado", "numero": "2", "chave_nfe": "35261011222333000181550010000000021219954665",
  "eventos": [
    { "id": 1, "tipo": "cancelamento", "sequencia": 1, "status": "processando", "texto": "Cliente desistiu da compra antes do envio",
      "protocolo_sefaz": null, "status_sefaz": null, "mensagem_sefaz": null, "solicitado_em": "2026-10-04T15:50:11+00:00", "caminho_xml": null }
  ]
}
```

Quando a SEFAZ registra, a nota vira `cancelado` e o evento `registrado`, já com o protocolo e o link do XML do evento:

```json
{
  "ref": "PED-1002", "status": "cancelado",
  "caminho_xml_nota_fiscal": "https://srv1839254.hstgr.cloud/api/v2/nfe/PED-1002/xml?cnpj_emitente=11222333000181", "caminho_danfe": null,
  "eventos": [
    { "id": 1, "tipo": "cancelamento", "sequencia": 1, "status": "registrado", "texto": "Cliente desistiu da compra antes do envio",
      "protocolo_sefaz": "135000000000001", "status_sefaz": "135", "mensagem_sefaz": "Evento registrado e vinculado a NF-e",
      "solicitado_em": "2026-10-04T15:50:11+00:00", "caminho_xml": "https://srv1839254.hstgr.cloud/api/v2/nfe/PED-1002/eventos/1/xml?cnpj_emitente=11222333000181" }
  ]
}
```

**Regras e erros** (`422`, com o motivo em `erros`):

- só se cancela nota `autorizado`;
- **prazo:** a SEFAZ aceita o cancelamento por um prazo depois da autorização (24 horas é o padrão nacional; algumas UFs admitem mais). Passou do prazo: a API recusa e orienta falar com o contador (devolução ou estorno);
- não pode haver outro cancelamento ou carta de correção em andamento;
- se a SEFAZ **recusar** o cancelamento, o evento fica `recusado` (com `status_sefaz` e `mensagem_sefaz`), a nota continua `autorizado` e você pode corrigir o motivo e pedir de novo.

```json
{ "codigo": "requisicao_invalida", "mensagem": "Confirme que a mercadoria não saiu. ...", "erros": { "confirmo": ["Confirme que a mercadoria não saiu. ..."] } }
```

## Carta de correção (CC-e)

`POST /nfe/{ref}/carta_correcao` corrige **dados acessórios** de uma nota autorizada: complemento de endereço, informações adicionais, dados de transporte e similares.

<!-- exemplo:correcao -->
```json
{ "correcao": "Correcao do complemento do endereco de entrega para Apto 12" }
```

- `correcao`: de 15 a 1000 caracteres.
- **Não** corrige valores, impostos, dados que mudem o destinatário nem datas. Para isso é cancelamento (dentro do prazo) ou nota de devolução/estorno.
- Resposta `202`; o evento aparece em `eventos` com `tipo: "carta_correcao"` e `sequencia` (1, 2, 3...). Por prática fiscal, cada nova carta deve **repetir as correções anteriores** que continuam valendo: a SEFAZ considera a mais recente.
- A SEFAZ aceita até **20 cartas** por nota. Não dá para enviar uma carta enquanto a anterior estiver em andamento, nem para nota cancelada.
- Se a SEFAZ recusar, o evento fica `recusado` e a **mesma sequência** pode ser usada de novo.

## Inutilizar numeração

Inutilizar é declarar à SEFAZ que uma faixa de números **nunca será usada**. Serve para números reservados que ficaram sem nota (uma nota rejeitada e abandonada, por exemplo). A plataforma não deixa buracos de propósito, mas notas rejeitadas que você desistiu de corrigir deixam números parados.

`POST /nfe/inutilizacao` (a mesma rota e os mesmos nomes do Focus NFe):

<!-- exemplo:inutilizar -->
```json
{
  "cnpj": "11222333000181",
  "serie": 1,
  "numero_inicial": 1,
  "numero_final": 2,
  "justificativa": "Numeros reservados para notas rejeitadas e abandonadas",
  "confirmo": true
}
```

| Campo | Regra |
|---|---|
| `cnpj` (ou `cnpj_emitente`) | a empresa. Dispensável se a conta tem uma só |
| `serie`, `numero_inicial`, `numero_final` | a faixa (contínua, até 1000 números, de **um só ano**) |
| `justificativa` | de 15 a 255 caracteres |
| `confirmo` | obrigatório e `true`: você afirma que os números **não foram usados em nenhuma NF-e**, inclusive em outro sistema de emissão |

Só entram na faixa números que **nunca existiram na SEFAZ**: notas rascunho, rejeitadas ou paradas por pendência. Número de nota autorizada, denegada, cancelada ou em processamento é recusado (`422`, em `erros.faixa`). A faixa também não pode passar do último número já gerado na série.

Resposta `202`, depois consulte por `GET /nfe/inutilizacoes`:

```json
{
  "id": 1, "cnpj": "11222333000181", "ambiente": "homologacao", "serie": "1", "numero_inicial": "5", "numero_final": "6",
  "status": "autorizado", "justificativa": "Numeros reservados para notas rejeitadas e abandonadas",
  "protocolo_sefaz": "141260000000001", "status_sefaz": "102", "mensagem_sefaz": "Inutilizacao de numero homologada",
  "solicitado_em": "2026-10-04T15:53:12+00:00", "caminho_xml": "https://srv1839254.hstgr.cloud/api/v2/nfe/inutilizacoes/1/xml?cnpj_emitente=11222333000181"
}
```

O `status` da inutilização é `processando_autorizacao` enquanto espera a SEFAZ, `autorizado` quando registrada e `erro_autorizacao` quando recusada (veja `status_sefaz` e `mensagem_sefaz`).

Quando a inutilização é registrada, **as notas daquela faixa passam a `inutilizado`** (uma para cada `ref` afetada). Para vender de novo aquele pedido, use uma `ref` nova: ela recebe um número novo.

O prazo legal para inutilizar é **até o dia 10 do mês seguinte** ao da emissão; a SEFAZ não barra depois, mas fora do prazo a obrigação acessória fica irregular: fale com o contador.

---

# Modo híbrido de tributação

Por padrão você **não informa tributo nenhum**: a plataforma calcula tudo pelo regime da empresa e pela operação. Se o seu ERP **já sabe** como uma mercadoria deve ser tributada (um CFOP específico, uma situação tributária, uma alíquota), você pode informar **por item**, e a plataforma passa a usar o que você mandou. O que você não informa continua sendo calculado.

É o "modo híbrido": campo por campo, **o que você informa vale mais** que as regras da plataforma, e o que falta a plataforma preenche.

> **Responsabilidade fiscal.** Os parâmetros que você informa são seus: a plataforma os usa, mas não os valida do ponto de vista fiscal (isso é a SEFAZ e o seu contador). O que ela garante é que o XML sai no leiaute certo e que as contas (bases e valores) batem com os parâmetros que você mandou.

## Os campos do item

| Campo | Efeito |
|---|---|
| `cfop` | CFOP da linha (4 dígitos). Precisa combinar com a operação: **`5xxx`** em venda dentro do estado, **`6xxx`** em venda para outro estado |
| `icms_situacao_tributaria` | **CSOSN** (3 dígitos) se a empresa é do **Simples Nacional/MEI**; **CST** (2 dígitos) nos outros regimes |
| `icms_aliquota` | alíquota do ICMS, em %, para os CST que tributam (00 e 20) |
| `icms_reducao_base_calculo` | redução da base de cálculo, em % (CST 20) |
| `fcp_aliquota` | alíquota do Fundo de Combate à Pobreza, em %, no DIFAL |
| `icms_difal` | `false` desliga o cálculo do DIFAL neste item |
| `pis_situacao_tributaria`, `pis_aliquota_porcentual` | CST do PIS e alíquota em % |
| `cofins_situacao_tributaria`, `cofins_aliquota_porcentual` | CST da COFINS e alíquota em % |
| `ipi_situacao_tributaria`, `ipi_aliquota` | CST do IPI e alíquota em % (empresa industrial) |

Todos são opcionais e **independentes**: informe só o que precisa mudar.

### Códigos de ICMS suportados

| Regime da empresa | Código | Significado |
|---|---|---|
| Simples Nacional / MEI | `101` | tributada com permissão de crédito (exige `p_cred_sn` no cadastro da empresa) |
| | `102`, `103` | tributada sem crédito; isenção de ICMS para faixa de receita |
| | `300`, `400` | imune; não tributada pelo Simples |
| | `500` | ICMS cobrado antes por substituição tributária (a empresa é a substituída) |
| Demais regimes | `00` | tributada integralmente |
| | `20` | com redução de base de cálculo (exige `icms_reducao_base_calculo`) |
| | `40`, `41` | isenta; não tributada |
| | `60` | ICMS cobrado antes por substituição tributária (a empresa é a substituída) |

Outros códigos (por exemplo CSOSN 201, 202, 203 e 900, ou CST 10, 30, 70 e 90, que envolvem **ICMS-ST como substituto**) ainda não são suportados e voltam `422 validacao_fiscal`.

Informar o código "do regime errado" é recusado com uma mensagem clara: CSOSN (3 dígitos) numa empresa de regime normal, ou CST (2 dígitos) numa empresa do Simples.

## Valores de imposto: nunca

Você informa **parâmetros** (código, alíquota, redução). Os **valores** (base de cálculo, valor do ICMS, do PIS, da COFINS, do IPI) são **sempre calculados pela API** a partir dos parâmetros. Campos como `icms_valor`, `icms_base_calculo`, `pis_valor`, `cofins_valor`, `ipi_valor`, `icms_modalidade_base_calculo` e `valor_total_tributos` são **recusados** com a mensagem *"A API calcula ... a partir da situação tributária e da alíquota informadas; não envie este valor."*

Isso é de propósito: assim o seu ERP não consegue mandar uma nota com a conta errada.

## Exemplos

### CFOP e situação tributária informados (Simples Nacional)

Um item sai com **CSOSN 103** e **CFOP 6949** (outra saída); o outro item não informa nada e segue o cálculo padrão (CSOSN 102, CFOP 6102):

<!-- exemplo:emitir -->
```json
{
  "valor_total": 130.00,
  "nome_destinatario": "COMERCIAL RIO LTDA",
  "cnpj_destinatario": "11.444.777/0001-61",
  "inscricao_estadual_destinatario": "12345678",
  "logradouro_destinatario": "Av Atlantica",
  "numero_destinatario": "500",
  "bairro_destinatario": "Copacabana",
  "municipio_destinatario": "Rio de Janeiro",
  "codigo_municipio_destinatario": "3304557",
  "uf_destinatario": "RJ",
  "cep_destinatario": "22010-000",
  "items": [
    { "codigo_produto": "CAMISA-P", "descricao": "Camiseta basica algodao P", "codigo_ncm": "61091000", "icms_origem": 0,
      "quantidade_comercial": 2, "valor_unitario_comercial": 50.00,
      "cfop": "6949", "icms_situacao_tributaria": "103" },
    { "codigo_produto": "BONE-U", "descricao": "Bone aba curva", "codigo_ncm": "65050090", "icms_origem": 0,
      "quantidade_comercial": 1, "valor_unitario_comercial": 30.00 }
  ],
  "formas_pagamento": [ { "forma_pagamento": "15", "indicador_pagamento": 1 } ]
}
```

### Redução de base de cálculo (regime normal)

Empresa do Lucro Presumido com **CST 20**: alíquota de 12% e **redução de 40%** na base. A API calcula: base de R$ 100,00 reduzida a R$ 60,00 e ICMS de R$ 7,20.

<!-- exemplo:emitir regime=presumido -->
```json
{
  "nome_destinatario": "COMERCIAL RIO LTDA",
  "cnpj_destinatario": "11.444.777/0001-61",
  "inscricao_estadual_destinatario": "12345678",
  "logradouro_destinatario": "Av Atlantica",
  "numero_destinatario": "500",
  "bairro_destinatario": "Copacabana",
  "municipio_destinatario": "Rio de Janeiro",
  "codigo_municipio_destinatario": "3304557",
  "uf_destinatario": "RJ",
  "cep_destinatario": "22010-000",
  "items": [
    { "codigo_produto": "CAMISA-P", "descricao": "Camiseta basica algodao P", "codigo_ncm": "61091000", "icms_origem": 0,
      "quantidade_comercial": 2, "valor_unitario_comercial": 50.00,
      "icms_situacao_tributaria": "20", "icms_aliquota": 12, "icms_reducao_base_calculo": 40 }
  ],
  "formas_pagamento": [ { "forma_pagamento": "15", "indicador_pagamento": 1 } ]
}
```

### PIS e COFINS monofásicos

Para uma mercadoria sujeita ao regime monofásico (PIS/COFINS cobrados na origem), informe a situação tributária `04` (sem alíquota) no PIS e na COFINS; o resto continua calculado:

```json
{ "codigo_produto": "BEBIDA-1", "descricao": "Refrigerante 2L", "codigo_ncm": "22021000", "icms_origem": 0,
  "quantidade_comercial": 6, "valor_unitario_comercial": 8.50,
  "pis_situacao_tributaria": "04", "cofins_situacao_tributaria": "04" }
```

CST `01` e `02` pedem a alíquota (`pis_aliquota_porcentual`, `cofins_aliquota_porcentual`). Os CST `04` a `09` não têm base nem valor. `49` e `99` saem sem destaque de valor.

## Quando o cálculo é recusado

O cenário que o motor não cobre volta como `422 validacao_fiscal`, com o motivo, **antes de gastar um número**:

```json
{ "codigo": "validacao_fiscal", "mensagem": "CSOSN 900 (ICMS-ST como substituto) não é suportado; cadastre a regra com o contador (NCM 61091000)." }
```

Se você receber uma recusa fiscal para um produto que o seu contador garante estar certo, informe os parâmetros do item (CFOP, situação tributária, alíquotas) ou peça à plataforma uma regra para aquele NCM. **A API nunca "chuta"**: sem regra e sem parâmetro, ela recusa.

## Como a plataforma decide o que você não informou

Quando você não informa, valem as regras da plataforma, que em resumo são:

- **Simples Nacional/MEI:** CSOSN `102` (ou `101`, se a empresa tem `p_cred_sn`); PIS e COFINS sem destaque.
- **Regime normal:** ICMS pela alíquota interna da UF da empresa (venda interna) ou pela interestadual (4%, 7% ou 12%); PIS e COFINS pelas alíquotas do regime.
- **CFOP:** venda interna `5102` (ou `5101` se `producao_propria`); interestadual a **contribuinte** `6102`/`6101`; interestadual a **não contribuinte** `6108`/`6107`; mercadoria com ICMS-ST cobrado antes (CSOSN 500 ou CST 60) `5405` (interna) ou `6404` (interestadual).
- **DIFAL e FCP:** calculados sozinhos na venda interestadual a consumidor final **não contribuinte**.
- **IBS/CBS** (reforma tributária): incluídos para as empresas dos regimes obrigados, no período de teste.
- **IPI:** só para empresa `industrial`; exige a situação tributária e a alíquota (no item ou numa regra).

Quem precisa da lista completa e das fórmulas (o contador da empresa, por exemplo) pode pedir à plataforma o documento de regras fiscais.

---

# Webhooks

Em vez de consultar `GET /nfe/{ref}` até a nota ficar pronta, você cadastra um endereço do seu sistema e a plataforma envia um `POST` **assinado** a cada mudança de estado das notas. É o jeito recomendado de saber do resultado da SEFAZ.

Webhooks avisam das notas emitidas **por esta API** (as que têm `ref`), das empresas da sua conta. A consulta continua sendo a fonte da verdade: use-a para conferir e para recuperar qualquer aviso que o seu sistema tenha perdido.

| Método | Rota | Para quê |
|---|---|---|
| `POST` | `/webhooks` | cadastrar um endereço |
| `GET` | `/webhooks` · `/webhooks/{id}` | listar · detalhar (o segredo nunca volta) |
| `PATCH` | `/webhooks/{id}` | alterar `url`, `eventos`, `cnpj_emitente`, `descricao`, `ativo` |
| `DELETE` | `/webhooks/{id}` | remover, junto com o histórico de entregas (`204`) |
| `POST` | `/webhooks/{id}/segredo` | gerar um segredo novo |
| `POST` | `/webhooks/{id}/teste` | enviar agora um evento de teste |
| `GET` | `/webhooks/{id}/entregas` | histórico de entregas (`?status=pendente\|entregue\|falhou`) |
| `POST` | `/webhooks/{id}/entregas/{entrega}/reenviar` | reenviar um evento |

## Cadastrar

<!-- exemplo:webhook -->
```json
{
  "url": "https://erp.exemplo.com.br/hooks/nfe",
  "eventos": ["nfe.autorizada", "nfe.rejeitada", "nfe.cancelada"],
  "descricao": "ERP producao"
}
```

| Campo | Regra |
|---|---|
| `url` | obrigatória, até 500 caracteres. **Só `https`**, sem usuário e senha na URL, e o endereço tem de resolver para **IP público** ([segurança](#seguranca-do-endereco)) |
| `eventos` | lista dos eventos que você quer. Omitida ou `null` = **todos** |
| `cnpj_emitente` | restringe a uma empresa. Omitido ou `null` = todas as empresas da conta |
| `descricao` | texto livre para você se organizar |
| `ativo` | `false` pausa as entregas (no `PATCH`) |

Resposta `201`. **O `segredo` só aparece agora**: guarde-o, é com ele que você confere a assinatura de cada chamada.

```json
{
  "segredo": "whsec_t6dAiRDmbFjc54TPAmTdOVqVsDSq3N9njQ2dlED4",
  "id": "88507252-a869-4c69-80ad-6e9dbc5f2d84",
  "url": "https://erp.exemplo.com.br/hooks/nfe",
  "eventos": ["nfe.autorizada", "nfe.cancelada", "nfe.rejeitada"],
  "cnpj_emitente": null,
  "descricao": "ERP producao",
  "ativo": true,
  "criado_em": "2026-10-04T15:50:12+00:00"
}
```

Cada conta pode ter até **10** endereços (`422 limite_de_webhooks`). Perdeu o segredo? `POST /webhooks/{id}/segredo` gera outro e invalida o antigo na hora.

## Os eventos

| Evento | Quando | `dados` |
|---|---|---|
| `nfe.autorizada` | a SEFAZ autorizou a nota | a nota |
| `nfe.rejeitada` | a SEFAZ rejeitou (`status_sefaz` e `mensagem_sefaz` dizem por quê) | a nota |
| `nfe.pendente` | a nota parou por uma pendência de cálculo ou cadastro depois de aceita | a nota |
| `nfe.denegada` | uso denegado | a nota |
| `nfe.cancelada` | o cancelamento foi registrado | a nota |
| `nfe.cancelamento_recusado` | a SEFAZ recusou o cancelamento (a nota continua autorizada) | a nota; `evento_id` aponta o cancelamento em `dados.eventos` |
| `nfe.cce_registrada` · `nfe.cce_recusada` | resultado da carta de correção | a nota; `evento_id` aponta a carta em `dados.eventos` |
| `nfe.inutilizada` | o número de uma nota foi inutilizado (um aviso por nota afetada) | a nota |
| `inutilizacao.registrada` · `inutilizacao.recusada` | resultado do pedido de inutilização | a inutilização (como em `GET /nfe/inutilizacoes`) |
| `webhook.teste` | só pelo `POST /webhooks/{id}/teste`; não se assina, chega sempre | `{ "mensagem": "..." }` |

## O que chega ao seu endereço

Um `POST` com `Content-Type: application/json`. Nos eventos `nfe.*`, o `dados` é **a nota inteira**, no mesmo formato de `GET /nfe/{ref}`:

```json
{
  "id": "cb71def7-68e6-463e-82b0-df2b4605120f",
  "evento": "nfe.autorizada",
  "criado_em": "2026-10-04T15:50:12+00:00",
  "cnpj_emitente": "11222333000181",
  "ref": "PED-1007",
  "dados": {
    "cnpj_emitente": "11222333000181",
    "ref": "PED-1007",
    "ambiente": "homologacao",
    "status": "autorizado",
    "numero": "7",
    "serie": "1",
    "chave_nfe": "35261011222333000181550010000000071445505082",
    "protocolo_sefaz": "135260000000001",
    "status_sefaz": "100",
    "mensagem_sefaz": "Autorizado o uso da NF-e",
    "mensagem": null,
    "valor_total": 115,
    "autorizada_em": "2026-10-04T15:50:12+00:00",
    "caminho_xml_nota_fiscal": "https://srv1839254.hstgr.cloud/api/v2/nfe/PED-1007/xml?cnpj_emitente=11222333000181",
    "caminho_danfe": "https://srv1839254.hstgr.cloud/api/v2/nfe/PED-1007/danfe?cnpj_emitente=11222333000181",
    "eventos": []
  }
}
```

| Campo do envelope | Significado |
|---|---|
| `id` | identificador do **evento**. É o mesmo em todas as tentativas e em todos os reenvios: **deduplique por ele** |
| `evento` | o tipo, da tabela acima |
| `criado_em` | quando o evento foi gerado |
| `cnpj_emitente`, `ref` | a empresa e a nota (`ref` não vem nos eventos de inutilização) |
| `evento_id` | nos eventos de carta de correção e de cancelamento recusado, o `id` do evento em `dados.eventos` |
| `dados` | o conteúdo do evento |

Cabeçalhos:

| Cabeçalho | Valor |
|---|---|
| `X-Webhook-Id` | igual ao `id` do corpo |
| `X-Webhook-Event` | o tipo do evento |
| `X-Webhook-Timestamp` | instante do envio, em segundos Unix |
| `X-Webhook-Signature` | `t=<timestamp>,v1=<assinatura>` |
| `User-Agent` | `EmissorNFe-Webhooks/1.0` |

## Confira a assinatura (sempre)

Qualquer um que descubra a URL do seu webhook pode mandar um `POST` para ela. **Só a assinatura prova que a chamada veio da plataforma.** Antes de confiar no corpo:

1. Leia `t` e `v1` do cabeçalho `X-Webhook-Signature`.
2. Calcule `HMAC-SHA256(segredo, "<t>.<corpo bruto>")` em **hexadecimal** (o `<t>` é o texto do cabeçalho, um ponto, e o corpo exatamente como chegou).
3. Compare com `v1` em **tempo constante** (nunca com `==`).
4. Recuse se `t` estiver a mais de **5 minutos** de agora (isso barra quem grava e repete uma chamada antiga).

Use o **corpo bruto**, antes de decodificar o JSON: um espaço diferente muda a assinatura.

**PHP**

```php
$segredo = getenv('WEBHOOK_SECRET');               // whsec_...
$corpo = file_get_contents('php://input');
$cabecalho = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

parse_str(str_replace(',', '&', $cabecalho), $partes);   // t=...&v1=...
$t = (int) ($partes['t'] ?? 0);
$esperada = hash_hmac('sha256', $t.'.'.$corpo, $segredo);

if (abs(time() - $t) > 300 || ! hash_equals($esperada, $partes['v1'] ?? '')) {
    http_response_code(401);
    exit;
}
$evento = json_decode($corpo, true);
// ...processe e responda 2xx rápido
http_response_code(200);
```

**Node.js (Express)**

```js
const crypto = require('crypto');
const express = require('express');
const app = express();

// raw: precisamos do corpo exatamente como chegou
app.post('/hooks/nfe', express.raw({ type: 'application/json' }), (req, res) => {
  const [t, v1] = req.get('X-Webhook-Signature').split(',').map((p) => p.split('=')[1]);
  const esperada = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(`${t}.${req.body.toString('utf8')}`).digest('hex');
  const ok = Math.abs(Date.now() / 1000 - Number(t)) <= 300
    && v1.length === esperada.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperada));
  if (!ok) return res.sendStatus(401);

  const evento = JSON.parse(req.body.toString('utf8'));
  // ...processe e responda 2xx rápido
  res.sendStatus(200);
});
```

**Python (Flask)**

```python
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)

@app.post('/hooks/nfe')
def webhook():
    corpo = request.get_data()                                  # bytes, como chegou
    partes = dict(p.split('=', 1) for p in request.headers.get('X-Webhook-Signature', '').split(','))
    t = int(partes.get('t', 0))
    esperada = hmac.new(os.environ['WEBHOOK_SECRET'].encode(), f'{t}.'.encode() + corpo, hashlib.sha256).hexdigest()
    if abs(time.time() - t) > 300 or not hmac.compare_digest(esperada, partes.get('v1', '')):
        abort(401)
    evento = request.get_json()
    # ...processe e responda 2xx rápido
    return '', 200
```

**Vetor de teste** para o seu verificador: com o segredo `whsec_exemplo_de_teste`, `t = 1700000000` e este corpo (sem quebra de linha no fim)

```json
{"id":"11111111-1111-1111-1111-111111111111","evento":"webhook.teste","criado_em":"2023-11-14T22:13:20+00:00","dados":{"mensagem":"Evento de teste do emissor de NF-e."}}
```

a assinatura correta é:

```text
902528f43d51b4707756145ad95c0b38b3890f25a580ab1a274ec7d7a428ee4d
```

ou seja, o cabeçalho `X-Webhook-Signature: t=1700000000,v1=902528f43d51b4707756145ad95c0b38b3890f25a580ab1a274ec7d7a428ee4d`. (No teste, ignore a checagem dos 5 minutos, já que o `t` é antigo.)

## Como responder e o que acontece nas falhas

- **Responda `2xx` em até 10 segundos** e processe o evento depois (numa fila sua). Qualquer outra coisa conta como falha: erro `4xx`/`5xx`, tempo esgotado, falha de conexão e **redirecionamento** (`3xx`, que **não é seguido**).
- Em caso de falha a entrega é **repetida com espera crescente**: 30 segundos, 2 minutos, 10 minutos, 30 minutos, 1 hora, 3 horas, 6 horas e 12 horas. São **9 tentativas em cerca de 21 horas**. Esgotadas, a entrega fica `falhou` e você pode reenviá-la.
- Todas as tentativas levam o **mesmo `id`**: o seu sistema deve ser **idempotente** (se já processou aquele `id`, responda `200` e ignore).
- **Não há garantia de ordem** entre eventos diferentes (um `nfe.cancelada` pode chegar antes de um `nfe.autorizada` atrasado). Quando o estado atual importar, use `criado_em` ou consulte `GET /nfe/{ref}`.
- Pode haver **mais de um evento** por nota (rejeitada, depois autorizada; autorizada, depois cancelada).

### Histórico e reenvio

`GET /webhooks/{id}/entregas` mostra o que foi enviado e como foi:

```json
{
  "data": [
    { "id": 2, "evento": "webhook.teste", "evento_id": "038dbca4-548b-4d4e-8352-734bd237f94a", "status": "entregue", "tentativas": 1,
      "proxima_tentativa_em": null, "ultimo_status_http": 200, "ultimo_erro": null, "ultima_resposta": "ok",
      "entregue_em": "2026-10-04T15:50:12+00:00", "criado_em": "2026-10-04T15:50:12+00:00" }
  ],
  "links": { "first": "...?page=1", "last": "...?page=1", "prev": null, "next": null },
  "meta": { "current_page": 1, "last_page": 1, "per_page": 25, "total": 1 }
}
```

`status`: `pendente` (aguardando a próxima tentativa), `entregue` ou `falhou`. `ultimo_status_http`, `ultimo_erro` e o começo de `ultima_resposta` ajudam a diagnosticar o seu lado.

`POST /webhooks/{id}/entregas/{entrega}/reenviar` cria uma **nova entrega com o mesmo evento** (o mesmo `evento_id`), por exemplo depois de você corrigir o que fazia a entrega falhar.

`POST /webhooks/{id}/teste` envia um `webhook.teste` agora: o jeito mais rápido de validar o endereço e o seu código de conferência. A resposta é a entrega, já com o resultado (`entregue` ou o erro).

## Segurança do endereço

Como a plataforma chama um endereço **que você escolhe**, ela impõe regras para não poder ser usada contra redes internas. São checadas no cadastro **e em cada entrega** (um DNS pode mudar depois do cadastro):

- só `https` (sem `http`);
- sem usuário e senha na URL (a autenticidade se confere pela assinatura);
- o endereço precisa resolver **só para IPs públicos** e ter ao menos um IPv4. Redes privadas (`10.x`, `172.16-31.x`, `192.168.x`), `localhost`, endereços de link-local e de metadados de nuvem são recusados;
- a conexão é fixada no IP validado e **redirecionamentos não são seguidos**.

Por isso **não dá para apontar o webhook para `localhost`** em desenvolvimento. Use um túnel (ngrok, Cloudflare Tunnel...) com endereço `https` público para receber os eventos na sua máquina.

O histórico de entregas é guardado por 30 dias.

---

# Erros

Todo erro tem o mesmo formato (`codigo`, `mensagem` e, na validação, `erros`; veja [Formato dos erros](02-autenticacao.md#formato-dos-erros)). **Programe contra o `codigo`**, que é estável; a `mensagem` é para uma pessoa ler e pode mudar de redação.

## Catálogo de códigos

| HTTP | `codigo` | Significa | O que fazer |
|---|---|---|---|
| `401` | `nao_autorizado` | chave ausente, inválida, revogada ou vencida (a mesma resposta nos quatro casos) | confira a chave; peça uma nova se foi revogada ou venceu. **Não** repita em laço |
| `403` | `conta_inativa` | a conta foi desativada | fale com o suporte |
| `404` | `empresa_nao_encontrada` | o CNPJ não existe **ou** não é da sua conta (a mesma resposta) | confira o CNPJ e a chave |
| `404` | `nota_nao_encontrada` | não há nota com essa `ref` nessa empresa e nesse ambiente | confira `ref`, `cnpj_emitente` e o ambiente atual da empresa |
| `404` | `produto_nao_encontrado` | não há produto com esse SKU nessa empresa | confira o SKU |
| `404` | `webhook_nao_encontrado` · `entrega_nao_encontrada` | o webhook ou a entrega não existe nesta conta | confira o `id` |
| `404` | `nao_encontrado` | rota ou arquivo que não existe (ou XML/DANFE ainda não disponível) | confira a URL; o XML só existe depois da autorização |
| `405` | `metodo_nao_permitido` | o método HTTP não vale para esta rota | confira o método na [referência](12-referencia.md) |
| `409` | `ref_ja_utilizada` | a `ref` já pertence a uma nota com **outro** conteúdo | se é a mesma venda, reenvie o conteúdo original; se é uma venda nova, use uma `ref` nova. `status_atual` diz em que estado está a nota |
| `422` | `requisicao_invalida` | formato, campo desconhecido, dado faltando, valor que não fecha. **Todos** os problemas vêm em `erros`, por campo | corrija os campos apontados e reenvie. Repetir igual não adianta |
| `422` | `validacao_fiscal` | o cálculo ou a empresa não permitem emitir: total que não fecha, cenário tributário não suportado, certificado vencido, empresa inativa, produção bloqueada | leia a `mensagem` (diz o que ajustar); corrija o cadastro ou os parâmetros fiscais |
| `422` | `cnpj_emitente_obrigatorio` | a conta tem mais de uma empresa e você não disse qual | envie `cnpj_emitente` |
| `422` | `limite_de_empresas` | a conta já tem o máximo de empresas | fale com o suporte para ampliar |
| `422` | `limite_de_webhooks` | a conta já tem o máximo de endereços de webhook (10) | remova um que não usa mais |
| `422` | `numero_retroativo` | o próximo número informado para a série é menor ou igual ao maior já usado nela (a numeração não volta) | use ao menos o `minimo` da resposta; número já usado não se reaproveita |
| `429` | `limite_de_requisicoes` | estourou o limite por minuto | espere o `Retry-After` (segundos) e repita |
| `5xx` | `erro_interno` | falha da plataforma | tente de novo depois de alguns segundos (com a **mesma `ref`**, repetir é seguro). Se persistir, cite o `X-Request-Id` ao suporte |

### Quando repetir a chamada

| Resposta | Repetir? |
|---|---|
| falha de rede, tempo esgotado, `5xx`, `429` | **sim**, com espera crescente. No `POST /nfe`, com a **mesma `ref` e o mesmo corpo** (é idempotente) |
| `401`, `403`, `404`, `405` | não: o problema é de chave, permissão ou endereço |
| `409`, `422` | não, **sem mudar nada**: corrija o que foi apontado |

## Como ler um `422`

`erros` é um objeto com **o caminho de cada campo** com problema e a lista de mensagens dele:

```json
{
  "codigo": "requisicao_invalida",
  "mensagem": "O campo nome destinatario é obrigatório.",
  "erros": {
    "nome_destinatario": ["O campo nome destinatario é obrigatório."],
    "items.0.codigo_ncm": ["O campo items.0.codigo_ncm deve ter 8 dígitos."],
    "formas_pagamento.1.valor_pagamento": ["..."]
  }
}
```

- `items.0.codigo_ncm` é o campo `codigo_ncm` do **primeiro** item (a contagem começa em 0).
- Erros de **importação** de produtos citam a linha: `"Linha 2: O campo NCM deve ter 8 dígitos."` (a linha é a posição na lista, começando em 1).
- Alguns erros não são de um campo, mas de uma regra: `erros.faixa` na inutilização, `erros.nota` num cancelamento fora do prazo, `erros.ambiente` com o que impede a produção, `erros.certificado` para um certificado recusado.
- `mensagem` repete o **primeiro** erro, para registrar numa linha de log.

## Recusa fiscal (`validacao_fiscal`)

Não é um defeito do JSON: o JSON está certo, mas o **cálculo** ou a **empresa** não permitem emitir. As mensagens dizem o que ajustar. As mais comuns:

| Mensagem (resumo) | O que fazer |
|---|---|
| `Total calculado (X) difere do total informado (Y).` | os valores estão em **centavos**. Confira preços, quantidades, frete, desconto e IPI; ou omita `valor_total` |
| `A soma das formas de pagamento (X) difere do valor da nota (Y).` | acerte os `valor_pagamento` |
| `Com mais de uma forma de pagamento, informe o valor de cada uma.` | informe `valor_pagamento` em todas |
| `Certificado A1 ... ausente ou vencido.` | envie um certificado válido ([Empresas](03-empresas.md#certificado-digital-a1)) |
| `Emissão em produção bloqueada: as tabelas fiscais ... (FISCAL_RATES_VERIFIED).` | a plataforma ainda não liberou a produção; emita em homologação ou fale com o suporte |
| `CSOSN 900 ... não é suportado` (e similares) | o cenário tributário (ICMS-ST como substituto, por exemplo) ainda não é coberto: veja [Modo híbrido](07-modo-hibrido.md) |
| `NCM inválido no item ...` | o NCM tem de ter 8 dígitos |
| `Vendedor industrial: cadastre ipi_cst/ipi_aliq ...` | empresa industrial exige a situação tributária e a alíquota do IPI no item |
| `UF inválida ou sem tabela fiscal` | confira a UF do destinatário |

## Rejeições comuns da SEFAZ

Se a nota passa pela validação e a SEFAZ a rejeita, o `status` fica `erro_autorizacao` e a nota traz `status_sefaz` (o `cStat`) e `mensagem_sefaz` (o texto original). **Ao conferir, o texto da SEFAZ é o que vale.** Os códigos abaixo são a leitura habitual da tabela da SEFAZ:

| `status_sefaz` | Costuma indicar | O que fazer |
|---|---|---|
| `203` | emissor não habilitado para emitir NF-e na UF | o CNPJ/IE precisa estar **credenciado** para emitir NF-e na SEFAZ da UF (vale também em homologação em várias UFs) |
| `209`, `233`, `234` | inscrição estadual do emitente inválida, não cadastrada ou não vinculada ao CNPJ | confira a `ie` da empresa ([`PATCH /empresas/{cnpj}`](03-empresas.md#atualizar-uma-empresa)) |
| `213` | o CNPJ do certificado não é o da empresa | envie o certificado correto |
| `280` a `286` | problema no certificado: inválido, vencido, sem CNPJ, cadeia não ICP-Brasil | envie um **e-CNPJ A1 ICP-Brasil** válido |
| `225` | falha de schema XML | é defeito do lado da plataforma: abra um chamado com o `ref` e o `X-Request-Id` |
| `108`, `109` | serviço da SEFAZ paralisado | **nada a fazer**: a plataforma tenta de novo sozinha por até 6 horas; a nota fica `processando_autorizacao` |
| `204`, `539` | a SEFAZ já tem uma nota com esta chave | tratado pela plataforma: ela consulta a chave e dá a nota por autorizada, sem emitir outra |
| `110`, `301`, `302`, `303` | uso denegado | o `status` fica `denegado`: o número foi consumido e a nota não vale. Fale com o contador; para vender de novo, use uma `ref` nova |

Em caso de rejeição, **corrija e reenvie na mesma `ref`**: o número fiscal é o mesmo (rejeição não consome número) e o histórico fica coerente.

## Problemas frequentes

**A nota está em `processando_autorizacao` há muito tempo.** A SEFAZ pode estar lenta ou fora do ar. A plataforma tenta de novo sozinha por até 6 horas e, depois de uma falha de comunicação, **consulta a chave antes de reenviar** (para nunca emitir duplicado). Consulte `GET /nfe/{ref}`: se `mensagem` trouxer uma falha de comunicação, é a SEFAZ. Não reenvie a mesma venda com `ref` nova: criaria uma segunda nota.

**Recebi `401` e a chave é a mesma de ontem.** A chave foi revogada ou venceu. Peça uma nova (e, na rotação, siga o [passo a passo](10-producao.md#chaves-de-api)).

**O webhook não chega.** (1) Use `POST /webhooks/{id}/teste` e veja o resultado; (2) em `GET /webhooks/{id}/entregas`, confira `ultimo_status_http` e `ultimo_erro`; (3) o endereço precisa ser `https` e público (não `localhost`); (4) o seu servidor precisa responder `2xx` em até 10 segundos; (5) confira se o `ativo` é `true` e se `eventos` inclui o evento esperado.

**`422` com `codigo_municipio_destinatario`.** A API não conseguiu descobrir o município pelo CEP. Informe `codigo_municipio_destinatario` (código IBGE de 7 dígitos).

**A mesma nota foi emitida duas vezes.** Só acontece se você usou **`ref` diferentes** para a mesma venda. Com a mesma `ref`, repetir devolve sempre a mesma nota. Use um identificador estável da venda (o número do pedido) como `ref`.

**Preciso corrigir o valor de uma nota autorizada.** Valores e impostos **não** se corrigem com carta de correção. Dentro do prazo, cancele ([Cancelar](06-eventos.md#cancelar)) e emita outra com `ref` nova; fora dele, é devolução ou estorno com o contador.

---

# Ir para produção e boas práticas

## Checklist de entrada em produção

Antes de ligar uma empresa em produção (as notas passam a ter **valor fiscal**):

**Cadastro**
- [ ] A empresa está cadastrada com os dados **reais**: razão social, CNPJ, **inscrição estadual habilitada** na SEFAZ, endereço completo e o **regime tributário verdadeiro** (ele decide como o ICMS é calculado).
- [ ] O certificado **A1 e-CNPJ ICP-Brasil** está instalado e dentro da validade (`GET /empresas/{cnpj}/certificado`).
- [ ] O CNPJ da empresa está **credenciado para emitir NF-e** na SEFAZ da UF.

**Testes em homologação** (cada um com nota real na SEFAZ de homologação)
- [ ] emissão de cada **tipo de venda** que o seu ERP faz: consumidor final, empresa contribuinte, interestadual, com frete e desconto, cada forma de pagamento (inclusive cartão e marketplace, se usa);
- [ ] **rejeição e reenvio**: provoque uma rejeição (um dado errado) e confirme que o seu sistema reenvia **na mesma `ref`**;
- [ ] **repetição por falha de rede**: reenvie o mesmo `POST` e confirme que nenhuma nota nova nasce;
- [ ] **cancelamento**, **carta de correção** e **inutilização**;
- [ ] **webhooks**: recebimento, **conferência da assinatura** (com o vetor de teste de [Webhooks](08-webhooks.md#confira-a-assinatura-sempre)), resposta rápida e idempotência por `id`;
- [ ] **download e guarda** do XML autorizado e do DANFE no seu sistema.

**Fiscal**
- [ ] **O contador da empresa revisou** os tributos que saem nas notas de homologação: a SEFAZ de homologação valida o leiaute e as regras de preenchimento, mas **não confere se o ICMS é o devido**.
- [ ] Os produtos têm **NCM e origem corretos** (importados mudam a alíquota interestadual) e GTIN válido, ou nenhum.

**Troca**
- [ ] `PUT /empresas/{cnpj}/ambiente` com `{"ambiente": "producao"}` (devolve o que ainda impede, se algo impedir).
- [ ] **A primeira nota de produção é acompanhada de perto**: emita uma venda real, confira a `chave_nfe` na consulta pública da SEFAZ e o DANFE.

Pode **reaproveitar as mesmas `ref`** que usou em homologação: as notas de produção começam do zero e a numeração é separada.

## Tentativas e erros de rede

A regra de ouro: **no `POST /nfe`, repetir é sempre seguro**, porque a `ref` é idempotente. Falhou? Repita com a **mesma `ref` e o mesmo corpo**.

| Situação | O que fazer |
|---|---|
| tempo esgotado ou conexão caiu, sem saber se a nota foi aceita | repita o `POST` igual. Ou consulte `GET /nfe/{ref}` primeiro |
| `5xx` | repita com espera crescente |
| `429` | espere o `Retry-After` e repita |
| `422`, `409` | **não** repita sem mudar nada: corrija o que foi apontado |

Use **espera exponencial com variação aleatória** (jitter), com um teto de tentativas. Por exemplo: 1 s, 2 s, 4 s, 8 s, 16 s (cada uma ± 20%), parando depois de 5 a 8 tentativas e alertando uma pessoa. Respeite `Retry-After` quando houver.

```text
espera = min(60, 2^tentativa) segundos × (0,8 a 1,2 aleatório)
```

## Acompanhar a nota

**Recomendação:** use **webhooks** para reagir e uma **reconciliação periódica** como rede de segurança.

- **Webhook:** processe o evento, responda `2xx` rápido, e seja idempotente (deduplique por `id`).
- **Polling** (se não puder receber webhooks): consulte `GET /nfe/{ref}` a cada 2 segundos nos primeiros 30 segundos, depois a cada 10, depois a cada minuto, até um estado final. **Nunca** consulte em laço sem espera: você bate no limite de requisições.
- **Reconciliação:** um trabalho seu (a cada 5 a 15 minutos) que consulta as notas que o seu sistema ainda considera pendentes e as notas dos últimos dias. Cobre o webhook que não chegou e o seu servidor que estava fora do ar.

Uma nota pode ficar em `processando_autorizacao` por **minutos ou até algumas horas** se a SEFAZ da UF estiver instável: a plataforma tenta de novo sozinha por até 6 horas e **consulta a chave antes de reenviar**, então nunca duplica. Não reenvie a venda com `ref` nova, e **não bloqueie a operação do cliente esperando a SEFAZ**: trate a nota como "em processamento" e siga.

## Guarde os documentos fiscais

- **XML autorizado:** guarde no seu sistema assim que o `nfe.autorizada` chegar (ou o `status` virar `autorizado`). A obrigação de guarda é de **5 anos**. Depois do cancelamento, o XML autorizado continua disponível na API, e o XML do evento de cancelamento vem em `eventos[].caminho_xml`: guarde os dois.
- **DANFE:** pode ser gerado de novo a qualquer momento enquanto a nota vale; guarde se precisar enviar ao cliente.
- **A chave de acesso** (`chave_nfe`) identifica a nota na consulta pública da SEFAZ. Guarde junto do pedido.

## Chaves de API

- Uma chave por **ambiente de implantação** (produção, homologação) e por sistema, para revogar uma sem afetar as outras.
- **Rotação sem parada:** (1) peça uma chave nova; (2) coloque-a no seu sistema e faça o deploy; (3) confirme pelo `GET /requisicoes` que a chave antiga parou de ser usada (campo `chave`, o prefixo); (4) peça a revogação da antiga.
- Guarde em **cofre de segredos** ou variável de ambiente. Nunca em repositório, no front-end ou em logs.
- Suspeita de vazamento? Revogue na hora: o efeito é imediato.

## Monitoramento

- **Registre o `X-Request-Id`** de cada resposta nos seus logs, ao lado do `ref`. É o que o suporte pede.
- **Monitore o certificado:** consulte `GET /empresas/{cnpj}/certificado` periodicamente e alerte com 30 dias de antecedência (`dias_restantes`). Certificado vencido **bloqueia toda a emissão** da empresa.
- **Alerte** quando uma nota ficar mais de 1 hora em `processando_autorizacao`, ou em `erro_autorizacao`/`erro_validacao` sem que alguém tenha agido.
- **Acompanhe as entregas** dos webhooks (`GET /webhooks/{id}/entregas?status=falhou`): entregas que esgotaram as tentativas precisam de reenvio.
- Use `GET /requisicoes?status=5xx` e `?status=429` para enxergar problemas da integração (por exemplo, estourar o limite).

## Numeração

A plataforma reserva o número da NF-e **em sequência, sem buracos nem duplicidade**, mesmo com várias lojas do mesmo CNPJ emitindo ao mesmo tempo. Uma nota rejeitada **mantém o número** e é reenviada com ele. Os únicos "buracos" que podem surgir são de notas rejeitadas que você nunca corrigiu: **inutilize** essa faixa ([Inutilizar numeração](06-eventos.md#inutilizar-numeracao)) até o dia 10 do mês seguinte.

Se a empresa **migra de outro emissor**, informe o próximo número **antes da primeira nota em produção**: a numeração da série começa em 1 e precisa continuar de onde o emissor anterior parou, senão haverá duplicidade com notas já emitidas. Veja [Numeração da NF-e](03-empresas.md#numeracao-da-nf-e-series-e-proximo-numero); pular números sem nota obriga a inutilizar a faixa pulada.

## Boas práticas de integração

- Use um **identificador estável da venda** como `ref` (o número do pedido). Nunca um valor aleatório gerado a cada tentativa: ele anularia a proteção contra duplicidade.
- **Valide no seu lado** o que puder (CPF/CNPJ, NCM de 8 dígitos, totais) para evitar viagens de ida e volta com `422`.
- **Informe sempre `codigo_municipio_destinatario`** (o IBGE do município do destinatário): evita depender de uma consulta externa por CEP.
- **Mantenha o catálogo** de produtos sincronizado com `PUT /empresas/{cnpj}/produtos/{sku}` (idempotente) para que os itens possam ir só com o essencial.
- Trate **campos novos** em respostas e **valores novos** de `status` sem quebrar (veja [Compatibilidade](02-autenticacao.md#compatibilidade)).
- Não faça **emissões em paralelo para a mesma venda**: uma `ref`, uma chamada por vez. Entre vendas diferentes, paralelize à vontade (respeitando o limite de 120 emissões por minuto).
- Em caso de dúvida fiscal sobre uma operação, **fale com o contador antes de emitir**: a nota fiscal tem consequência tributária.

## Limitações atuais

A plataforma é transparente sobre o que **ainda não cobre**. Nesses casos ela recusa com `422` em vez de arriscar:

- **ICMS-ST como substituto** (CSOSN 201, 202, 203, 900; CST 10, 30, 70, 90);
- **devolução, nota complementar e de ajuste** (`finalidade_emissao` diferente de 1) e nota de **entrada**;
- **destinatário no exterior**, exportação e importação;
- **transportadora, volumes e reboque**: a nota sai só com a modalidade do frete;
- **unidade tributável diferente da comercial**;
- **contingência** (SVC e EPEC): com a SEFAZ fora do ar a plataforma aguarda e tenta de novo por até 6 horas, mas não emite em contingência.

Precisa de algum desses? Fale com o suporte: ajuda a definir a prioridade.

---

# Exemplos de código

Clientes mínimos que **emitem uma nota e acompanham até o estado final**, nas três linguagens mais comuns em integrações. São pontos de partida: ajuste o tratamento de erros e o armazenamento ao seu sistema. Todos seguem as [boas práticas](10-producao.md): `ref` estável, repetição com espera crescente e tratamento do `Retry-After`.

Defina a base e a chave pelo ambiente:

```bash
export EMISSOR_URL="https://srv1839254.hstgr.cloud/api/v2"
export EMISSOR_CHAVE="nfe_SUA_CHAVE_AQUI"
```

## PHP

Só com a extensão `curl` (sem dependências):

```php
<?php

final class EmissorNfe
{
    public function __construct(private string $base, private string $chave) {}

    /** @return array{status: int, corpo: array} */
    private function chamar(string $metodo, string $caminho, ?array $json = null): array
    {
        for ($tentativa = 0; ; $tentativa++) {
            $ch = curl_init($this->base.$caminho);
            curl_setopt_array($ch, [
                CURLOPT_CUSTOMREQUEST => $metodo,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_HEADER => true,
                CURLOPT_TIMEOUT => 30,
                CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$this->chave, 'Accept: application/json', 'Content-Type: application/json'],
                CURLOPT_POSTFIELDS => $json === null ? null : json_encode($json, JSON_UNESCAPED_UNICODE),
            ]);
            $resposta = curl_exec($ch);
            $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            $tamanhoCabecalho = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
            curl_close($ch);

            // Falha de rede, 5xx ou 429: repete com espera crescente. É seguro porque a ref é idempotente.
            $repetir = $resposta === false || $status >= 500 || $status === 429;
            if ($repetir && $tentativa < 6) {
                preg_match('/^Retry-After:\s*(\d+)/mi', (string) $resposta, $m);
                sleep(isset($m[1]) ? (int) $m[1] : min(60, 2 ** $tentativa));
                continue;
            }

            return ['status' => $status, 'corpo' => (array) json_decode(substr((string) $resposta, $tamanhoCabecalho), true)];
        }
    }

    /** Emite e espera o estado final. Devolve a nota. */
    public function emitir(string $ref, array $venda, int $esperaMaxima = 120): array
    {
        $r = $this->chamar('POST', '/nfe?ref='.rawurlencode($ref), $venda);
        if ($r['status'] === 422 || $r['status'] === 409) {
            throw new RuntimeException($r['corpo']['codigo'].': '.$r['corpo']['mensagem']);   // corrija e reenvie: não adianta repetir igual
        }

        $nota = $r['corpo'];
        $limite = time() + $esperaMaxima;
        $final = ['autorizado', 'cancelado', 'denegado', 'inutilizado', 'erro_autorizacao', 'erro_validacao'];
        for ($espera = 2; ! in_array($nota['status'] ?? '', $final, true) && time() < $limite; $espera = min($espera * 2, 15)) {
            sleep($espera);
            $nota = $this->chamar('GET', '/nfe/'.rawurlencode($ref).'?cnpj_emitente='.$venda['cnpj_emitente'])['corpo'];
        }

        return $nota;
    }
}

$api = new EmissorNfe(getenv('EMISSOR_URL'), getenv('EMISSOR_CHAVE'));
$nota = $api->emitir('PEDIDO-1001', [
    'cnpj_emitente' => '11222333000181',
    'nome_destinatario' => 'Maria da Silva', 'cpf_destinatario' => '529.982.247-25',
    'logradouro_destinatario' => 'Av Atlantica', 'numero_destinatario' => '500', 'bairro_destinatario' => 'Copacabana',
    'municipio_destinatario' => 'Rio de Janeiro', 'codigo_municipio_destinatario' => '3304557', 'uf_destinatario' => 'RJ', 'cep_destinatario' => '22010-000',
    'items' => [[
        'codigo_produto' => 'CAMISA-P', 'descricao' => 'Camiseta basica P', 'codigo_ncm' => '61091000', 'icms_origem' => 0,
        'quantidade_comercial' => 1, 'valor_unitario_comercial' => 100.00,
    ]],
    'formas_pagamento' => [['forma_pagamento' => '17']],
]);

echo $nota['status'].' '.($nota['chave_nfe'] ?? '').PHP_EOL;
```

## Node.js

Node 18 ou mais novo (usa o `fetch` nativo):

```js
const BASE = process.env.EMISSOR_URL;
const CHAVE = process.env.EMISSOR_CHAVE;
const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
const FINAIS = ['autorizado', 'cancelado', 'denegado', 'inutilizado', 'erro_autorizacao', 'erro_validacao'];

async function chamar(metodo, caminho, corpo) {
  for (let tentativa = 0; ; tentativa++) {
    let resposta;
    try {
      resposta = await fetch(BASE + caminho, {
        method: metodo,
        headers: { Authorization: `Bearer ${CHAVE}`, Accept: 'application/json', 'Content-Type': 'application/json' },
        body: corpo ? JSON.stringify(corpo) : undefined,
        signal: AbortSignal.timeout(30_000),
      });
    } catch (e) {
      resposta = null; // falha de rede ou tempo esgotado
    }
    // Falha de rede, 5xx ou 429: repete com espera crescente (seguro: a ref é idempotente)
    if ((resposta === null || resposta.status >= 500 || resposta.status === 429) && tentativa < 6) {
      const retryAfter = Number(resposta?.headers.get('Retry-After'));
      await dormir((retryAfter || Math.min(60, 2 ** tentativa)) * 1000);
      continue;
    }
    if (resposta === null) throw new Error('sem resposta da API');
    return { status: resposta.status, corpo: await resposta.json() };
  }
}

async function emitir(ref, venda, esperaMaximaSeg = 120) {
  const r = await chamar('POST', `/nfe?ref=${encodeURIComponent(ref)}`, venda);
  if (r.status === 422 || r.status === 409) throw new Error(`${r.corpo.codigo}: ${r.corpo.mensagem}`); // corrija e reenvie

  let nota = r.corpo;
  const limite = Date.now() + esperaMaximaSeg * 1000;
  for (let espera = 2; !FINAIS.includes(nota.status) && Date.now() < limite; espera = Math.min(espera * 2, 15)) {
    await dormir(espera * 1000);
    nota = (await chamar('GET', `/nfe/${encodeURIComponent(ref)}?cnpj_emitente=${venda.cnpj_emitente}`)).corpo;
  }
  return nota;
}

emitir('PEDIDO-1001', {
  cnpj_emitente: '11222333000181',
  nome_destinatario: 'Maria da Silva', cpf_destinatario: '529.982.247-25',
  logradouro_destinatario: 'Av Atlantica', numero_destinatario: '500', bairro_destinatario: 'Copacabana',
  municipio_destinatario: 'Rio de Janeiro', codigo_municipio_destinatario: '3304557', uf_destinatario: 'RJ', cep_destinatario: '22010-000',
  items: [{ codigo_produto: 'CAMISA-P', descricao: 'Camiseta basica P', codigo_ncm: '61091000', icms_origem: 0, quantidade_comercial: 1, valor_unitario_comercial: 100.0 }],
  formas_pagamento: [{ forma_pagamento: '17' }],
}).then((nota) => console.log(nota.status, nota.chave_nfe));
```

## Python

Com a biblioteca `requests` (`pip install requests`):

```python
import os
import time
import requests

BASE = os.environ['EMISSOR_URL']
CABECALHOS = {'Authorization': f"Bearer {os.environ['EMISSOR_CHAVE']}", 'Accept': 'application/json'}
FINAIS = {'autorizado', 'cancelado', 'denegado', 'inutilizado', 'erro_autorizacao', 'erro_validacao'}


def chamar(metodo, caminho, corpo=None):
    for tentativa in range(7):
        try:
            r = requests.request(metodo, BASE + caminho, json=corpo, headers=CABECALHOS, timeout=30)
        except requests.RequestException:
            r = None  # falha de rede ou tempo esgotado
        # Falha de rede, 5xx ou 429: repete com espera crescente (seguro: a ref é idempotente)
        if (r is None or r.status_code >= 500 or r.status_code == 429) and tentativa < 6:
            retry_after = r.headers.get('Retry-After') if r is not None else None
            time.sleep(int(retry_after) if retry_after else min(60, 2 ** tentativa))
            continue
        if r is None:
            raise RuntimeError('sem resposta da API')
        return r.status_code, r.json()


def emitir(ref, venda, espera_maxima=120):
    status, nota = chamar('POST', f'/nfe?ref={ref}', venda)
    if status in (422, 409):
        raise RuntimeError(f"{nota['codigo']}: {nota['mensagem']}")  # corrija e reenvie: não adianta repetir igual

    limite, espera = time.time() + espera_maxima, 2
    while nota['status'] not in FINAIS and time.time() < limite:
        time.sleep(espera)
        espera = min(espera * 2, 15)
        _, nota = chamar('GET', f"/nfe/{ref}?cnpj_emitente={venda['cnpj_emitente']}")
    return nota


nota = emitir('PEDIDO-1001', {
    'cnpj_emitente': '11222333000181',
    'nome_destinatario': 'Maria da Silva', 'cpf_destinatario': '529.982.247-25',
    'logradouro_destinatario': 'Av Atlantica', 'numero_destinatario': '500', 'bairro_destinatario': 'Copacabana',
    'municipio_destinatario': 'Rio de Janeiro', 'codigo_municipio_destinatario': '3304557', 'uf_destinatario': 'RJ', 'cep_destinatario': '22010-000',
    'items': [{'codigo_produto': 'CAMISA-P', 'descricao': 'Camiseta basica P', 'codigo_ncm': '61091000', 'icms_origem': 0,
               'quantidade_comercial': 1, 'valor_unitario_comercial': 100.00}],
    'formas_pagamento': [{'forma_pagamento': '17'}],
})
print(nota['status'], nota.get('chave_nfe'))
```

## Baixar o XML autorizado

Com a nota `autorizado`, use o link devolvido em `caminho_xml_nota_fiscal` (ele exige a mesma chave de API). Guarde o arquivo: é o documento fiscal.

```bash
curl -s -o "NFe-PEDIDO-1001.xml" "$EMISSOR_URL/nfe/PEDIDO-1001/xml?cnpj_emitente=11222333000181" -H "Authorization: Bearer $EMISSOR_CHAVE"
curl -s -o "DANFE-PEDIDO-1001.pdf" "$EMISSOR_URL/nfe/PEDIDO-1001/danfe?cnpj_emitente=11222333000181" -H "Authorization: Bearer $EMISSOR_CHAVE"
```

## Cancelar e corrigir

```bash
# cancelar (nota autorizada, dentro do prazo)
curl -s -X DELETE "$EMISSOR_URL/nfe/PEDIDO-1001?cnpj_emitente=11222333000181" -H "Authorization: Bearer $EMISSOR_CHAVE" -H "Content-Type: application/json" \
  -d '{"justificativa": "Cliente desistiu da compra antes do envio", "confirmo": true}'

# carta de correção
curl -s -X POST "$EMISSOR_URL/nfe/PEDIDO-1001/carta_correcao?cnpj_emitente=11222333000181" -H "Authorization: Bearer $EMISSOR_CHAVE" -H "Content-Type: application/json" \
  -d '{"correcao": "Correcao do complemento do endereco de entrega para Apto 12"}'
```

## Receber webhooks

Os receptores prontos (PHP, Node.js e Python) com a conferência da assinatura estão em [Webhooks](08-webhooks.md#confira-a-assinatura-sempre).

---

# Referência de endpoints

Todos os caminhos são relativos à base `https://srv1839254.hstgr.cloud/api/v2` e exigem a [chave de API](02-autenticacao.md) (a exceção é `GET /openapi.json`). O contrato completo, com todos os campos e esquemas, é o [`/api/v2/openapi.json`](/api/v2/openapi.json) (OpenAPI 3.1).

Nas rotas de nota, `{ref}` é a `ref` que **você** deu à venda. A empresa é informada por `cnpj_emitente` (no corpo ou na query) e pode ser omitida quando a conta tem uma só. Nas rotas de empresa, `{cnpj}` aceita máscara.

## Empresas

| Método | Caminho | O que faz | Sucesso | Detalhes |
|---|---|---|---|---|
| `GET` | `/empresas` | Lista as empresas da conta | `200` | [Empresas](03-empresas.md) |
| `POST` | `/empresas` | Cadastra uma empresa (nasce em homologação) | `201` | [Cadastrar](03-empresas.md#cadastrar-uma-empresa) |
| `GET` | `/empresas/{cnpj}` | Detalha uma empresa | `200` | [Resposta da empresa](03-empresas.md#resposta-da-empresa) |
| `PATCH` | `/empresas/{cnpj}` | Atualiza parcialmente | `200` | [Atualizar](03-empresas.md#atualizar-uma-empresa) |
| `PUT` | `/empresas/{cnpj}/ambiente` | Troca homologação e produção | `200` | [Trocar de ambiente](03-empresas.md#trocar-de-ambiente) |
| `GET` | `/empresas/{cnpj}/series` | Séries da NF-e e o próximo número de cada uma | `200` | [Numeração](03-empresas.md#numeracao-da-nf-e-series-e-proximo-numero) |
| `PUT` | `/empresas/{cnpj}/series/{serie}` | Define o próximo número da série (só para a frente) | `200` | [Numeração](03-empresas.md#numeracao-da-nf-e-series-e-proximo-numero) |
| `GET` | `/empresas/{cnpj}/certificado` | Situação do certificado (nunca o arquivo) | `200` | [Certificado](03-empresas.md#certificado-digital-a1) |
| `PUT` | `/empresas/{cnpj}/certificado` | Envia o certificado A1 | `200` | [Certificado](03-empresas.md#certificado-digital-a1) |
| `DELETE` | `/empresas/{cnpj}/certificado` | Remove o certificado | `200` | [Certificado](03-empresas.md#certificado-digital-a1) |

## Produtos

| Método | Caminho | O que faz | Sucesso | Detalhes |
|---|---|---|---|---|
| `GET` | `/empresas/{cnpj}/produtos` | Lista o catálogo (paginado, filtro `q`) | `200` | [Listar](04-produtos.md#listar) |
| `POST` | `/empresas/{cnpj}/produtos/importacao` | Importa vários (tudo ou nada) | `200` | [Importar](04-produtos.md#importar-varios-de-uma-vez) |
| `GET` | `/empresas/{cnpj}/produtos/{sku}` | Detalha um produto | `200` | [Produtos](04-produtos.md) |
| `PUT` | `/empresas/{cnpj}/produtos/{sku}` | Cria ou substitui (idempotente) | `201` ou `200` | [Criar ou atualizar](04-produtos.md#criar-ou-atualizar-um-produto) |
| `DELETE` | `/empresas/{cnpj}/produtos/{sku}` | Remove | `204` | [Produtos](04-produtos.md) |

## NF-e

| Método | Caminho | O que faz | Sucesso | Detalhes |
|---|---|---|---|---|
| `POST` | `/nfe?ref={ref}` | Emite uma NF-e | `202` (ou `200` no reenvio igual, já em estado final) | [Emitir NF-e](05-emitir-nfe.md) |
| `GET` | `/nfe/{ref}` | Estado da nota, com chave, protocolo, links e eventos | `200` | [Consultar](06-eventos.md#consultar) |
| `DELETE` | `/nfe/{ref}` | Cancela a nota | `202` | [Cancelar](06-eventos.md#cancelar) |
| `POST` | `/nfe/{ref}/carta_correcao` | Carta de correção (CC-e) | `202` | [Carta de correção](06-eventos.md#carta-de-correcao-cc-e) |
| `GET` | `/nfe/{ref}/xml` | XML da nota autorizada | `200` (XML) | [XML e DANFE](06-eventos.md#xml-e-danfe) |
| `GET` | `/nfe/{ref}/danfe` | DANFE em PDF | `200` (PDF) | [XML e DANFE](06-eventos.md#xml-e-danfe) |
| `GET` | `/nfe/{ref}/eventos/{event}/xml` | XML de um evento registrado (`eventos[].id`) | `200` (XML) | [XML e DANFE](06-eventos.md#xml-e-danfe) |
| `POST` | `/nfe/inutilizacao` | Inutiliza uma faixa de numeração | `202` | [Inutilizar](06-eventos.md#inutilizar-numeracao) |
| `GET` | `/nfe/inutilizacoes` | Histórico de inutilizações da empresa | `200` | [Inutilizar](06-eventos.md#inutilizar-numeracao) |
| `GET` | `/nfe/inutilizacoes/{voiding}/xml` | XML de uma inutilização registrada | `200` (XML) | [Inutilizar](06-eventos.md#inutilizar-numeracao) |

## Webhooks

| Método | Caminho | O que faz | Sucesso | Detalhes |
|---|---|---|---|---|
| `GET` | `/webhooks` | Lista os endereços cadastrados | `200` | [Webhooks](08-webhooks.md) |
| `POST` | `/webhooks` | Cadastra um endereço (devolve o `segredo` uma única vez) | `201` | [Cadastrar](08-webhooks.md#cadastrar) |
| `GET` | `/webhooks/{webhook}` | Detalha (o segredo nunca volta) | `200` | [Webhooks](08-webhooks.md) |
| `PATCH` | `/webhooks/{webhook}` | Altera endereço, eventos ou ativação | `200` | [Webhooks](08-webhooks.md) |
| `DELETE` | `/webhooks/{webhook}` | Remove, com o histórico de entregas | `204` | [Webhooks](08-webhooks.md) |
| `POST` | `/webhooks/{webhook}/segredo` | Gera um segredo novo (o antigo morre na hora) | `200` | [Webhooks](08-webhooks.md) |
| `POST` | `/webhooks/{webhook}/teste` | Envia um evento `webhook.teste` | `202` | [Webhooks](08-webhooks.md) |
| `GET` | `/webhooks/{webhook}/entregas` | Histórico de entregas (paginado, filtro `status`) | `200` | [Histórico e reenvio](08-webhooks.md#historico-e-reenvio) |
| `POST` | `/webhooks/{webhook}/entregas/{delivery}/reenviar` | Reenvia uma entrega | `202` | [Histórico e reenvio](08-webhooks.md#historico-e-reenvio) |

## Operação

| Método | Caminho | O que faz | Sucesso | Detalhes |
|---|---|---|---|---|
| `GET` | `/requisicoes` | Log das chamadas da conta (só metadados, 30 dias) | `200` | [X-Request-Id e log](02-autenticacao.md#x-request-id-e-o-log-de-requisicoes) |
| `GET` | `/openapi.json` | Este contrato, público e sem chave | `200` | [Comece aqui](index.md) |

## Cabeçalhos

| Cabeçalho | Sentido | Para quê |
|---|---|---|
| `Authorization: Bearer nfe_...` | envio | a chave de API (ou HTTP Basic com a chave como usuário) |
| `Content-Type: application/json` | envio | em toda chamada com corpo JSON |
| `X-Request-Id` | resposta | identifica a chamada do nosso lado: registre e informe ao suporte |
| `X-RateLimit-Limit`, `X-RateLimit-Remaining` | resposta | o limite por minuto da chave e quanto resta |
| `Retry-After` | resposta (`429`) | segundos a esperar antes de repetir |
| `Allow` | resposta (`405`) | os métodos que a rota aceita |
| `X-Webhook-Id`, `X-Webhook-Event`, `X-Webhook-Timestamp`, `X-Webhook-Signature` | webhook recebido | veja [Webhooks](08-webhooks.md#confira-a-assinatura-sempre) |

## Estados da nota (`status`)

| Valor | Final? | Significado |
|---|---|---|
| `processando_autorizacao` | não | na fila ou aguardando a SEFAZ |
| `autorizado` | sim | autorizada: tem `chave_nfe`, protocolo, XML e DANFE |
| `erro_autorizacao` | não (reenviável) | a SEFAZ rejeitou: corrija e reenvie na mesma `ref` |
| `erro_validacao` | não (reenviável) | parou por uma pendência de cálculo ou cadastro antes da SEFAZ: corrija e reenvie na mesma `ref` |
| `denegado` | sim | a SEFAZ denegou o uso (problema fiscal do emitente ou do destinatário) |
| `cancelado` | sim | cancelada |
| `inutilizado` | sim | número inutilizado |

Detalhes e transições em [Conceitos](01-conceitos.md#estados-da-nota). Trate um valor desconhecido como "não final".

---

# Novidades e compatibilidade

A versão da API vai na URL (`/api/v2`) e no campo `info.version` do [contrato OpenAPI](/api/v2/openapi.json).

## Política de compatibilidade

Dentro da `v2` **só adicionamos**. Isto não quebra o seu cliente e pode acontecer a qualquer momento:

- campos novos em respostas e em eventos de webhook;
- endpoints novos e parâmetros opcionais novos;
- valores novos em enumerações (`status`, `evento`, códigos de erro);
- campos opcionais novos nas requisições.

Por isso o seu cliente deve **ignorar campos que não conhece**, tratar um `status` desconhecido como "não final" e programar contra o `codigo` do erro, não contra a `mensagem`.

Isto **não** acontece dentro da `v2`: remover ou renomear campo, mudar o tipo de um campo, mudar o sentido de um código HTTP, tornar obrigatório um campo que era opcional. Mudanças assim só entram em uma versão nova (`/api/v3`), com aviso prévio e a `v2` mantida em paralelo por um período de transição combinado com cada cliente.

Correções de bug que fazem a API cumprir o que está documentado não contam como quebra.

## 2.1.0

- **Numeração por série:** `GET /empresas/{cnpj}/series` lista as séries da empresa com o último e o próximo número; `PUT /empresas/{cnpj}/series/{serie}` define o próximo número (só para a frente), para quem continua a numeração de outro emissor. Erro novo: `422` `numero_retroativo`.
- **`serie` no corpo da nota:** opcional (0 a 889, padrão 1) em `POST /nfe`; a nota sai nessa série e consome o número dela.

## 2.0.0

Primeira versão publicada da API para ERPs.

- **Contas e chaves:** uma conta por ERP, várias chaves (rotação sem parada), várias empresas (CNPJs) por conta.
- **Empresas:** cadastro, atualização, certificado A1 e troca de ambiente pela própria API.
- **Produtos:** catálogo por empresa (SKU → NCM, origem...), com importação em lote.
- **NF-e:** emissão idempotente por `ref`, consulta, XML, DANFE, cancelamento, carta de correção e inutilização de numeração, com numeração em sequência por empresa e série.
- **Modo híbrido de tributação:** o ERP informa o que sabe (CFOP, situação tributária, alíquotas) e a plataforma calcula e preenche o resto.
- **Webhooks assinados** com HMAC-SHA256, repetição automática (9 tentativas em cerca de 21 horas) e histórico de entregas com reenvio.
- **Operação:** limites por minuto por chave, `X-Request-Id`, log de requisições e o contrato OpenAPI 3.1 público.

O que a plataforma ainda **não** cobre está em [Limitações atuais](10-producao.md#limitacoes-atuais).

## Como acompanhar mudanças

O contrato [`/api/v2/openapi.json`](/api/v2/openapi.json) acompanha sempre a versão em produção: compare-o com a cópia que o seu gerador de cliente usou para saber o que mudou. Dúvidas e pedidos de funcionalidade vão pelo mesmo canal de suporte que lhe entregou a chave de API.