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

# Changelog

> O que mudou na superfície pública da engineAPI, release a release.

## v2.22.0 (2026-09-12)

## 2026-09-12: Regra fiscal alterada não muda mais a sua emissão sem aviso

O EngineAPI espelha as tabelas oficiais de tributação (IBS/CBS) da fonte e as republica para o motor de emissão. Quando a fonte alterava a regra de um NCM — uma classe de tributação nova, uma redução diferente —, o espelho copiava a alteração no ciclo seguinte e o tratamento tributário do seu produto mudava no mesmo dia.

Agora, antes de gravar, o motor compara a publicação nova com a que está valendo e cruza o resultado com os NCMs que **você emitiu nos últimos 90 dias**. Se a alteração alcança algum deles, ela fica **retida**: a emissão continua com a regra anterior, e a mudança só entra depois de conferida e liberada por uma pessoa da plataforma.

Nada muda no seu código: nenhuma rota, nenhum campo e nenhum contrato foram alterados. O que muda é a garantia — uma reclassificação na fonte deixa de virar, sem aviso, um `422 TRIBUTACAO_NAO_RESOLVIDA` na sua próxima nota.

## 2026-09-12: a classe IBS/CBS fica no cadastro do produto

Quando o NCM aparece em mais de um anexo, a escolha de `cClassTrib` vale para o produto naquele emissor, não para cada nota. Informe a classe uma vez (no payload ou em `POST /v1/fiscal/classification/classe`); as próximas emissões reusam se a classe continuar entre as candidatas vigentes. Para listar as candidatas sem emitir: `POST /v1/fiscal/classes-por-ncm`. O `422 NCM_MULTICLASSE` passa a ter entrada própria no catálogo de erros.

## 2026-09-12: changelog fiscal por ciclo

Quando o espelho do Cérebro Fiscal muda a classe ou o percentual de um NCM, isso passa a ter página própria: o que mudou, o `422` na emissão assistida, e um registro manual de ciclo. Alerta automático e histórico gerado por ciclo ainda não existem. Para NCM em mais de um anexo da LC 214/2025, a documentação separa o que o produto é (anexo/item) de como a operação acontece (diferimento, adquirente).

## v2.21.0 (2026-09-11)

## 2026-09-11: espelho fiscal parado passa a alertar em vez de envelhecer calado

As tabelas do Cérebro Fiscal sincronizam por substituição integral, então toda sincronização bem-sucedida renova a data de atualização de todas as linhas. Quando uma delas falha, as guardas do motor preservam de propósito o espelho anterior — o dado não some, mas envelhece sem aviso. Agora um vigia confere diariamente a idade de cada espelho vindo do SAC e alerta acima de 48 horas, e o resumo de cada sincronização é lido na hora: tabela pulada ou com zero linhas vira alerta na mesma execução. `tax_rule_icms` fica de fora por não vir do SAC (tem base curada própria) e não gera alerta falso.

## 2026-09-11: login pelo painel volta a registrar o IP real de quem entrou

O painel autentica pelo servidor, não pelo navegador: quem abre a conexão com a API é o próprio painel. Por isso a auditoria registrava o endereço local em todo login feito pela tela — senha, Google e segundo fator —, e o alerta de login de superadmin saía sem dizer de onde. Agora o endereço do visitante é carregado adiante até a API, e `Parceiros → Últimos logins` volta a mostrar o IP verdadeiro. Quando não há origem confiável para ler, nada é inventado: fica registrado o endereço da conexão, nunca um valor escolhido por quem chamou. O bloqueio progressivo por tentativas erradas e o teto de tentativas por minuto também deixam de somar todos os usuários do painel no mesmo balde — antes um cliente podia ver "muitas tentativas" por causa do movimento dos outros.

## 2026-09-11: carta de correção pelo painel conta os 30 dias a partir da autorização

O prazo de 30 dias da Carta de Correção enviada pelo painel era medido pela data em que a nota foi criada no sistema, que pode ser bem anterior à autorização (reemissão, importação, fila). Agora conta a partir da autorização, como a regra fiscal e a própria mensagem do painel sempre disseram.

## 2026-09-11: itens da nota guardam quantidade e preço unitário com precisão exata

Quantidade e preço unitário dos itens passam a ser gravados como decimal exato, como já acontecia com os totais. O preço unitário não é arredondado: `50 × 0,015` continua `50 × 0,015`, e o item fecha com o total.

## 2026-09-11: cadastro do segundo fator no login voltou a ser legível

Na tela de cadastro do segundo fator, título, texto de apoio e chave manual apareciam em azul-marinho sobre azul-marinho. O contraste do título foi de 1,30:1 para 16,95:1.

## v2.20.0 (2026-09-09)

## 2026-09-09: Verificação em duas etapas, sessão curta e bloqueio de login

Contas com acesso privilegiado — superadmin e dono de parceiro — passam a exigir um aplicativo autenticador (TOTP de 6 dígitos) além da senha, com 10 códigos de recuperação de uso único. O `access_token` agora vale 12 horas e vem acompanhado de um `refresh_token` de 30 dias, rotacionado a cada uso; o logout revoga a sessão no servidor, e trocar a senha derruba todas as sessões abertas. Tentativas falhas de login bloqueiam progressivamente por e-mail e por IP, sempre com a mesma resposta `401 Credenciais inválidas`.

## 2026-09-09: O CSC da NFC-e é cifrado antes de ser guardado

O CSC que você envia em `POST /v1/companies` ou `PATCH /v1/companies/{id}` é cifrado (AES-256-GCM) antes de chegar ao banco, amarrado ao emissor: o mesmo valor guardado para outro emissor não decifra. Ele nunca volta em nenhuma resposta — o `GET` do emissor não traz o campo — e nunca aparece em log.

A partir desta versão, um emissor cujo CSC esteja guardado **fora** desse formato tem a emissão de NFC-e recusada com erro nomeado, em vez de seguir com o valor como está. Isso só alcança emissor cujo CSC tenha sido gravado por fora da API; quem cadastrou o CSC pelas rotas acima já está no formato cifrado e não precisa fazer nada. Se você receber essa recusa, reenvie o CSC por `PATCH /v1/companies/{id}`.

O mesmo vale para a senha do certificado A1 enviada em `POST /v1/companies/{id}/certificate`.

## 2026-09-09: Qual IP a EngineAPI registra das suas chamadas

Toda chamada autenticada entra na trilha de auditoria do seu parceiro com o endereço de origem, e é esse endereço que aparece no painel e nos alertas de segurança.

A API resolve o endereço a partir de `X-Engine-Client-IP`, `X-Forwarded-For` e `X-Real-IP`, mas **apenas quando esses cabeçalhos chegam pelo gateway da EngineAPI**. Se a sua integração enviar um desses cabeçalhos numa chamada direta, ele é descartado e vale o endereço real da conexão: não é possível escolher qual IP aparece na sua auditoria.

Se você chama a EngineAPI através de um proxy próprio (NAT, saída fixa, gateway corporativo), o endereço registrado é o de saída desse proxy, não o da máquina interna.

### O que dispara alerta de segurança

Estas situações avisam a equipe da EngineAPI na hora, mesmo quando a chamada é sua e legítima:

* 20 ou mais respostas `401`/`403` do mesmo endereço em 5 minutos (normalmente é chave errada ou expirada em ambiente automatizado);
* 3 ou mais cadastros criados do mesmo endereço em 1 hora;
* gravação do destino do webhook (`PATCH /v1/webhooks/config`), rotação do secret (`POST /v1/webhooks/secret/regenerate`) e regeneração de chave de API.

Nenhuma delas bloqueia a chamada. Se a sua esteira de testes costuma bater em `401` em série, vale usar a chave `ek_test_` e tratar o erro em vez de repetir a tentativa em laço.

## 2026-09-09: Política de conteúdo (CSP) do painel

Se você embute o painel da EngineAPI em um iframe, isso deixa de funcionar: `app.engineapi.com.br` passa a responder com `Content-Security-Policy: frame-ancestors 'none'` e `X-Frame-Options: DENY`. Não há como liberar por parceiro — o painel é uma superfície com sessão autenticada e enquadrá-la é o vetor clássico de clickjacking.

Integração continua sendo pela API (`api.engineapi.com.br`), que não mudou de contrato.

### HSTS em todos os hosts

Os hosts públicos da EngineAPI passam a mandar `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload`. Na prática: depois da primeira visita, o browser recusa `http://` para qualquer `*.engineapi.com.br`. Chamadas de servidor (SDK, curl, sua integração) não são afetadas — elas já usam `https://`.

## v2.19.0 (2026-09-06)

## 2026-09-06: Onde pego o secret do webhook

O secret HMAC (`whsec_...`) só aparece completo em dois momentos: no `PATCH /v1/webhooks/config` que cria a `webhookUrl` pela primeira vez, e em todo `POST /v1/webhooks/secret/regenerate`. As duas respostas trazem `revealedOnce: true`. Guarde o valor na hora — não há rota que o recupere depois; regenerar invalida o anterior e mostra um novo.

### Configuração e rotação do webhook exigem chave de produção

`PATCH /v1/webhooks/config`, `POST /v1/webhooks/secret/regenerate` e o retry/purge da Dead Letter Queue só aceitam o login do dashboard ou uma chave `ek_live_`. Uma chave `ek_test_` recebe `403 AMBIENTE_DE_TESTE_SEM_ESCRITA_WEBHOOK`: o secret HMAC é único por parceiro (não por ambiente), então uma integração de homologação rotacionando o secret invalidaria a assinatura que sua produção usa. Ler a configuração (`GET /config`), o histórico (`GET /logs`) e a DLQ (`GET /dlq`) continua liberado pra `ek_test_`.

## 2026-09-06: Resposta do webhook declarada no contrato

As respostas de configuração e rotação do secret do webhook agora têm schema público que declara `revealedOnce`.

## v2.18.0 (2026-09-02)

## 2026-09-02: cadastro por CNPJ evita consultas repetidas ao provedor

A primeira consulta busca os dados cadastrais e as seguintes usam o cache permanente da engineAPI. Refresh exige motivo, datasets extras declaram o crédito consumido e a rota de saldo mostra apenas o consumo mensal do parceiro autenticado.

## 2026-09-02: PATCH de empresa recusa mudar ambiente em silêncio

`PATCH /v1/companies/:id` com `sandbox` ou `ambienteFiscal` diferente do atual devolve `422 AMBIENTE_IMUTAVEL` e não escreve nada. Roundtrip do GET (valor igual) continua 200. Promover o emissor a SEFAZ real (`sandbox: false`) é `PATCH /v1/admin/companies/:id/sandbox` (SUPERADMIN), com certificado A1 válido; `ambienteFiscal` nessa rota não muda.

## v2.17.0 (2026-08-31)

## 2026-08-31: cadastro por CNPJ evita consultas repetidas ao provedor

A primeira consulta busca os dados cadastrais e as seguintes usam o cache permanente da engineAPI. Refresh exige motivo, datasets extras declaram o crédito consumido e a rota de saldo mostra apenas o consumo mensal do parceiro autenticado.

## 2026-08-31: numeração automática reconcilia CStat 539 em vez de devolver 400 cru

Antes, emitir sem `numero` com o contador atrasado (restore, migração ou teste antigo) ia à SEFAZ, queimava o número e voltava `400` com o texto cru do CStat 539. O integrador tinha que adivinhar um número alto.

Depois, o motor lê a chave conflitante na mensagem da SEFAZ, consulta a chave própria (não reemite às cegas), avança o contador e tenta de novo uma vez. Se a numeração era automática e o próximo número estiver livre, a emissão autoriza no mesmo request. Se não der, a resposta é `400` com `code: SEQUENCIA_DESSINCRONIZADA` e `details.informeNumeroMaiorQue` igual ao último nNF que a SEFAZ já tem nesta série — informe um `numero` maior que esse valor, ou omita `numero` e tente de novo. O webhook `invoice.rejected` dispara antes do 400, na fila e na NFC-e.

## v2.16.1 (2026-08-30)

## 2026-08-30: Certificado vencido ou de outro CNPJ na régua de "pronto para emitir NFS-e"

O #911 fez `prontoPara.nfse` cobrar a **presença** do A1. Faltava a validade
e a titularidade: certificado vencido (A1 dura 1 ano) ou emitido para outro
CNPJ ainda lia `"nfse": true`, o FalaNota mostrava o botão e a falha só
aparecia na assinatura da DPS.

`POST/GET/PATCH /v1/companies` agora responde `"nfse": false` com nome
próprio em `faltando.nfse`:

* `certificado` — ausente (já existia)
* `certificadoVencido` — `certExpiry` no passado
* `certificadoOutroCnpj` — raiz (8 dígitos) do A1 diferente da do emissor. Filial com e-CNPJ da matriz não entra aqui.

Se o parse da validade falhar, `avisos[]` traz `CERTIFICADO_VALIDADE_DESCONHECIDA` (aviso, não recusa).

A emissão recusa com `422 CERTIFICADO_VENCIDO` ou
`422 CERTIFICADO_OUTRO_CNPJ` antes de transmitir. Nos 30 dias anteriores ao
vencimento, `avisos[]` traz `CERTIFICADO_EXPIRANDO` — é aviso, não
reprovação. Até o instante de expirar o emissor continua pronto; se vencer
durante a ida à SEFIN, a recusa é do Fisco.

## 2026-08-30: Município não aderente no cadastro

`POST/GET/PATCH /v1/companies` passa a devolver `avisos[]` com
`MUNICIPIO_NAO_ADERENTE_PADRAO_NACIONAL` só quando a cobertura é
`nao_aderente` confirmada. `desconhecido` não gera aviso e o cadastro nunca
falha por cobertura. Na emissão de NFS-e o mesmo código recusa em 422 antes
de numerar, ou mapeia o E0037 da SEFIN preservando a mensagem original em
`details`.

## 2026-08-30: `avisos` no SDK TypeScript

O tipo `Company` do `@engineapi/sdk` marcava `avisos` como opcional
(`avisos?`), mas as respostas de `POST/GET/PATCH /v1/companies` sempre trazem
o array — vazio quando não há aviso. O tipo passa a refletir isso
(`avisos: AvisoCadastro[]`), então não é mais preciso tratar um `undefined`
que a API não devolve. Nenhuma mudança de comportamento na API.

## 2026-08-30: Certificado na régua de "pronto para emitir NFS-e"

A emissão de NFS-e assina a DPS com o certificado A1 do emissor, mas
`prontoPara.nfse` não olhava esse campo: emissor sem certificado lia
`"nfse": true` no cadastro e só descobria o problema no erro de assinatura,
depois de o número da DPS já ter sido alocado.

`POST/GET/PATCH /v1/companies` agora responde `"nfse": false` com
`"certificado"` em `faltando.nfse` sempre que o provider efetivo de NFS-e não
for o mock, na mesma régua já aplicada a `nfe`/`nfce`. Na emissão, o mesmo
cadastro recusa com `422 CERTIFICADO_AUSENTE` antes de transmitir — nenhum
nDPS é consumido. Emissor em **sandbox** continua emitindo sem certificado.

## v2.16.0 (2026-08-28)

## 2026-08-28: Campos fiscais que ainda não emitimos agora dizem o porquê

Ao enviar Imposto Seletivo, retenções federais da NF-e, o valor aproximado de tributos da Lei da Transparência ou os grupos avançados de IBS/CBS, a resposta explica que o recurso ainda não existe, em vez de dizer apenas que o campo não foi reconhecido. Se houver caminho equivalente em outro documento, a mensagem indica qual é.

## 2026-08-28: NCM de gênero e CEP opcional deixam de bloquear emissões válidas

Itens de NF-e e NFC-e agora aceitam NCM com 2 dígitos nas situações previstas pelo leiaute, além do código completo de 8 dígitos. Na NF-e, o CEP do destinatário pode ser omitido; quando informado, continua documentado como 8 dígitos sem máscara.

## 2026-08-28: Aviso de validação: finalidade e origem da mercadoria

Valores que o contrato aceitava, mas que o leiaute oficial da NF-e/NFC-e rejeita, agora são recusados antes da numeração do documento.

| Campo                         | Passa a recusar                         | Faixa aceita | Evidência do leiaute                                            |
| ----------------------------- | --------------------------------------- | ------------ | --------------------------------------------------------------- |
| `finNFe` (NF-e)               | inteiros menores que 1 ou maiores que 6 | 1 a 6        | enumeração oficial `TFinNFe` do leiaute 4.00                    |
| `items[].icms.origem` (NF-e)  | inteiros menores que 0 ou maiores que 8 | 0 a 8        | enumeração oficial `Torig` do leiaute 4.00                      |
| `items[].icms.origem` (NFC-e) | inteiros menores que 0 ou maiores que 8 | 0 a 8        | enumeração oficial `Torig`, compartilhada pelos modelos 55 e 65 |

O erro identifica o campo e a faixa aceita. Valores fora dessas enumerações não formam um XML válido para autorização pela SEFAZ.

## 2026-08-28: Tamanho e enumeração do leiaute recusam na entrada

A API de NF-e e NFC-e deixa de aceitar valores que o XSD do leiaute 4.00 já recusaria depois da numeração: descrição acima de 120 caracteres, unidade acima de 6, CFOP com primeiro dígito 4, CNPJ/CPF com 12 ou 13 dígitos, `tpNF` fora de 0/1, CST IBS/CBS com menos de 3 dígitos, entre outros. A recusa é 400, nomeia o campo e a faixa, e não consome número fiscal. CEP com hífen continua válido. Finalidade da nota (`finNFe`) e origem do ICMS não entram nesta mudança.

## 2026-08-28: pastor de auto-merge pode ser exercitado sob demanda

O pastor da `develop` deixou de depender de um merge alheio para rodar. O botão manual aceita `aviso` ou `ativo` só naquela execução; sem input, o comportamento de hoje (`PASTOR_ATIVO`) não muda.

## 2026-08-28: timeout de emissão passa a ser reconciliado pela chave de acesso

Quando a SEFAZ recebe o documento mas a resposta se perde, a engineAPI consulta a
chave persistida antes do envio. Documento já autorizado, cancelado ou rejeitado
tem o estado recuperado sem uma segunda transmissão; somente uma chave não
encontrada pode ser tentada novamente.

## 2026-08-28: Consumidor final derivado para não contribuinte

Em NF-e, `indFinal` continua opcional. Se vier omitido e o destinatário for não contribuinte, a API preenche 1. Declarar 0 contra esse destinatário agora falha em 422, com o número fiscal intacto.

## 2026-08-28: Cartão e PIX: informe `tpIntegra` antes de emitir

**Migração:** se você emite NF-e/NFC-e com forma `"03"`, `"04"` ou `"17"`, o payload agora precisa de `pagamentos[].cartao.tpIntegra` (`1` integrado ou `2` não integrado). Sem isso a API responde `422 PAGAMENTO_SEM_DADOS_DO_MEIO` **antes** de reservar número fiscal, no lugar da rejeição 391 da SEFAZ depois. PIX estático (chave copia-e-cola) usa `{ "tpIntegra": 2 }`; CNPJ da credenciadora, bandeira e autorização são opcionais.

## 2026-08-28: Estados de entrega de webhook

O histórico de webhooks informa os estados de reenvio persistente e a DLQ permite acompanhar falhas.

## 2026-08-28: status de entrega de webhook

O histórico de webhooks agora pode informar `retrying`, `processing` e `dead_letter`, além dos estados anteriores. Use a DLQ para acompanhar ou reagendar falhas persistentes.

## v2.15.0 (2026-08-22)

## 2026-08-22: NF-e deriva idDest do 1º dígito do CFOP

Antes, omitir `idDest` gravava sempre 1 (operação interna). A SEFAZ casa `idDest` com o 1º dígito do CFOP (B25a/B25b, rejeições 732/733), não com o par de UFs: CFOP 6102 sem `idDest` levava 732; derivar 2 da UF numa venda balcão 5102 levaria 733 e queimaria número. Agora a API deriva 1, 2 ou 3 do CFOP dos itens. Valor explícito vence sempre — o campo é declaratório. Itens mistos recusam 422 `CFOP_IDDEST_DIVERGENTE` nomeando os códigos, antes de reservar o número fiscal. A UF do emissor vem do cadastro da empresa. Na fila, o valor derivado entra só na chamada de emissão; o registro persistido guarda o que o parceiro mandou.

## 2026-08-22: sandbox sintético passa a emitir cNF válido no grupo ide

O XML sintético usado no emissor de demonstração passou a escrever `cNF` na segunda posição do `ide`, com 8 dígitos e diferente de `nNF`, como exige o leiaute. Com isso, a validação XSD do fixture do sandbox deixa de carregar o erro estrutural histórico e o teste agora cobra zero erro de schema.

## 2026-08-22: e-commerce B2C interestadual deixa de ser barrado por CEST

Venda interestadual a consumidor final não contribuinte (`indFinal=1` e `indIEDest=9`) passa a resolver CST 00 no fluxo assistido mesmo quando o NCM tem CEST. Substituição tributária não incide nessa operação: a cadeia encerra no consumidor final, e o Fisco de SP manda CST 00 (RC 24282/2021). Exceção: faturamento direto de veículo novo no escopo do Conv. 51/00 (`8429.59`, `8433.59`, `8702`, `8703`, `8704`, `8705` e `8711`, fontes Conv. 51/00 + Conv. 132/92 Anexo II + Conv. 52/93) continua em recusa de ST. Destinatário isento (`indIEDest=2`) não entra nessa porta. A partilha do DIFAL (grupo `items[].icms.ufDestino`) segue obrigatória.

## 2026-08-22: detector de ST ignora CEST de capítulo-inteiro do Anexo XXVI

NCM que só aparecia no CEST porque o Anexo XXVI cita o capítulo inteiro (ex.: 84224090 / 28.061.00 "Artigos de casa") deixa de ser recusado por substituição tributária, em qualquer canal. CEST específico de verdade — eletrônico 21.057.00, bebidas do segmento 17 — segue recusando. A cláusula sétima §1º do Convênio ICMS 142/18 manda: quando a descrição não reproduz a NCM, só a descrição decide. Risco residual: porta-a-porta legítimo sairia com CST 00; a mitigação (flag no Issuer, opção 2) segue na fila do #633.

## 2026-08-22: `/version` volta a reportar a tag do release em produção

Antes, o endpoint podia responder um `git describe` antigo (por exemplo `v2.11.0-39-g…`) mesmo com a tag real do release já publicada. Depois, produção injeta a tag semântica do commit em execução — a que aponta para ele, ou a primeira release que o contém.

## 2026-08-22: sync de CEST corrige paginação inicial do SAC

O provider do SAC passou a iniciar a leitura em `0` no endpoint paginado de CEST e ganhou teste de regressão cobrindo um fluxo com duas páginas (`0` e `1`), evitando retorno silencioso da perda de dados no espelho.

## 2026-08-22: Regime Normal: exceção do canal porta-a-porta por emissor

A documentação pública passou a explicar a exceção do Convênio ICMS 45/99 para revenda porta-a-porta: com `canalPortaAPorta=true` no emissor, os CESTs de capítulo-inteiro do segmento 28 voltam a bloquear a emissão automática por ST em NF-e e NFC-e (ex.: venda de balcão interna no modelo 65). Com a flag desligada, permanece a regra global que não conclui ST por NCM nesses capítulos; na tripla DIFAL estrita, a tripla segue prevalecendo.

## 2026-08-22: sync de NCM corrige paginação inicial da tabela de redução

O provider fiscal passou a iniciar em `page=0` a leitura paginada do NCM-Anexo-Reducao e ganhou teste de regressão cobrindo as páginas `0` e `1`, evitando perda silenciosa da primeira fatia de regras da fonte.

## 2026-08-22: nItem na devolução (DFeReferenciado)

A NT 2025.002-RTC exige referenciar por item na devolução: cada `det` leva `DFeReferenciado/chaveAcesso` e `nItem` (número do item na nota original). Informe `items[].documentoReferenciado.nItem` (e opcionalmente `chaveAcesso`; senão usa `referenciadas[0]`). Se a venda original saiu por esta conta e cada `cProd` é único na original, a API deriva o `nItem`. Caso contrário recusa 422 `DEVOLUCAO_SEM_NITEM` antes de consumir número fiscal.

## v2.14.0 (2026-08-15)

## 2026-08-15: preparação do release passa a caber em um comando

O carimbo de versão deixa de depender de três edições manuais. A preparação agora
valida o trem, consolida os fragmentos e garante que o primeiro título deste canal
seja a versão que o gate do release aceita.

## 2026-08-15: infraestrutura de release: back-merge automático

Todo merge na main agora volta pra develop automaticamente, mantendo a proteção
de regressão de hotfix sem passo manual. Sem efeito na superfície da API.

## 2026-08-15: changelog por fragmento evita conflito entre entregas paralelas

Cada entrega passa a registrar sua mudança num arquivo próprio em `changelog.d/`. No
release, os fragmentos são consolidados neste canal e no changelog do repositório antes
do carimbo de versão. Assim, PRs paralelos deixam de disputar o topo dos mesmos dois
arquivos.

## 2026-08-15: mensagens da guarda ICMS-ST e `code` de regime no CST 00

Três residuais da guarda de ICMS-ST, todos em 422 antes da numeração.

* **Payload misto.** Campo de ST (`modBCST`/`vBCST`/`pICMSST`/`vICMSST`) e ICMS próprio (`modBC`/`vBC`/`pICMS`/`vICMS`) no mesmo item, sem CST `10`/`30`/`70`, deixam de caber numa frase só. A ST aponta para os grupos de ST; o ICMS próprio aponta para os grupos em que o leiaute os declara (`10`/`20`/`51`/`70`/`90`).
* **Caminho assistido já ativo.** A recusa do CST `00` manual com `resolverTributacao: true` passa a dizer que o motor **passa a escrever** o grupo depois que você remove o `cst`. O enrichment pula item com CST manual: "escreve o grupo sozinho" era cedo demais.
* **⚠️ Contrato: CRT 1/4 com CST `00` e ICMS próprio.** Esse payload deixa de devolver `ICMS_ST_INVALIDO` e passa a devolver `CST_REGIME_INCOMPATIVEL` — o mesmo `code` do Simples com CST `10`/`30`/`70`. Quem roteia por `code` trata o caso como incompatibilidade de regime, não como forma do grupo de ST.

## 2026-08-15: formatos decimais fixos respeitam precisão e teto do leiaute

A validação numérica passa a usar as duas dimensões declaradas no dicionário:
casas decimais e quantidade máxima de dígitos inteiros. Na NF-e e na NFC-e,
campos `13v2` aceitam até 13 dígitos inteiros e 2 casas; na NFS-e, valores
`TSDec15V2` aceitam até 15 inteiros e 2 casas. Valor acima do teto devolve `400`
com o caminho e o valor recebido, antes de reservar número fiscal.

A seleção deixa de depender do prefixo monetário `v`. Por isso os pesos dos
volumes transportados na NF-e, `pesoLiquido` e `pesoBruto`, passam a seguir o
`12v3` do leiaute: no máximo 3 casas decimais, sem arredondamento silencioso.
Formatos variáveis, como o valor unitário `11v0-10`, conservam a faixa de casas
e passam a respeitar o teto de dígitos inteiros.

A tolerância de 4 ULPs continua absorvendo apenas resíduo de ponto flutuante na
comparação das casas. Ela não aumenta o teto. A borda `99999999999.99` e o teto
`9999999999999.99` do `TDec_1302` permanecem aceitos; a regressão percorre
20.002 valores válidos de centavo nas bordas para impedir falsa rejeição.

## v2.13.0 (2026-08-13)

## 2026-08-13: valores monetários de total recusam fração de centavo em vez de arredondar

Um valor como `1.005` num campo monetário de total (frete, desconto, seguro, valores de
ICMS/ST/DIFAL/benefícios, pagamentos, troco, cobrança) era escrito no documento como
`1.00`, um centavo abaixo do que o payload declarou, sem aviso. Antes: a validação
aceitava qualquer número e o documento saía com o valor arredondado. Depois: campo de
total com mais de 2 casas decimais devolve `400` nomeando o campo, o valor recebido e a
regra, antes de reservar número fiscal. Na NFS-e vale a mesma regra para
`servico.valorServicos` (que também passa a recusar valor negativo), descontos, deduções
e retenções.

Dois detalhes de compatibilidade:

* **Ruído de representação binária não recusa.** Resultado de conta em ponto flutuante,
  como `0.1 * 3` = `0.30000000000000004`, é aceito e escrito como `0.30`, em qualquer
  grandeza. A recusa é para frações reais de centavo, como `1.005` e `19.999`.
* **Campos unitários não mudam.** `items[].valorUnitario` continua aceitando até 10
  casas decimais, conforme o leiaute.

O contrato OpenAPI dos campos de duas casas da NFS-e deixou de publicar `multipleOf:
0.01`: validadores de cliente aplicam essa regra com divisão em ponto flutuante e
recusariam valores válidos como `4.35`. O teto de casas está na descrição de cada campo.

## 2026-08-13: o contrato do complemento do tomador afirma o teto e as mensagens ficam específicas

O campo `tomador.endereco.complemento` da NFS-e sempre teve teto de 156 caracteres,
mas o contrato OpenAPI publicado deixou de afirmá-lo quando o campo ganhou a
normalização de vazio, e um valor de tipo errado devolvia a mensagem genérica
`Invalid input`. Antes: o schema publicado trazia duas variantes do campo, uma sem
`maxLength`, e `complemento: 42` respondia sem dizer o tipo esperado. Depois: o
contrato publica o teto e a descrição no campo único, o tipo errado responde
nomeando campo e tipo (`esperado texto, recebido número`), e string composta só de
espaços **acima de 156 caracteres** passa a ser recusada com `400` em vez de virar
ausência (até 156, o comportamento de tratar como ausência continua igual).

## v2.12.1 (2026-08-13)

## 2026-08-13: a recusa do ICMS próprio manual diz a causa real, caso a caso

Informar `icms.cst: "00"` com os campos do ICMS próprio (`icms.modBC`, `icms.vBC`,
`icms.pICMS`, `icms.vICMS`) recebia uma única orientação: trocar para o CST `10`/`30`/`70`.
Para quem não cobra substituição tributária na operação, seguir a orientação mudaria a
declaração fiscal do documento. A recusa continua acontecendo antes de qualquer número
fiscal ser consumido, mas a resposta agora separa os casos:

* **Só campos do ICMS próprio com CST `00`**: o leiaute prevê os campos, e a engineAPI
  ainda não os escreve pelo caminho manual. A resposta manda remover os campos e aponta
  este changelog para acompanhar quando o cenário estiver coberto.
* **Qualquer campo de ST junto** (`icms.vBCST`, `icms.vICMSST`, ...): a orientação de
  informar CST `10`/`30`/`70` permanece, porque cobrar ST nesta operação é exatamente o
  que esses grupos declaram.
* **Emissor no Simples Nacional ou MEI**: a resposta lembra que o regime declara
  `icms.csosn`, não `icms.cst`.
* **`resolverTributacao: true` já enviado**: a resposta manda remover a declaração manual
  redundante; o caminho assistido escreve o grupo.

## 2026-08-12: validações de NFS-e passam a recusar valores que o documento não representa

O endereço do tomador agora trata `complemento` vazio como ausência: omitir, enviar `""`
ou enviar apenas espaços produz o mesmo documento sem complemento, e o catálogo público
o mostra como opcional.

Também ficaram mais estritos os valores que não poderiam chegar ao documento nacional.
Antes, `servico.aliquotaIss: 0.0250`, endereço com travessão ou `logradouro` somente com
espaços podiam atravessar a validação inicial e falhar tarde ou gerar documento inválido.
Depois, cada caso devolve `400` com o nome do campo antes de reservar número da DPS. A
alíquota aceita no máximo duas casas decimais e um dígito na parte inteira.

## v2.12.0 (2026-08-12)

## 2026-08-12: declare à vista ou a prazo no pagamento da NF-e

**`pagamentos[].indicador` entrou no contrato da NF-e**: `0` é à vista, `1` é a
prazo (a tag `indPag` do leiaute). O campo é opcional: sem ele, a tag continua
fora do documento, como antes. A exceção é deliberada: NF-e com `cobranca`
(fatura e duplicatas) passa a declarar `1` (a prazo) automaticamente, porque é o
que a venda a prazo é; se você enviar um valor, o seu vence. Antes: enviar
`indicador` era recusado com `400`; a NF-e de venda a prazo saía sem a condição
de pagamento declarada. Depois: o campo é aceito na NF-e (e continua fora da
NFC-e, que tem regra própria). Guia: **Venda a prazo**.

## v2.11.0 (2026-08-12)

## 2026-08-12: benefícios fiscais de ICMS emitem (redução de base, isenção, suspensão, diferimento e outras)

**Empresa com benefício estadual passa a emitir informando os números do próprio ato
concessório.** O contrato de `items[].icms` ganhou os códigos de situação tributária
`20` (redução de base), `40` (isenta), `41` (não tributada), `50` (suspensão), `51`
(diferimento) e `90` (outras), para emissor em Regime Normal (`crt: 3`) ou no Simples
com excesso de sublimite (`crt: 2`). Na NFC-e valem `20`, `40` e `41`: o leiaute do
modelo 65 tem lista fechada de códigos, e os outros três recusam com
`422 ICMS_BENEFICIO_NAO_SUPORTADO` mandando emitir NF-e.

Campos novos em `items[].icms`: `modBC`, `pRedBC`, `cBenefRBC`, o Fundo de Combate à
Pobreza (`vBCFCP`, `pFCP`, `vFCP`), a desoneração (`vICMSDeson`, `motDesICMS`,
`indDeduzDeson`) e o diferimento (`vICMSOp`, `pDif`, `vICMSDif`, `pFCPDif`, `vFCPDif`,
`vFCPEfet`). Fora do imposto, dois grupos novos: `items[].gCred` (crédito presumido
concedido pela UF, até 4 por item) e `procRef` na raiz do corpo (o processo ou ato
concessório que ampara o benefício, até 100 por nota).

Três coisas que valem a leitura antes de integrar:

* **A base do código `20` que você informa JÁ É a base reduzida.** A API não aplica a
  redução por você, e confere `valor` contra base vezes alíquota com tolerância de um
  centavo.
* **`indDeduzDeson` é obrigatório sempre que `vICMSDeson` é informado.** É ele que diz
  se o valor desonerado sai do total da nota, e o leiaute não define o que significa a
  ausência dele. Exigir é mais estrito que a norma, de propósito: com `"1"`, o total
  transmitido sai reduzido e os `pagamentos` precisam fechar com esse total.
* **Os motivos de desoneração mudam com o código.** Nos códigos `20` e `90` o leiaute
  admite `3`, `9` e `12`; nos `40`, `41` e `50` admite `1`, `3`, `4`, `5`, `6`, `7`,
  `8`, `9`, `10`, `11`, `16` e `90`. Motivo fora do conjunto recusa listando os que
  valem ali.

Bloco pela metade recusa com `422 ICMS_BENEFICIO_INCOMPLETO` e campo no código errado
com `422 ICMS_BENEFICIO_INVALIDO`, sempre **antes** de consumir número fiscal. O ICMS próprio destes grupos usa as tags do leiaute (`vBC`, `pICMS`, `vICMS`), o mesmo vocabulário da substituição tributária: os nomes clássicos seguem aceitos no ICMS sem benefício e no Simples, e junto de um código de benefício recusam, para o motor nunca escrever um nome e descartar o outro.

Três recusas
que valem a menção porque protegem o total da nota: uma nota **não** mistura
`indDeduzDeson` `"1"` com `"0"` (o total sairia parcialmente deduzido, e não há regra
confirmada para esse caso); item com `indTot: 0` não declara desoneração dedutora (sai do
valor de mercadoria e não pode reduzir o total); e `vICMSDeson` que arredonda para `0,00`
recusa, porque o grupo inteiro sumiria do documento em silêncio.

Quando a desoneração deduz do total, o valor registrado da nota sai reduzido junto com o
`vNF` transmitido: o que você lê no webhook e na consulta é o mesmo número que foi ao
documento.

O que **não** mudou: a API continua sem descobrir sozinha que a sua operação tem
benefício (o caminho assistido segue emitindo tributação integral) e sem conferir se o
código de benefício existe na tabela da sua UF. Dos códigos, conferimos o formato que o
leiaute exige. Detalhe completo no guia de benefícios de ICMS.

**Provado com emissão real autorizada em homologação** no `cst` `"20"` (protocolo
`152260027584995`), com o XML conferido tag a tag: redução de base de 60%, base de
R$ 40,00 e ICMS de R$ 7,20 no item, e o total do documento batendo com ele. Os demais
códigos (`40`, `41`, `50`, `51` e `90`) têm a estrutura validada contra o leiaute
oficial e ainda aguardam a emissão real.

⚠️ **Em Goiás, o `cBenef` é obrigatório no `cst` `"20"`**: sem ele a nota volta rejeitada
com a rejeição `930`, e o número fiscal já foi consumido. Informe o código da tabela de
benefícios da sua UF no item.

## 2026-08-12: substituição tributária cobrada na sua operação (CST 10, 30 e 70) emite na NF-e

**Quem RETÉM a ST agora emite.** Indústria, atacado e distribuição que recolhem o
imposto das etapas seguintes da cadeia usavam `icms.cst` `"10"`, `"30"` ou `"70"` e
recebiam `422 CST_NAO_SUPORTADO_NFE`. Agora os três emitem na **NF-e (modelo 55)**,
para emissor em Regime Normal (`crt: 3`) ou no Simples com excesso de sublimite
(`crt: 2`), pelo mesmo caminho que a revenda com ST já retida (`cst: "60"`) usa desde
a versão anterior: **passthrough**. Você informa os números, a engineAPI valida a
forma e transmite.

| Sua operação                    | `icms.cst` | Campos                                                                                          |
| ------------------------------- | ---------- | ----------------------------------------------------------------------------------------------- |
| Tributada, com ST               | `"10"`     | ICMS próprio (`modBC`, `vBC`, `pICMS`, `vICMS`) + ST (`modBCST`, `vBCST`, `pICMSST`, `vICMSST`) |
| Isenta ou não tributada, com ST | `"30"`     | Só a ST                                                                                         |
| Redução de base, com ST         | `"70"`     | Igual ao `"10"`, mais `pRedBC`                                                                  |

Opcionais: `pMVAST`, `pRedBCST` e os trios do Fundo de Combate à Pobreza
(`vBCFCP`/`pFCP`/`vFCP` e `vBCFCPST`/`pFCPST`/`vFCPST`), cada um inteiro ou ausente.

**A margem de valor agregado e a alíquota interna de ST não são estimadas** pela
engineAPI: elas variam por estado, produto e convênio, e chutar qualquer uma delas
seria inventar imposto em documento assinado. A base da ST também não é recalculada a
partir da margem.

<Warning>
  **A ST compõe o total da nota.** `vNF = produtos − desconto + frete + seguro + outras
      despesas + IPI + ICMS-ST + FCP-ST`. Numa venda de R$ 100,00 com R$ 7,20 de ST, o
  pagamento é de R\$ 107,20. Se `pagamentos` não fechar com esse total, a emissão recusa
  com `422 PAGAMENTO_DIVERGENTE` antes de consumir número fiscal.
</Warning>

O que é conferido antes de emitir: `vICMS` contra `vBC × pICMS` (tolerância de
R\$ 0,01), que é a única conta com regra de validação escrita no leiaute; bloco pela
metade (`422 ICMS_ST_INCOMPLETO`); e combinação que o documento não representa
(`422 ICMS_ST_INVALIDO`), como `pRedBC` no CST `"10"` (esse campo só existe no `"70"`),
ICMS próprio no CST `"30"`, modalidade fora da tabela, ou alíquota acima de 100%. O
valor da ST (`vICMSST`) **não** é conferido contra base vezes alíquota: não existe
regra escrita para essa conta, e checar o que a norma não afirma recusaria emissão
legítima.

Os campos antigos `icms.baseCalculoST`, `aliquotaST` e `valorST` nunca chegaram ao
documento e seguem recusados, agora apontando os nomes que emitem.

Continuam fora, com recusa nomeada: desoneração do ICMS e da ST (`vICMSDeson`,
`motDesICMS`, `vICMSSTDeson`), partilha entre estados (`ICMSPart`), repasse
interestadual de ST em combustível e os códigos de ST do Simples
(`csosn` `201`/`202`/`203`). Na **NFC-e** os três códigos de ST não existem no
leiaute: a emissão recusa antes de numerar e os campos devolvem `400`.

**Provado com emissão real autorizada em homologação** no `cst` `"10"`
(protocolo `152260027584436`), com o total do documento conferido tag a tag. Os
`cst` `"30"` e `"70"` têm a estrutura validada contra o leiaute oficial e ainda
aguardam a emissão real.

Detalhe completo: [Regime Normal](/guides/regime-normal) e
[Cobertura fiscal](/cobertura).

## 2026-08-12: e-commerce interestadual emite com a partilha do ICMS (DIFAL)

**A venda para outro estado, a consumidor final não contribuinte, passa a emitir com o
grupo `ICMSUFDest`**, para emissor **fora do Simples Nacional**. O Supremo, na ADI
5.464, suspendeu a exigência dessa partilha para os optantes do Simples (cobrá-la
depende de lei complementar), então emissor com `crt: 1` (Simples) ou `crt: 4` (MEI)
que informe o grupo recebe `422 DIFAL_NAO_APLICAVEL` antes de numerar; o caminho vale
para `crt: 3` (Regime Normal) e `crt: 2` (Simples com excesso de sublimite).

O contrato ganhou `items[].icms.ufDestino` na NF-e (modelo 55),
com os nomes do leiaute: `vBCUFDest`, `pICMSUFDest`, `pICMSInter`, `pICMSInterPart`,
`vICMSUFDest`, `vICMSUFRemet` e, quando o estado de destino cobra o adicional de Fundo
de Combate à Pobreza, `vBCFCPUFDest`, `pFCPUFDest` e `vFCPUFDest`. Os três somatórios do
total da nota saem calculados dos itens.

É **passthrough**: a engineAPI valida a forma e transmite, e **não calcula a partilha**.
A base de cálculo e a alíquota internas aplicáveis são as da legislação do estado de
DESTINO, que a engineAPI não tem curada. O que ela faz é recusar antes de consumir
número fiscal o que o documento não representaria: `422 DIFAL_NAO_APLICAVEL`
quando o grupo vem fora da hipótese de partilha (a operação precisa ser interestadual,
`idDest: 2`, a consumidor final, `indFinal: 1`, com destinatário não contribuinte,
`indicadorIE: 9` ou ausente), `422 DIFAL_INCOMPLETO` quando o grupo vem pela metade (os
seis campos obrigatórios e o bloco de FCP vão inteiros ou não vão) e
`422 DIFAL_INVALIDO` quando o número é incoerente: alíquota interestadual fora de
4/7/12, percentual de partilha diferente de 100 (integral para o destino desde 2019),
parcela do remetente maior que zero e adicional de FCP que não fecha com base vezes
percentual.

Com `resolverTributacao: true`, informar o grupo **destrava** o cálculo: a mesma
operação sem ele continua recusando com `422 TRIBUTACAO_NAO_RESOLVIDA`, agora com a
mensagem apontando o campo que resolve, em vez de anunciar uma limitação.

Na NFC-e (modelo 65) o grupo não existe: aquele documento é sempre operação interna, e
enviar o campo devolve `400` nomeando-o. Guia completo: [DIFAL](/guides/difal).

## 2026-08-10: o XML de uma emissão rejeitada agora é recuperável

**`GET /v1/nfe/xml/{id}` devolve o XML transmitido de uma nota rejeitada.** Antes a
rota só alcançava documento autorizado, porque o caminho era derivado da chave de
acesso, e emissão rejeitada não tem chave. Na prática, quem recebia uma recusa da
SEFAZ ficava só com o código e o motivo, sem o documento para conferir.

Agora a mesma rota serve os dois casos: com a chave de acesso (44 dígitos), o XML
autorizado; com o `id` da nota, também o de uma rejeitada. É o documento assinado
exatamente como foi enviado, então dá para conferir tag a tag o que foi recusado.

Nada muda para quem já usa a chave de acesso.

## 2026-08-07: devolução, complementar e ajuste (`finNFe` 2, 3 e 4) emitem, com documento referenciado e forma Sem Pagamento

**NF-e complementar, de ajuste ou de devolução (`ide.finNFe: 2`, `3` ou `4`) emite.** O
contrato ganhou o documento fiscal referenciado: `referenciadas[].chaveAcesso` (array na
raiz do corpo, chave de acesso de 44 dígitos do documento original) vira a tag do
documento referenciado no leiaute, até **500** por nota. A chave aceita NF-e (`55`),
NFC-e (`65`) e CF-e SAT (`59`). As três finalidades exigem o campo; sem ele a engineAPI
recusa com `422 FINALIDADE_SEM_NFREF` antes de consumir número fiscal, em vez de a nota
sair rejeitada pela SEFAZ **depois** de o número já ter sido consumido (`cStat 254` na
complementar, `cStat 321` na devolução). Em `finNFe: 2` vale **exatamente uma**
referência: a nota complementar complementa uma nota específica.

A chave é conferida antes de emitir, e cada eixo tem `422 REFERENCIADA_INVALIDA` próprio:
dígito verificador pelo módulo 11, código de UF, ano/mês com mês entre `01` e `12`,
modelo, número do documento não zerado, chave repetida no array e referência à própria
nota. O que a API **não** confere: CNPJ e UF embutidos na chave contra o emitente.

```json theme={null}
{
  "error": {
    "type": "https://engineapi.com.br/errors/FINALIDADE_SEM_NFREF",
    "status": 422,
    "detail": "\"finNFe\": 4 (devolução/retorno) exige o documento fiscal referenciado para a SEFAZ aceitar a operação: sem ele a nota sai rejeitada com cStat 321 (\"NF-e de devolução de mercadoria não possui documento fiscal referenciado\"). Informe \"referenciadas\": [{ \"chaveAcesso\": \"<44 dígitos da nota original>\" }] no corpo da requisição, ou emita com \"finNFe\": 1 (normal). Cobertura atual: https://docs.engineapi.com.br/cobertura. Nada foi emitido; nenhum número fiscal foi consumido."
  }
}
```

**A forma de pagamento `"90"` (Sem Pagamento) também chega ao contrato.**
`pagamentos: [{ "forma": "90", "valor": 0 }]`, única entrada e sem `troco`, documenta uma
operação sem contraprestação (remessa, bonificação, comodato, devolução) e emite na NF-e
(modelo 55). `finNFe: 3` e `4` exigem essa forma: `422 FINALIDADE_EXIGE_SEM_PAGAMENTO` se
vier outra. Outra forma junto de `"90"`, ou `troco` maior que zero, recusa com
`422 SEM_PAGAMENTO_INVALIDO` (não há pagamento do qual devolver troco). Só na NF-e: a
NFC-e veda a forma `"90"` (`422 SEM_PAGAMENTO_VEDADO_NFCE`).

<Warning>
  **Mudança de comportamento, se você já integra:** `finNFe: 2`/`3`/`4` sem
  `referenciadas` deixa de ser recusado com o `422 FINALIDADE_SEM_NFREF` genérico e passa
  a apontar o campo do contrato que resolve. Quem já enviava `referenciadas` (ou
  `pagamentos` com forma `"90"`) e recebia recusa passa a emitir. `finNFe` ausente ou `1`
  (normal) segue sem nenhuma mudança.
</Warning>

Ver [Cobertura fiscal](/cobertura) e [Erros e Rejeições](/guides/errors#finnfe-2-3-ou-4-sem-o-documento-referenciado-422).

## v2.10.0 (2026-08-07)

## 2026-08-07: revenda de mercadoria com ICMS-ST já retido na NF-e (CST 60)

**Quem revende mercadoria cuja Substituição Tributária já foi retida pelo
fornecedor (bebida, autopeça, cosmético, medicamento) passa a ter caminho na
NF-e.** O item aceita `icms.cst: "60"` com os valores que vieram na nota de
entrada, em `items[].icms`: `vBCSTRet`, `pST` e `vICMSSTRet` (base, alíquota
suportada pelo consumidor final e valor retido), mais, quando houver, o FCP-ST
retido (`vBCFCPSTRet`, `pFCPSTRet`, `vFCPSTRet`) e o ICMS efetivo
(`pRedBCEfet`, `vBCEfet`, `pICMSEfet`, `vICMSEfet`). `vICMSSubstituto` é
opcional e acompanha o primeiro bloco.

```json theme={null}
"icms": {
  "origem": 0,
  "cst": "60",
  "vBCSTRet": 120.50,
  "pST": 18,
  "vICMSSTRet": 21.69
}
```

Nada é calculado: os números vêm do seu fornecedor e a engineAPI transcreve,
recusando o que o documento não comporta. Cada bloco vai inteiro ou não vai:
bloco pela metade recusa com `422 ICMS_ST_RETIDO_INCOMPLETO`, e campo de ST
retido sem o `cst: "60"` recusa com `422 ICMS_ST_RETIDO_INVALIDO`, sempre antes
de consumir número fiscal. O `vFCPSTRet` do total da nota passa a ser somado
dos itens; o valor da nota (`vNF`) não muda.

Vale para emissor em Regime Normal (`crt: 3`) e no Simples com excesso de
sublimite (`crt: 2`), que são os dois regimes cujo documento carrega `cst`. Na
NFC-e (modelo 65) esses onze campos não fazem parte do contrato: enviá-los
devolve `400` nomeando o campo.

<Warning>
  **Mudança de comportamento consciente, se você já integra:** item que traz
  `icms.cst: "60"` **junto** de `icms.csosn`, num emissor do Simples pleno
  (`crt: 1`) ou MEI (`crt: 4`), antes era aceito e saía `201`: o `cst` era
  ignorado e o documento saía pelo `csosn`. Agora recusa com `422
      CST_REGIME_INCOMPATIVEL`. O `cst` deixou de ser um campo sem efeito, e
  aceitar os dois códigos no mesmo item seria aceitar duas tributações
  contraditórias e escolher uma em silêncio.

  Nesses regimes o documento carrega `csosn`, e o código equivalente (`500`)
  ainda não é emitido. Se o seu cadastro manda os dois no mesmo item, remova o
  `cst`: o desfecho volta a ser o de sempre, agora sem ambiguidade.

  O que **não** mudou: `icms.cst: "60"` sozinho nesses regimes já recusava com
  `422 CST_REGIME_INCOMPATIVEL` e continua igual, só que agora a recusa sai na
  primeira tentativa, sem pedir antes que você complete campos que o documento
  do seu regime não carrega.
</Warning>

O lado SUBSTITUTO da cadeia (CST `10`/`30`/`70`/`90`, imposto cobrado NESTA
operação) continua recusando: ele depende de margem de valor agregado e
alíquota interna de ST, que variam por UF, NCM e convênio, e a engineAPI não
estima nenhuma das duas. Estado atual, com o que já emite e o que falta provar:
[Cobertura fiscal](/cobertura).

## 2026-08-07: desconto e dedução na NFS-e, e campos que passam a chegar ao documento

**Prestador que dá desconto ou informa dedução volta a emitir.**
`servico.descontoIncondicionado` e `servico.valorDeducoes` eram escritos fora
dos grupos que o leiaute da DPS exige, e o documento era recusado por schema
depois que o número já tinha sido consumido. Agora o desconto sai em
`vDescCondIncond` e a dedução em `vDedRed/vDR`, os grupos oficiais. O
`servico.descontoCondicionado` passa a ter efeito no mesmo grupo. Vale lembrar
a diferença que o leiaute faz: o desconto **incondicionado** e a dedução
reduzem a base de cálculo do ISSQN, o **condicionado** não.

**Cinco campos que a API aceitava e não escrevia passam a chegar ao
documento:** `servico.codigoNBS` (`cServ/cNBS`), `servico.codigoTributacaoMunicipio`
(`cServ/cTribMun`), `tomador.inscricaoMunicipal` (`toma/IM`),
`informacoesComplementares` (`serv/infoCompl/xInfComp`) e o desconto
condicionado acima.

<Warning>
  **Mudança de comportamento.** Se você já envia algum desses cinco campos, a
  nota passa a sair **com** eles: até aqui a emissão respondia `201` e o
  documento saía sem o dado. Com `resolverTributacao: true`, o código NBS
  resolvido pelo Cérebro Fiscal também passa a constar no documento.

  Junto vieram validações que recusam **antes** de consumir numeração, em vez
  de deixar a rejeição chegar da SEFIN: NBS com 9 dígitos,
  código de tributação municipal com 3 dígitos (nos dois campos que o
  escrevem), inscrição municipal até 15 caracteres, informações complementares
  até 2000 caracteres e sem travessão, aspas curvas, reticências ou emoji
  (o leiaute só admite caracteres latinos básicos neste campo; quebra de linha
  é aceita e vira espaço), e desconto/dedução com no máximo 2 casas decimais.

  **`rps` passa a ser recusado** na emissão pelo Padrão Nacional
  (`422 RPS_SEM_CAMPO_NO_PADRAO_NACIONAL`): o leiaute da DPS não tem campo
  para número nem série de RPS, porque a própria DPS ocupa esse lugar. Antes o
  campo era aceito e a nota saía sem ele. Remova `rps` do payload e use o
  número da DPS devolvido na emissão para a sua correspondência.

  Códigos novos: `RPS_SEM_CAMPO_NO_PADRAO_NACIONAL` e `CTRIBMUN_CONFLITO`
  (os dois campos de código de tributação municipal informados com valores
  diferentes). Ver [Erros](/guides/errors).
</Warning>

## v2.9.0 (2026-08-06)

## 2026-08-07: campo infCpl vazio não leva mais texto padrão pro documento

**Quando `informacoesComplementares` não é preenchido, o campo `infCpl` some
do documento em vez de trazer um texto padrão nosso.** Antes, a NF-e e a NFC-e
sem esse campo saíam com um aviso genérico impresso no DANFE/DANFCE do seu
cliente. `informacoesComplementares` é opcional no leiaute: sem ele, a nota
sai limpa, do jeito que o cliente final vê.

<Warning>
  **Mudança de comportamento, se você já integra:** se o seu cadastro nunca
  preenchia `informacoesComplementares`, o documento até aqui saía com um
  texto padrão nosso nesse campo. A partir de agora, sem o campo, o documento
  simplesmente não traz mais nada ali.
</Warning>

**A identificação do software emissor (`verProc`, campo técnico do leiaute)
agora diz `engineAPI`.** Antes saía com um nome antigo do produto. O crédito
de quem gerou o documento continua aparecendo no PDF (DANFE/DANFCE), no rodapé
da página, sem usar o campo fiscal.

## 2026-08-06: cBenef passa a ser aceito na NF-e e na NFC-e

**`items[].cBenef` (código de benefício fiscal da UF) agora é aceito e vai
para o documento.** Antes, o campo era descartado e a nota podia sair sem
ele, mesmo quando o cenário do item exige benefício fiscal informado, o que a
SEFAZ recusa na validação. Formato de 8 ou 10 caracteres alfanuméricos, ou o
literal `SEM CBENEF`; opcional, junto de `icms.cst` no mesmo item.

## 2026-08-05: ICMS monofásico de combustíveis (CST 02 e 61)

**Revenda de combustível e de GLP passa a emitir.**
O ICMS de combustível não é percentual: é **monofásico e ad rem**, um valor em
reais por unidade de medida, cobrado uma vez só na cadeia. O item agora aceita
o grupo do leiaute em `items[].icms`: `qBCMono`, `adRemICMS` e `vICMSMono`
(CST `02`, tributação própria) ou `qBCMonoRet`, `adRemICMSRet` e
`vICMSMonoRet` (CST `61`, cobrada anteriormente, o caso da revenda). Vale nos
dois regimes, **inclusive Simples Nacional**, e na NFC-e (onde o leiaute
aceita só o CST `61`).

```json theme={null}
"icms": {
  "origem": 0,
  "cst": "61",
  "qBCMonoRet": 13,
  "adRemICMSRet": 1.2196,
  "vICMSMonoRet": 15.85
}
```

Antes de emitir, a engineAPI confere o que a SEFAZ vai conferir: o valor tem
que ser quantidade × alíquota ad rem, medido no número que vai no documento, e
o item precisa do grupo `combustivel` com o código da ANP. Divergência devolve
`422` sem consumir número fiscal. CST `15` (retenção) e `53` (diferimento)
ainda não são emitidos, e dizem isso com `422 ICMS_MONOFASICO_NAO_SUPORTADO`.

**O grupo `combustivel` chegou à NFC-e.**
Quem revende no balcão (posto, distribuidora de GLP) emite NFC-e, e agora pode
informar o código da ANP no mesmo documento. Detalhe completo: [Guia de
combustíveis](/guides/combustiveis).

## 2026-08-05: CNAE de desenvolvimento de software resolve serviço padrão

**Emissor com CNAE `6203-1/00` (desenvolvimento e licenciamento de software
não customizável) agora resolve item da lista de serviços automaticamente**
no fluxo de resolução de serviço padrão da NFS-e, sem precisar informar o
item manualmente. A tabela curada cobre hoje ocupações de MEI e este primeiro
CNAE de empresa em geral; CNAEs fora da tabela seguem exigindo o item
explícito no payload.

## 2026-08-05: venda a prazo com fatura e duplicatas na NF-e

**A NF-e aceita o grupo `cobranca` (fatura e duplicatas do parcelamento).**
Venda B2B a prazo agora emite com o detalhamento financeiro completo: uma
`fatura` (número, valor original, desconto e valor líquido) e uma lista de
`duplicatas` (número, vencimento e valor), independentes entre si no leiaute
(XSD). O campo é informativo: não altera o valor da nota nem a lista de
`pagamentos`.

**Correção (2026-08-30, #930):** apesar de estruturalmente independentes
no XSD, `duplicatas` sem `fatura` nunca foi aceito pela SEFAZ na prática — a
API passou a recusar essa combinação localmente, antes de numerar. Ver [Guia:
Venda a prazo](/guides/venda-a-prazo).

```json theme={null}
"cobranca": {
  "fatura": { "numero": "001", "valorOriginal": 1000, "valorDesconto": 0, "valorLiquido": 1000 },
  "duplicatas": [
    { "numero": "001/001", "vencimento": "2026-09-05", "valor": 500 },
    { "numero": "001/002", "vencimento": "2026-10-05", "valor": 500 }
  ]
}
```

Quando os dois vêm juntos, a soma das duplicatas precisa bater com o valor
líquido da fatura, com `422` antes de qualquer envio à SEFAZ se não bater. Na
NFC-e o campo é recusado com `422` (documento de balcão, sem faturamento a
prazo). Guia completo: [Venda a prazo](/guides/venda-a-prazo).

## 2026-08-05: CSOSN do Simples Nacional recusa antes da SEFAZ, por regime e documento

**`csosn` fora do que o motor emite corretamente agora recusa com `422
CSOSN_NAO_SUPORTADO`, antes de qualquer chamada à SEFAZ.** O conjunto que
emite hoje é `102`, `103`, `300` e `400` (Simples), com domínio mais estreito
para MEI. A tabela completa está em [Erros e
Rejeições](/guides/errors#catalogo-de-codigos-de-recusa) e em [Cobertura
Fiscal](/cobertura#cst-e-csosn).

<Warning>
  **Duas mudanças de comportamento conscientes, se você já integra:**

  * `csosn: "500"` (ICMS já cobrado por Substituição Tributária) e `csosn:
    "900"` (categoria residual) agora recusam com `422`. Os dois validam na
    forma do leiaute, mas exigem informação que a engineAPI ainda não
    resolve sozinha (CEST/CFOP para `500`; resolução do destinatário para
    `900`): emitir sem ela arriscaria um documento tecnicamente válido mas
    faticamente incorreto.
  * `csosn` aceito (`102`/`103`/`300`/`400`) junto com `icms.aliquota`,
    `icms.baseCalculo` ou `icms.valor` agora recusa com `422
    CSOSN_VALORES_NAO_SUPORTADOS`: nenhum desses campos existe no grupo do
    leiaute desses códigos, e antes eram descartados em silêncio.

  Se o seu cadastro usa `500`, `900` ou envia esses 3 campos junto com
  `csosn`, revise antes de atualizar em produção.
</Warning>

**MEI (`crt: 4`) com `csosn: "102"` agora exige CFOP compatível** (`5102`/
`6102` na NF-e; só `5102` na NFC-e). Fora disso, `422
CSOSN_CFOP_MEI_INCOMPATIVEL`.

## 2026-08-05: retenções federais e de ISS com efeito real na NFS-e

**`retencoes` passa a valer de verdade no documento da NFS-e (Padrão
Nacional).** Antes, o campo era recusado com `422` logo na entrada; agora vai
para a DPS com efeito real: `issRetidoPor` (2 = retido pelo tomador, 3 =
retido pelo intermediário), mais os valores retidos de `inss`, `irrf`,
`csll`, `pis` e `cofins`, cada um no grupo do leiaute correspondente. O
indicador de PIS/COFINS é derivado automaticamente do que você já informou
(nunca precisa declarar um código adicional).

```json theme={null}
"retencoes": {
  "issRetidoPor": 2,
  "inss": 150.00,
  "irrf": 48.00
}
```

Combinações que o leiaute não permite (por exemplo, `issRetidoPor` divergente
de um indicador de baixo nível já informado) recusam com `422` antes do
envio. Detalhe completo por tributo: [Erros e Rejeições](/guides/errors).

## 2026-08-05: frete, seguro e outras despesas na NF-e e na NFC-e

**O item aceita `valorFrete`, `valorSeguro` e `outrasDespesas`, com efeito
real no documento.** Os três valores somam no total da nota (`indTot: 0`
exclui o item específico dessa soma, sem afetar a tributação dele) e entram
na base de cálculo do ICMS e do IBS/CBS, como manda a lei para frete cobrado
pelo remetente e destacado em separado.

```json theme={null}
{
  "descricao": "Produto com frete destacado",
  "valorFrete": 50.00,
  "valorSeguro": 10.00,
  "outrasDespesas": 5.00,
  "indTot": 1
}
```

Valor negativo ou `indTot` fora de `{0, 1}` recusa com `422
FRETE_SEGURO_OUTRO_INVALIDO`, antes de qualquer envio à SEFAZ.

## 2026-08-05: NF-e passa a declarar a forma de pagamento real

**Cada entrada de `pagamentos[]` agora vai para o documento com a forma e o
valor exatos que você enviou.** Antes, toda NF-e saía declarando pagamento em
dinheiro para a SEFAZ, mesmo quando o pagamento real era cartão, PIX ou
boleto: o valor da nota estava certo, mas a forma declarada, não.

<Warning>
  **Mudança de comportamento, se você já integra:** se o seu cadastro nunca
  informou `pagamentos[].forma` (campo já existia no contrato, mas era
  ignorado na prática), toda nota até aqui foi declarada como dinheiro à
  SEFAZ independentemente da forma real. Revise `pagamentos[]` no seu payload
  antes de assumir que está tudo certo.
</Warning>

`troco` (para pagamento em dinheiro com valor recebido maior que o total)
também passa a ir para o documento, na mesma seção do pagamento.

## 2026-08-04: contrato de emissão mais estrito, cobertura fiscal documentada

**Campo desconhecido no payload agora recusa com `400` acionável.**
Antes, um campo fora do contrato de emissão (por exemplo, um grupo do leiaute
que a API ainda não suporta) era descartado antes de chegar à SEFAZ, e a nota
podia sair autorizada sem o dado que você enviou. Agora todo objeto dos
payloads de emissão (NF-e, NFC-e, NFS-e, lote e todos os sub-objetos) é
estrito: campo desconhecido devolve `400` antes de qualquer efeito, sem
número fiscal consumido, citando o campo, o objeto e os campos aceitos ali.

<CodeGroup>
  ```json Antes theme={null}
  // POST /v1/nfe com um campo fora do contrato dentro de "transporte"
  // 200: nota AUTORIZADA sem o dado enviado, sem aviso nenhum
  ```

  ```json Depois theme={null}
  // POST /v1/nfe com um campo fora do contrato dentro de "transporte"
  // 400
  {
    "error": {
      "campo": "transporte.veiculo",
      "motivo": "campo desconhecido",
      "camposAceitos": ["modFrete", "transportadora", "volumes"]
    }
  }
  ```
</CodeGroup>

**Nova página: [Cobertura Fiscal](/cobertura).**
Antes de integrar, agora dá para responder "o cenário do meu cliente emite
hoje?" sem abrir chamado: documento cruzado com regime tributário e operação,
célula a célula, com o estado real (disponível, disponível em breve, não
suportado) e o erro exato para os cenários ainda não suportados.

## 2026-08-04: combustíveis, inutilização de notas e mensagens de erro sem laço

**Grupo de combustíveis (GLP) emite de verdade.**
NF-e com CFOP de combustível agora aceita e valida o grupo `combustivel`
(código ANP, percentuais de GLP, `vPart`) contra o leiaute oficial. Sem o
grupo, um CFOP de combustível recusa antes com `422
COMBUSTIVEL_GRUPO_OBRIGATORIO`: a engineAPI nunca deixa a SEFAZ rejeitar
depois de consumir o número fiscal. Detalhe completo: [Guia de
combustíveis](/guides/combustiveis).

**Inutilização de faixa de numeração para NF-e e NFC-e.**
Novo endpoint para inutilizar uma faixa de números não usados, com os mesmos
limites de tamanho de justificativa e de tamanho de faixa por chamada nos
dois documentos.

**Mensagens de erro pararam de se contradizer.**
Um mesmo cenário podia recusar com `TRIBUTACAO_NAO_RESOLVIDA` numa chamada e
com `CST_NAO_SUPORTADO` na chamada seguinte, para o mesmo payload, sem
caminho de saída. A mensagem agora é determinística e nomeia o motivo real na
primeira resposta. Formato completo: [Erros e Rejeições](/guides/errors).
