# API Fiscal do Admin Panorama — CT-e (modelo 57) e MDF-e (modelo 58)

Documento **único e autossuficiente** para integrar um ERP externo à emissão de documentos fiscais de transporte do Admin Panorama. Escrito para ser lido por uma IA (agente de código) que vai implementar/manter o integrador.

- Fonte: código-fonte do `panoverse-backend` (não do `docs/api/swagger.json`, que está desatualizado — ver §12).
- Última verificação contra o código: **09/09/2026**.
- É esta API que o ERP da **Rodo TT** usa hoje em produção para emitir CT-e e MDF-e.

## Regras para a IA que ler este documento

1. **Use somente os campos listados aqui.** Campo que não está aqui é ignorado silenciosamente pela API (o parser lê chave a chave) — não "inove" nomes.
2. **A lista de endpoints da §3 é completa.** Não existe endpoint por simetria: não há `PUT`, não há `DELETE`, não há endpoint de consulta por `id_erp`, não há endpoint de NF-e (55).
3. **Tudo é assíncrono.** Nenhum POST devolve o documento autorizado. O POST grava + enfileira; jobs em background falam com a SEFAZ.
4. **A fonte da verdade do desfecho é o `GET` por chave**, nunca o corpo do webhook (§9).
5. Quando faltar informação aqui, **pergunte** — não deduza regra fiscal.

---

## 1. O que a API faz (e o que não faz)

| A API faz | O ERP externo faz |
|---|---|
| Numeração e série do documento | Manda os dados de negócio |
| Resolução de CFOP (estadual/interestadual) | Manda CEP/IBGE de início e fim |
| Cálculo de impostos, fechamento, geração e assinatura do XML | — |
| Transmissão à SEFAZ, retry, guarda do XML | Reconsulta o status quando avisado |
| Cadastro/atualização das pessoas (remetente, destinatário…) por CPF/CNPJ | Manda CPF/CNPJ + endereço |
| Eventos: cancelamento, CC-e, encerramento, inclusão de condutor | Dispara os eventos |

**Não existe hoje**: emissão de **NF-e (modelo 55)** por payload externo, inclusão de DF-e em MDF-e em viagem (evento 110115), tela de administração de MDF-e.

---

## 2. Base, autenticação e formato

**Base URL**: mesmo host do Admin do cliente (informado pela Panorama). Ex.: `https://{cliente}-admin.panoramasistemas.com.br`.

**Autenticação** — duas formas equivalentes; a **empresa emissora é derivada do usuário do token**:

| Forma | Como |
|---|---|
| Query string (a que o ERP da Rodo TT usa) | `POST /api/v1/faturamento/cte/?token=<TOKEN>` |
| HTTP Basic | header `Authorization: Basic base64(<login>:<TOKEN>)` |

- `TOKEN` = token do usuário de integração (`un_usuario.usu_token`), gerado no cadastro de usuário do Admin.
- Sem token válido: **HTTP 403** com corpo em texto puro `Invalid Access - Verify your credentials` (não é JSON).
- `Content-Type: application/json` em todo POST. Barra final no path é opcional (`/cte` == `/cte/`).

### Envelope de resposta (todas as rotas)

```json
{
  "status": "PROCESSADO",      // "PROCESSADO" | "ERRO"
  "success": true,             // booleano — cheque SEMPRE este campo
  "message": "texto humano",
  "codeError": 1,              // só quando houve erro (ver tabela abaixo)
  "...": "campos da operação (ou objeto data)"
}
```

| `codeError` | Significado |
|---|---|
| `1` | Dados inválidos / regra de negócio violada |
| `2` | Erro interno |
| `3` | Documento não localizado |
| `4` | Lote de CC-e inválido (só `POST /cte/cce`) |
| `5` | JSON inválido ou corpo vazio (só `POST /cte/cce`) |

### ⚠️ Quirk de HTTP status (importante para o tratamento de erro)

- `POST /cte` e `POST /cte/cancelamento`: **qualquer erro sai como HTTP 422**, mesmo "não encontrado" (que deveria ser 404) ou erro interno. Use `codeError` + `message` para classificar, não o status HTTP.
- Demais rotas (`GET`, `/cte/cce`, todas as de MDF-e) devolvem o status correto: 400, 401, 404, 422, 500.
- Regra geral segura: **`success === false` ⇒ erro**, independentemente do HTTP status.

---

## 3. Mapa completo de endpoints

| Documento | Verbo | Path | Efeito |
|---|---|---|---|
| CT-e | `POST` | `/api/v1/faturamento/cte` | Registra CT-e e enfileira autorização (idempotente por `cte_id_erp`) |
| CT-e | `GET` | `/api/v1/faturamento/cte/{chave}` | Estado consolidado do CT-e + suas CC-es |
| CT-e | `POST` | `/api/v1/faturamento/cte/cancelamento` | Enfileira cancelamento |
| CT-e | `POST` | `/api/v1/faturamento/cte/cce` | Enfileira 1..50 cartas de correção |
| MDF-e | `POST` | `/api/v1/faturamento/mdfe` | Registra MDF-e (idempotente por `mdfe_id_erp`) |
| MDF-e | `GET` | `/api/v1/faturamento/mdfe/{chave}` | Estado consolidado do MDF-e |
| MDF-e | `POST` | `/api/v1/faturamento/mdfe/encerramento` | Evento 110112 |
| MDF-e | `POST` | `/api/v1/faturamento/mdfe/cancelamento` | Evento 110111 |
| MDF-e | `POST` | `/api/v1/faturamento/mdfe/condutor` | Evento 110114 (inclusão de condutor) |

`{chave}` = chave de acesso de 44 dígitos (só dígitos; máscara é removida). Menos de 44 dígitos ⇒ 422.

---

## 4. CT-e — `POST /api/v1/faturamento/cte`

### 4.1 Payload — campos de topo

| Campo | Tipo | Obrig. | Regra |
|---|---|---|---|
| `cte_id_erp` | string | **sim** | ID do CT-e no ERP. Chave de idempotência, **unique por empresa**. Volta no webhook como `id`. |
| `data_emissao` | string ISO-8601 com offset | não (default: agora) | Ex.: `2026-09-09T14:32:00-03:00`. **Mande sempre a hora do envio**, não a de criação do registro: data atrasada leva à rejeição SEFAZ 228. Em reenvio de rejeitado, se o mês mudar o Admin regenera a chave. |
| `tipo_tomador` | int | sim (default 3) | `0` remetente, `1` expedidor, `2` recebedor, `3` destinatário, `4` outros |
| `tipo_servico` | int | sim | `0` normal, `1` subcontratação, `2` redespacho, `3` redespacho intermediário, `4` serviço vinculado a multimodal |
| `carga_lotacao` | bool | não (false) | |
| `reentrega` | bool | não (false) | |
| `processar_automatico` | bool | não (**true**) | `false` deixa o CT-e parado em DIGITADA (o job só pega `true`) |
| `calculo_imposto_pendente` | bool | não (true) | |
| `versao_payload` | int | não (1) | Gravado em `nf_operacao` para auditoria |
| `serie` | string/int | não | Se omitido: série padrão da configuração global; se a empresa tiver só uma série, usa ela; se tiver várias e não houver padrão ⇒ erro |
| `numero` | int | não | Se omitido, o Admin gera o próximo da série (com lock) |
| `chave` | string(44) | não | Normalmente **não enviar** — o Admin monta a chave |
| `trajeto` | objeto | sim (na prática) | Ver 4.2 |
| `carga` | objeto | sim | Ver 4.2 |
| `valores_prestacao` | objeto | sim | Ver 4.2 |
| `quantidades` | array | não | Ver 4.2 |
| `notas_referenciadas` | array | **sim**, exceto `tipo_servico` 3 ou 4 | Ver 4.2 |
| `emissor` | pessoa | **sim** | CNPJ tem de corresponder a uma Empresa cadastrada no Admin |
| `remetente` | pessoa | **sim** | |
| `expedidor` | pessoa | **sim** | Se o ERP não tem expedidor, repita o remetente |
| `destinatario` | pessoa | **sim** | |
| `tomador` | pessoa | só se `tipo_tomador = 4` | Nos demais casos é derivado (0→remetente, 1→expedidor, 2 e 3→destinatário) |

**Normalização automática do payload**: chaves com `-` viram `_` (`cpf-cnpj` == `cpf_cnpj`); string vazia em `complemento`, `referencia`, `nome_fantasia`, `inscricao_estadual` vira `null`.

### 4.2 Sub-objetos

**`trajeto`** — define o município de início/fim e, por consequência, o CFOP (estadual x interestadual):

| Campo | Tipo | Regra |
|---|---|---|
| `ibge_inicio` | string | Código IBGE do município de início (preferido) |
| `cep_inicio` | string | Fallback quando não há IBGE. Fallback do fallback: `remetente.endereco.cep`, depois `expedidor.endereco.cep` |
| `ibge_fim` / `cep_fim` | string | Idem; fallback: `destinatario.endereco.cep`, depois `tomador.endereco.cep` |

Se nenhum dos caminhos resolver um município cadastrado ⇒ erro `Nao foi possivel identificar municipio de inicio/fim do CT-e.`

**`carga`**: `valor` (number, valor da carga), `produto_predominante` (string), `peso_bruto` (number, kg), `peso_liquido` (number, opc.), `quantidade` (int, opc.).

**`valores_prestacao`**: `valor_total_prestacao` (number — vira o valor total da nota), `valor_recebido` (number), `valor_total_frete`, `valor_total_produto`, `valor_total_desconto`, `valor_total_acrescimo` (opcionais, default 0), `componentes[]` = `{ "nome": string, "valor": number }` (cada componente vira um item de serviço `CTE-COMP-001`, `CTE-COMP-002`…).

**`quantidades[]`**: `{ "unidade": "KG", "quantidade": 230.5 }`. A unidade é casada por sigla e depois por nome; não encontrada ⇒ cai para `UNID`.
⚠️ **Se `unidade` for KG (ou KILO/KILOGRAMA/KILOGRAMAS), `quantidade` tem de ser igual a `carga.peso_bruto`** (tolerância 0,0001), senão 422.

**`notas_referenciadas[]`**: `{ "chave": "<44 dígitos>" }` (aceita também string pura ou `nforig_chave`). Precisa de **ao menos uma chave de NF-e válida** (44 dígitos, ano da chave ≥ 2005 e ≤ ano atual + 1), exceto quando `tipo_servico` for 3 ou 4. Chaves duplicadas são deduplicadas; inválidas são descartadas silenciosamente.

**Objeto pessoa** (`emissor`, `remetente`, `expedidor`, `destinatario`, `tomador`) — o Admin faz **upsert do cadastro de pessoa por CPF/CNPJ**:

| Campo | Tipo | Regra |
|---|---|---|
| `cpf_cnpj` | string | **obrigatório**, 11 ou 14 dígitos (máscara é removida) |
| `tipo` | int | `1` física, `2` jurídica. Se informado, tem de bater com o tamanho do documento (PF↔11, PJ↔14), senão 422. Se omitido, é inferido pelo tamanho |
| `nome` | string | Razão social / nome |
| `nome_fantasia` | string | Só PJ |
| `ie` (ou `inscricao_estadual`) | string | Só PJ |
| `contribuinte_icms` | bool | Só PJ, default `true` |
| `tipo_contribuinte`, `regime_tributacao` | int | Só PJ, opcionais |
| `rntrc` | string | **Só no `emissor`** — vai para o CT-e |
| `endereco` | objeto | Ver abaixo |
| `email`, `telefone` | string | Viram contatos (e-mail = tipo 1, telefone = tipo 4 comercial) |
| `contatos[]` | array | `{ tipo, valor\|contato, nome, whatsapp, setor }`. `tipo` aceita número (`1` e-mail, `2` celular, `3` residencial, `4` comercial, `5` skype) ou string (`email`, `celular`, `residencial`); qualquer outro valor cai em comercial |

**`endereco`**: `logradouro` (aceita alias `endereco`), `numero`, `bairro`, `cep` (só dígitos), `codigo_ibge` (alias `c_mun`), `complemento`, `referencia`, `nome_endereco`.
A cidade é resolvida por `codigo_ibge` e, na falta, por CEP (base DNE). Não resolvendo ⇒ erro `Nao foi possivel identificar cidade para <slot>.endereco.cep`.

### 4.3 Exemplo completo de requisição

```http
POST /api/v1/faturamento/cte/?token=SEU_TOKEN HTTP/1.1
Content-Type: application/json
```

```json
{
  "cte_id_erp": "CTE-2026-000123",
  "data_emissao": "2026-09-09T14:32:00-03:00",
  "tipo_tomador": 3,
  "tipo_servico": 0,
  "carga_lotacao": false,
  "processar_automatico": true,

  "trajeto": {
    "cep_inicio": "95012500",
    "ibge_inicio": "4305108",
    "cep_fim": "01310930",
    "ibge_fim": "3550308"
  },

  "carga": {
    "valor": 10000.25,
    "produto_predominante": "Moveis",
    "peso_bruto": 230.5
  },

  "valores_prestacao": {
    "valor_total_prestacao": 1500.75,
    "valor_recebido": 1500.75,
    "componentes": [
      { "nome": "Frete valor", "valor": 1200.5 },
      { "nome": "Pedagio", "valor": 300.25 }
    ]
  },

  "quantidades": [
    { "unidade": "KG", "quantidade": 230.5 }
  ],

  "notas_referenciadas": [
    { "chave": "43260512345678000199550010000001231000001234" }
  ],

  "emissor": {
    "tipo": 2,
    "cpf_cnpj": "48259736000377",
    "nome": "RODO TT TRANSPORTES LTDA",
    "inscricao_estadual": "1234567890",
    "rntrc": "55410277",
    "endereco": {
      "logradouro": "Rua das Industrias",
      "numero": "1500",
      "bairro": "Distrito Industrial",
      "cep": "95012500",
      "codigo_ibge": "4305108"
    }
  },

  "remetente": {
    "tipo": 2,
    "cpf_cnpj": "12345678000199",
    "nome": "INDUSTRIA EXEMPLO LTDA",
    "inscricao_estadual": "9876543210",
    "endereco": {
      "logradouro": "Av. Industrial",
      "numero": "200",
      "bairro": "Centro",
      "cep": "95012500",
      "codigo_ibge": "4305108"
    }
  },

  "expedidor": {
    "tipo": 2,
    "cpf_cnpj": "12345678000199",
    "nome": "INDUSTRIA EXEMPLO LTDA",
    "endereco": {
      "logradouro": "Av. Industrial",
      "numero": "200",
      "bairro": "Centro",
      "cep": "95012500",
      "codigo_ibge": "4305108"
    }
  },

  "destinatario": {
    "tipo": 2,
    "cpf_cnpj": "98765432000188",
    "nome": "LOJA DESTINO SA",
    "inscricao_estadual": "1112223334",
    "endereco": {
      "logradouro": "Av. Paulista",
      "numero": "1000",
      "bairro": "Bela Vista",
      "cep": "01310930",
      "codigo_ibge": "3550308"
    }
  }
}
```

### 4.4 Resposta do POST (HTTP 200)

```json
{
  "status": "PROCESSADO",
  "success": true,
  "message": "CT-e recebido com sucesso. Processamento (impostos, fechamento e autorizacao) sera realizado pelo job em background.",
  "notaFiscalId": 123456,
  "async": true,
  "status": "DIGITADA",
  "processingMode": "job",
  "processedAt": "2026-09-09T14:32:01-03:00",
  "processingMs": 182
}
```

Campos:

| Campo | Descrição |
|---|---|
| `notaFiscalId` | `nf_id` no Admin — guarde para correlação/suporte |
| `async` | `true` quando entrou (ou voltou) para a fila |
| `status` | Situação atual: `DIGITADA`, `AUTORIZADA`, `AUTORIZACAO_REJEITADA`, `DENEGADA`, `CANCELADA`, `CANCELAMENTO_REJEITADO` |
| `processingMode` | `job` = novo, entrou na fila · `corrigido` = rejeitado atualizado e reenfileirado · `idempotent` = já existia, nada reprocessado |
| `processedAt`, `processingMs` | Telemetria da requisição |

### 4.5 Idempotência e reenvio (comportamento crítico)

`cte_id_erp` tem unique constraint. Ao reenviar o mesmo `cte_id_erp`:

| Situação atual do CT-e | O que a API faz | `processingMode` |
|---|---|---|
| Não existe | Cria e enfileira | `job` |
| `AUTORIZACAO_REJEITADA` / `DENEGADA` | **Atualiza a nota com os dados novos** e reenfileira, preservando número/série (chave é regenerada se o mês mudou) | `corrigido` |
| `AUTORIZADA` | Não reprocessa; **redispara o webhook** `cte.emissao.autorizada` para o ERP reconsultar | `idempotent` |
| Qualquer outra (DIGITADA, em processamento…) | Devolve a nota existente sem alterar | `idempotent` |

⇒ **Corrigir um CT-e rejeitado = reenviar o mesmo `cte_id_erp` com o payload corrigido.** Não existe endpoint de "editar".

---

## 5. CT-e — `GET /api/v1/faturamento/cte/{chave}`

Estado consolidado. **É a fonte da verdade** do fluxo notify-to-reconsult.

```json
{
  "status": "PROCESSADO",
  "success": true,
  "data": {
    "data_evento": "2026-09-09 14:32:00",
    "nf_id": 123456,
    "chave_cte": "35260548259736000377570040000000101000001995",
    "numero": "10",
    "serie": "4",
    "situacao": "AUTORIZADA",
    "protocolo": "135260000123456",
    "cnpj_emissor": "48259736000377",
    "xml_url": "https://.../35260548259736000377570040000000101000001995.xml",
    "erro": null,
    "cces": []
  }
}
```

| Campo | Observação |
|---|---|
| `situacao` | `DIGITADA` \| `AUTORIZADA` \| `AUTORIZACAO_REJEITADA` \| `DENEGADA` \| `CANCELADA` \| `CANCELAMENTO_REJEITADO` |
| `numero` | Pode vir número ou string — normalize no cliente |
| `xml_url` | URL do XML no storage; `null` enquanto não houver XML |
| `erro` | `null` quando não há erro, senão `{ origem, motivo, data }` |
| `cces[]` | CC-es do CT-e (ver §6.2) |

**`erro` — de onde vem o motivo:**

| Situação | `origem` | Conteúdo de `motivo` |
|---|---|---|
| `AUTORIZACAO_REJEITADA`, `CANCELAMENTO_REJEITADO` | `SEFAZ` | xMotivo da SEFAZ, ex.: `Rejeicao 539: Duplicidade de CT-e` |
| `DENEGADA` | `SEFAZ` | Quando disponível (denegação nem sempre grava motivo ⇒ pode vir `null`) |
| `DIGITADA` travada | `PROCESSAMENTO` | Falha pré-SEFAZ (imposto/fechamento/XML). Após 5 tentativas o CT-e **fica** em DIGITADA e o `erro` avisa que não progride mais |

O **cStat numérico não é exposto** — só o texto (`motivo`). Se precisar tratar por código, faça parsing do texto ou peça a evolução da API.

Erros: `422` chave sem 44 dígitos · `404` chave não localizada.

---

## 6. CT-e — eventos

### 6.1 Cancelamento — `POST /api/v1/faturamento/cte/cancelamento`

```json
{
  "chave": "35260548259736000377570040000000101000001995",
  "justificativa": "Erro de digitacao no valor da prestacao do servico"
}
```

| Campo | Regra |
|---|---|
| `chave` (alias `chave_cte`) ou `nf_id` | Um dos dois identifica o CT-e |
| `justificativa` | **mínimo 15 caracteres** |
| `numero_justificativa` | Opcional, apenas registrado |

Pré-condições: situação `AUTORIZADA`, `FECHADA` ou `CANCELAMENTO_REJEITADO` (permite retry); não pode já estar cancelado nem com cancelamento em voo.

Resposta (HTTP 200):

```json
{
  "status": "PROCESSADO",
  "success": true,
  "message": "Cancelamento de CT-e enfileirado para processamento na SEFAZ. O status final sera consolidado pelo job da fila SEFAZ.",
  "notaFiscalId": 123456,
  "async": true,
  "cancellationStatus": "EM_CANCELAMENTO",
  "queueStatus": "PENDENTE",
  "finalStatusSource": "sefaz_queue_job",
  "processedAt": "2026-09-09T14:40:00-03:00",
  "processingMs": 95
}
```

Desfecho: `CANCELADA` ou `CANCELAMENTO_REJEITADO` — leia pelo `GET` após o webhook.

### 6.2 Carta de Correção (CC-e) — `POST /api/v1/faturamento/cte/cce`

Aceita **um objeto ou um array de até 50 itens**. Cada item vira um evento SEFAZ independente.

```json
[
  {
    "chave_cte": "35260548259736000377570040000000101000001995",
    "data_correcao": "2026-09-09T15:00:00-03:00",
    "correcoes": [
      {
        "grupo_alterado": "ide",
        "campo_alterado": "xObs",
        "valor_alterado": "Observacao corrigida"
      }
    ]
  }
]
```

| Campo | Regra |
|---|---|
| `chave_cte` | 44 dígitos **com dígito verificador válido** (a API recalcula o DV) |
| `correcoes[]` | ≥ 1 item; cada um exige `grupo_alterado`, `campo_alterado`, `valor_alterado` (grupo/campo conforme o manual do CT-e) |
| `data_correcao` | Opcional; se enviado, formato estrito `AAAA-MM-DDTHH:MM:SS±HH:MM` |

Pré-requisito: o CT-e tem de **existir e estar autorizado no Admin**. Numeração (`nro_cce`, 1..20) é automática — não envie.

Resposta (HTTP 202):

```json
{
  "status": "PROCESSADO",
  "success": true,
  "message": "CC-e recebida. 1 operacao(oes) enfileirada(s) no SEFAZ para processamento assincrono.",
  "data": {
    "total_recebido": 1,
    "total_enfileirado": 1,
    "total_rejeitado": 0,
    "processado_em": "2026-09-09T15:00:01-03:00",
    "processing_ms": 120,
    "processados": [
      { "indice": 0, "chave_cte": "352605...", "status": "EM_FILA", "operacao_sefaz_id": 98765, "nro_cce": 1 }
    ],
    "rejeitados": []
  }
}
```

- `status` do item: `EM_FILA` (criada) ou `JA_ENFILEIRADO` (já havia uma CC-e desse CT-e em voo — devolve o `operacao_sefaz_id` existente, não duplica).
- Motivos de rejeição por item: `chave_invalida`, `duplicado_no_payload`, `correcoes_vazias`, `correcao_campos_invalidos`, `data_correcao_invalida`, `cte_nao_localizado`, `cte_nao_autorizado`, `documento_nao_e_cte`, `erro_geracao_cce`.
- Nenhum item válido ⇒ HTTP 422 com `data.rejeitados`. Lote > 50 ⇒ HTTP 422.

**Correlação**: guarde `operacao_sefaz_id`. É por ele que você reconcilia com o array `cces[]` do `GET /cte/{chave}`:

```json
{
  "operacao_sefaz_id": 98765,
  "nro_cce": 1,
  "situacao": "AUTORIZADA",     // PROCESSANDO | AUTORIZADA | REJEITADA
  "protocolo": "135260000999",
  "motivo": null,                // preenchido só em REJEITADA
  "data_evento": "2026-09-09 15:02:11",
  "xml_url": "https://.../evento.xml"
}
```

---

## 7. MDF-e — `POST /api/v1/faturamento/mdfe`

### 7.1 Payload — campos de topo

| Campo | Tipo | Obrig. | Regra |
|---|---|---|---|
| `mdfe_id_erp` (alias `id_erp`) | string | não, **mas recomendado** | Chave de idempotência, unique por `(empresa, id_erp)` |
| `emissor.cpf_cnpj` | string | não | Define a empresa emissora. Omitido, usa a empresa do usuário do token. Nenhuma das duas resolve ⇒ **401** |
| `uf_inicial`, `uf_final` | string(2) | **sim** | |
| `tipo_emitente` | int | **sim** | `tpEmit` do manual MDF-e (1 = prestador de serviço de transporte, 2 = carga própria, 3 = CT-e globalizado) |
| `tipo_transportador` | int | não | `tpTransp` (1 ETC, 2 TAC, 3 CTC) |
| `produto_predominante` | string | **sim** | |
| `tipo_carga` | int | **sim** | `tpCarga`; enviado à SEFAZ com 2 dígitos (`5` → `05`) |
| `valor_total_carga` | number/string | **sim** | |
| `quantidade_carga` | number/string | **sim** | |
| `antt` | objeto | **sim** | Exige `antt.rntrc` |
| `veiculos[]` | array | **sim** | ≥ 1 |
| `condutores[]` | array | **sim** | ≥ 1 |
| `ctes[]` | array | **sim** | ≥ 1 |
| `municipios[]` | array | **sim** | ≥ 1 e **ao menos um com `tipo = 2` (descarregamento)** |
| `processar_automatico` | bool | não (**false**) | ⚠️ **Default é `false`** — sem `true` o MDF-e fica parado em DIGITADO. Enviar `true`. |
| `ambiente` | int | não | `1` produção, `2` homologação. Sem isso usa a config da empresa; se nenhuma existir ⇒ 422 |
| `numero` + `serie` | int + string | não | Se omitidos, usa a série padrão de MDF-e da configuração |
| `data_emissao` | string ISO | não (agora) | |
| `modal` | int | não (`1` rodoviário) | |
| `tipo_emissao` | int | não | |
| `codigo_unidade_carga` | string | não | |
| `versao_processo` | string | não (`3.00.00`) | |
| `metadados` | objeto livre | não | Guardado como JSON |
| `percurso[]`, `seguro`, `contratantes[]`, `vales_pedagio[]` | ver 7.2 | não | |

### 7.2 Sub-objetos

**`antt`**: `rntrc` (obrigatório), `categoria_combinacao_veicular` (`categCombVeic`), `ciot` = `{ codigo (até 12 díg.), cpf_cnpj_responsavel (11 ou 14 díg.) }`. O CIOT é gerado fora (IPEF/provedor) e chega pronto; informar `codigo` sem responsável válido ⇒ 422.

**`veiculos[]`**:

| Campo | Obrig. | Regra |
|---|---|---|
| `placa` | sim | |
| `uf` | sim | |
| `tipo` | não (`1`) | `1` tração, `2` reboque |
| `tara` | **sim** | inteiro em kg, > 0 (schema SEFAZ) |
| `tipo_rodado` | **sim p/ tração** | `tpRod` |
| `tipo_carroceria` | **sim** | `tpCar` |
| `renavam`, `codigo_interno` (`cInt`), `capacidade_kg`, `capacidade_m3` | não | |

**`condutores[]`**: `{ "cpf": "12345678901", "nome": "..." }` — todos são vinculados ao **primeiro veículo de tração** da lista.

**`municipios[]`**: `{ "tipo": 1|2, "codigo_ibge": 4305108, "ordem": 0 }` (aceita `cid_id` no lugar do IBGE). `tipo`: `1` carregamento, `2` descarregamento. Município repetido no mesmo `tipo` é descartado (a SEFAZ rejeita duplicidade). IBGE não cadastrado ⇒ 422.

**`percurso[]`**: `["SC", "PR"]` ou `[{ "uf": "SC" }]`. UF igual à inicial ou final é **descartada** automaticamente (a SEFAZ rejeita); viagem intraestadual ⇒ percurso vazio.

**`ctes[]`**:

| Campo | Regra |
|---|---|
| `chave` | **44 dígitos** — obrigatório |
| `nf_id` | Opcional; se informado, o CT-e no Admin precisa estar **AUTORIZADO**, senão 422 |
| `reentrega` | bool, opcional |
| `municipio_descarga_codigo_ibge` (aliases `codigo_ibge_descarga`, `codigo_ibge`) ou `municipio_descarga_id` | Opcional. Se omitido, assume o **primeiro município de descarregamento** do MDF-e. Se informado, tem de existir entre os `municipios` de descarga, senão 422 |
| `unidades_transporte[]` | `{ tipo, identificador, qtd_rat, unidades_carga[] { tipo, identificador, qtd_rat } }` |

**`contratantes[]`**: `{ "cnpj": "..." }` ou `{ "cpf": "..." }` ou `{ "cpf_cnpj": "..." }` (separado por tamanho: 14 = CNPJ, 11 = CPF) + `nome` opcional. Aceita objeto único no lugar do array.

**`vales_pedagio[]`**: `cnpj_fornecedor`, `cnpj_pagador` ou `cpf_pagador`, `numero` (alias `numero_compra`), `valor`, `tipo`.

**`seguro`** (objeto ou array): `tipo_responsavel` (`1` emitente, `2` tomador), `documento_responsavel` (aliases `cnpj_responsavel`, `cpf_responsavel`, `documento_resp`, `cnpj_resp`, `cpf_resp` — se ausente e responsável = emitente, usa o CNPJ da empresa), `nome_seguradora`, `cnpj_seguradora`, `numero_apolice`, `averbacoes[]` (strings ou `{ "numero": "..." }`).

### 7.3 Exemplo de requisição

```json
{
  "mdfe_id_erp": "MDFE-2026-000045",
  "processar_automatico": true,
  "uf_inicial": "RS",
  "uf_final": "SP",
  "tipo_emitente": 1,
  "tipo_transportador": 1,
  "produto_predominante": "Moveis",
  "tipo_carga": 5,
  "valor_total_carga": 150000.00,
  "quantidade_carga": 12500.5,

  "antt": {
    "rntrc": "55410277",
    "categoria_combinacao_veicular": "02",
    "ciot": { "codigo": "123456789012", "cpf_cnpj_responsavel": "48259736000377" }
  },

  "veiculos": [
    { "placa": "ABC1D23", "uf": "RS", "tipo": 1, "tara": 8500, "capacidade_kg": 25000,
      "tipo_rodado": "06", "tipo_carroceria": "02", "renavam": "12345678901" },
    { "placa": "XYZ4E56", "uf": "RS", "tipo": 2, "tara": 5000, "capacidade_kg": 20000,
      "tipo_carroceria": "02" }
  ],

  "condutores": [
    { "cpf": "12345678901", "nome": "JOAO DA SILVA" }
  ],

  "municipios": [
    { "tipo": 1, "codigo_ibge": 4305108, "ordem": 0 },
    { "tipo": 2, "codigo_ibge": 3550308, "ordem": 1 }
  ],

  "percurso": ["SC", "PR"],

  "ctes": [
    { "chave": "35260548259736000377570040000000101000001995",
      "municipio_descarga_codigo_ibge": 3550308 }
  ],

  "seguro": {
    "tipo_responsavel": 1,
    "nome_seguradora": "SEGURADORA EXEMPLO",
    "cnpj_seguradora": "11222333000144",
    "numero_apolice": "AP-998877",
    "averbacoes": ["123456789"]
  },

  "vales_pedagio": [
    { "cnpj_fornecedor": "22333444000155", "numero": "VP-0001", "valor": 350.00 }
  ]
}
```

### 7.4 Respostas

**HTTP 201** (novo):

```json
{
  "status": "PROCESSADO",
  "success": true,
  "message": "MDF-e recebido com sucesso.",
  "data": {
    "mdfe_id": 987654,
    "nf_id": 987654,
    "situacao": 1,
    "processar_automatico": true,
    "processamento": "AGUARDANDO_JOB_PROCESSAMENTO_DIGITADO",
    "processing_ms": 240
  }
}
```

**HTTP 200** (idempotente): mesmo formato com `"processamento": "IDEMPOTENTE"` e `message: "MDF-e ja recebido anteriormente."`.

`mdfe_id` == `nf_id` (o MDF-e é satélite 1:1 da nota). `situacao` aqui é **numérica** (ver §7.6) — diferente do `GET`, que devolve o nome.

**Erros**: `400` JSON inválido · `401` empresa emissora não identificada · `422` validação · `500` interno.

### 7.5 Idempotência e reenvio

| Situação | Comportamento |
|---|---|
| `mdfe_id_erp` já existe e o MDF-e **não** está rejeitado | HTTP 200 `IDEMPOTENTE`, nada é recriado |
| `mdfe_id_erp` já existe e está `AUTORIZACAO_REJEITADA` sem operação SEFAZ em andamento | **Reprocessa a mesma nota**: limpa satélites, volta para DIGITADO e regrava tudo com o payload novo (histórico de XML/operações é preservado) |
| Sem `mdfe_id_erp`, mas com `numero`+`serie` já existentes | Mesma regra acima, casando por número+série |

### 7.6 Situações do MDF-e

| Código | Nome no `GET` | Significado |
|---|---|---|
| 1 | `DIGITADA` | Recebido, aguardando job |
| 2 | `EM_PROCESSAMENTO` | XML montado/assinado, enviado à fila SEFAZ |
| 3 | `AUTORIZADA` | Autorizado |
| 4 | `CANCELADA` | Cancelado |
| 5 | `DENEGADA` | Denegado |
| 6 | `AUTORIZACAO_REJEITADA` | Rejeitado na emissão (reenviável) |
| 8 | `CANCELAMENTO_REJEITADO` | Cancelamento rejeitado (permite retry) |
| 11 | `ENCERRADA` | Encerrado |

---

## 8. MDF-e — consulta e eventos

### 8.1 `GET /api/v1/faturamento/mdfe/{chave}`

```json
{
  "status": "PROCESSADO",
  "success": true,
  "data": {
    "nf_id": 987654,
    "chave_mdfe": "43260548259736000377580010000000451000000452",
    "numero": "45",
    "serie": "1",
    "situacao": "AUTORIZADA",
    "protocolo": "143260000123456",
    "cnpj_emissor": "48259736000377",
    "data_evento": "2026-09-09 08:00:00",
    "xml_url": "https://.../mdfe.xml",
    "encerrado": false,
    "data_encerramento": null
  }
}
```

### 8.2 Identificação do MDF-e nos eventos

Todos os POSTs de evento localizam o MDF-e por, nesta ordem: `mdfe_id`/`nf_id` → `chave` → `mdfe_id_erp`.
Usar `mdfe_id_erp` **exige** também `emissor.cpf_cnpj` (ou sessão autenticada), porque é escopado por empresa. Com `chave`, nada mais é necessário.

### 8.3 Encerramento — `POST /api/v1/faturamento/mdfe/encerramento` (evento 110112)

```json
{
  "chave": "43260548259736000377580010000000451000000452",
  "municipio_encerramento_codigo_ibge": 3550308,
  "data_encerramento": "2026-09-10T18:00:00-03:00"
}
```

- Município: `municipio_encerramento_codigo_ibge` (aliases `codigo_ibge_encerramento`, `codigo_ibge`) ou `cidade_encerramento_id` (aliases `municipio_encerramento_id`, `cidade_id`). Não localizado ⇒ 422.
- `data_encerramento` opcional (default: agora). A SEFAZ recebe **só a data** (`AAAA-MM-DD`).
- `ind_encerramento_por_terceiro` opcional.
- Exige MDF-e `AUTORIZADA`, sem encerramento pendente.
- Resposta **HTTP 202** com `data.encerramento_status = "EM_FILA"` e `data.operacao_id`.

### 8.4 Cancelamento — `POST /api/v1/faturamento/mdfe/cancelamento` (evento 110111)

```json
{
  "chave": "43260548259736000377580010000000451000000452",
  "justificativa": "Viagem cancelada por indisponibilidade do veiculo"
}
```

- `justificativa` (aliases `xJust`, `motivo`): **15 a 255 caracteres**.
- Exige `AUTORIZADA` (ou `CANCELAMENTO_REJEITADO` para retry), não encerrado/cancelado, sem cancelamento pendente.
- Resposta **HTTP 202** com `data.cancelamento_status = "EM_FILA"`.
- ⚠️ A SEFAZ tem **janela de tempo** para cancelar MDF-e e bloqueia se já houver documento transportado/encerramento — fora disso volta rejeição (visível pelo `GET`).

### 8.5 Inclusão de condutor — `POST /api/v1/faturamento/mdfe/condutor` (evento 110114)

```json
{
  "chave": "43260548259736000377580010000000451000000452",
  "condutor": { "cpf": "98765432100", "nome": "MARIA SOUZA" }
}
```

- `cpf` (11 dígitos) e `nome` obrigatórios; aceita também `cpf`/`nome` na raiz.
- Exige MDF-e `AUTORIZADA`, não encerrado, sem inclusão pendente.
- Resposta **HTTP 202** com `data.inclusao_condutor_status = "EM_FILA"`.
- O MDF-e **continua AUTORIZADA** e **não há webhook** para este evento nesta versão — se precisar confirmar, reconsulte.

---

## 9. Webhooks (padrão notify-to-reconsult)

O Admin dispara um `POST` para a URL configurada por empresa no Admin (Configurações → Webhook CT-e / Webhook MDF-e).

**Corpo do webhook (é só um sinal):**

```json
{ "id": "CTE-2026-000123", "chave": "352605..." }
```

- `id` = `cte_id_erp` / `mdfe_id_erp` (pode ser `null` se não foi informado); `chave` = chave de acesso.
- ⚠️ **O corpo NÃO contém o nome do evento nem o status.** O nome do evento existe só no log do servidor. Por isso o contrato é: recebeu webhook ⇒ **chame o `GET` da chave** e reconcilie a partir dali.
- CC-e é a exceção: o corpo ganha `nro_cce`, `protocolo` e, em rejeição, `motivo`.

**Eventos que disparam webhook** (nomes usados internamente):
`cte.emissao.autorizada|rejeitada|denegada`, `cte.cancelamento.autorizado|rejeitado`, `cte.cce.autorizada|rejeitada`, `mdfe.emissao.*`, `mdfe.cancelamento.*`, `mdfe.encerramento.*`.
Não há webhook para inclusão de condutor.

**Entrega**: timeout de 10s (conexão) + 10s (resposta). Falha é apenas logada — **não há retry automático garantido** para CT-e (há fila de retry no MDF-e). Responda `2xx` rápido e processe de forma assíncrona do seu lado.

**Fluxo recomendado no ERP:**

```
POST /cte  ──► 200 { notaFiscalId, status: DIGITADA, processingMode: job }
                    │
                    ├── webhook { id, chave }  ──► GET /cte/{chave}
                    │                                 ├── AUTORIZADA → grava protocolo + xml_url
                    │                                 ├── AUTORIZACAO_REJEITADA → mostra erro.motivo ao operador
                    │                                 └── DIGITADA + erro.origem=PROCESSAMENTO → travou, precisa correção
                    └── rede de segurança: polling do GET a cada N min para chaves sem desfecho
```

Como o webhook pode se perder (falha de rede, indisponibilidade), **mantenha um polling de segurança** do `GET` para documentos sem desfecho.

---

## 10. Pré-requisitos de cadastro no Admin (antes da primeira emissão)

Sem isso o POST responde 422/500 com mensagem explícita:

1. **Empresa** cadastrada com o CNPJ que vai no `emissor.cpf_cnpj` (CT-e) / usuário do token (MDF-e).
2. **Certificado digital** válido configurado para a empresa.
3. **Série** de nota cadastrada (CT-e: série padrão na configuração global de faturamento; MDF-e: série padrão em Configurações → MDF-e, vinculada à espécie 58).
4. **CFOP** estadual e interestadual definidos na configuração global de faturamento **e** uma `CFOPConfiguracao` marcada como padrão + ativa para cada um (CT-e).
5. **Configuração global de MDF-e** (inclui `ambiente`) para MDF-e.
6. **URL de webhook** configurada (opcional, mas é o que evita polling agressivo).
7. **Jobs habilitados** (`admin.ad_job`): processamento de CT-e digitada, fila SEFAZ CT-e, processamento de MDF-e digitado, fila SEFAZ MDF-e. Os de MDF-e nascem **desativados** na instalação.

---

## 11. Erros comuns e como reagir

| Sintoma | Causa | Ação |
|---|---|---|
| HTTP 403 texto puro | Token inválido/ausente | Conferir token e a query `?token=` |
| 422 `Campo obrigatorio invalido: emissor` | Objeto pessoa ausente | Enviar os 4 obrigatórios (emissor, remetente, expedidor, destinatário) |
| 422 `Documento incompatível…` | `tipo` (1/2) não bate com o tamanho do CPF/CNPJ | Corrigir `tipo` ou o documento; CNPJ com zeros à esquerda não pode ser truncado |
| 422 `Nao foi possivel identificar municipio de inicio/fim` | IBGE/CEP não resolvem cidade | Enviar `trajeto.ibge_inicio`/`ibge_fim` |
| 422 `Nao localizada configuracao de CFOP…` | Falta `CFOPConfiguracao` padrão/ativa | Cadastro no Admin |
| 422 `Quantidade em KG invalida` | `quantidades[KG].quantidade ≠ carga.peso_bruto` | Igualar os dois |
| 422 `Grupo Documentos Transportados…` | Sem NF-e válida em `notas_referenciadas` | Enviar chave de 44 dígitos válida (ou usar `tipo_servico` 3/4) |
| CT-e parado em `DIGITADA` com `erro.origem = PROCESSAMENTO` | Circuit breaker: 5 tentativas falharam antes da SEFAZ | Corrigir o dado e reenviar o mesmo `cte_id_erp` |
| MDF-e parado em `DIGITADA` para sempre | `processar_automatico` não veio `true` (default false) ou job desligado | Reenviar com `true` / ligar job |
| Rejeição SEFAZ 228 (data de emissão atrasada) | `data_emissao` antiga no reenvio | Mandar a **hora do envio** |
| Rejeição SEFAZ 539 (duplicidade) | Reenvio gerando chave repetida | Usar sempre o mesmo `cte_id_erp` (o Admin cuida da chave) |

---

## 12. Divergências conhecidas do `docs/api/swagger.json`

O swagger publicado está defasado. Onde divergir, **este documento vence**:

- Não documenta `cte_id_erp` (hoje **obrigatório** e base da idempotência) nem `trajeto.ibge_*`.
- Não documenta `GET /api/v1/faturamento/cte/{chave}` nem nada de MDF-e.
- Descreve a resposta do POST com `authorizationStatus` / `webhookStatus` — campos que **não existem mais**; hoje são `status` e `processingMode`.
- Fala em "status final informado via webhook"; o modelo real é **notify-to-reconsult** (webhook é só sinal).

---

## 13. Glossário

| Termo | Significado |
|---|---|
| CT-e | Conhecimento de Transporte Eletrônico, modelo 57 |
| MDF-e | Manifesto Eletrônico de Documentos Fiscais, modelo 58 |
| CC-e | Carta de Correção Eletrônica (evento 110110 do CT-e) |
| Chave | Chave de acesso de 44 dígitos, com dígito verificador |
| `nf_id` | ID interno do documento no Admin (CT-e e MDF-e reusam a entidade NotaFiscal) |
| Tomador | Quem contrata o frete (define quem paga o ICMS do serviço) |
| RNTRC | Registro Nacional de Transportadores Rodoviários de Carga |
| CIOT | Código Identificador da Operação de Transporte |
| Encerramento | Evento que fecha a viagem do MDF-e (obrigatório após a entrega) |
