Começe a facilitar sua vida profissional e fortalecer sua network

LoginCriar conta

© 2026 daash.IO - Todos os direitos reservados.Desenvolvido por Kalin Digital

  • Perguntas frequentes
  • Politica de privacidade

Integração direta com o Sistema Nacional NFS-e (ADN gov.br)

Kalin DigitalKalin Digital
·atualizado há cerca de 1 hora

Compartilhando os aprendizados da nossa migração de SaaS de NFS-e para integração direta com o gov.br/nfse. Foco em erros que matamos no soco (XSD, pattern, ordem de elementos, timezone, instabilidade do PDF) e as soluções definitivas — pra quem vai começar agora não perder dias.

Cobre: emissão, consulta de status, cancelamento (eventos), exemplos completos de request/response, e a saga do PDF.


TL;DR

  • mTLS com certificado A1, sem OAuth/token. Quem tem o .pfx da empresa emite — fim.
  • Endpoint principal: POST https://sefin.nfse.gov.br/SefinNacional/nfse com body { dpsXmlGZipB64: "<XML assinado, gzipado, base64>" }.
  • Resposta síncrona com chaveAcesso (50 pos) e nfseXmlGZipB64 (a NFS-e autorizada já volta pronta).
  • Cancelamento: POST /SefinNacional/nfse/{chave}/eventos com XML do evento e101101 (gzip+base64) — também síncrono.
  • Consulta: GET /SefinNacional/nfse/{chave} ou GET /SefinNacional/dps/{idDps}.
  • O XML é o documento juridicamente válido, o PDF é só renderização. Salve o XML, é a sua cópia legal.
  • O endpoint de PDF (/danfse/{chave}) é instável crônico (502/503). Use retry agressivo + ofereça XML como fallback.

Sumário

  1. Stack e dependências
  2. Endpoints e ambientes
  3. Autenticação mTLS
  4. Fluxo de emissão (passo a passo)
  5. Erros do XSD que vão te pegar
  6. Exemplos de request/response
  7. Consulta de status
  8. Cancelamento (evento e101101)
  9. O drama do PDF DANFSE
  10. Idempotência
  11. Gotchas adicionais
  12. Como descobrir os tipos certos do XSD
  13. Checklist pra produção
  14. Recursos úteis

Stack e dependências

{
  "axios": "^1.7",
  "fast-xml-parser": "^5.7",
  "luxon": "^3.6",
  "node-forge": "^1",
  "xml-crypto": "^6"
}

Nenhuma biblioteca específica de NFS-e. O troco é o XSD oficial v1.01.


Endpoints e ambientes

GrupoProdução Restrita (homologação)Produção
ADNadn.producaorestrita.nfse.gov.bradn.nfse.gov.br
SEFINsefin.producaorestrita.nfse.gov.brsefin.nfse.gov.br

Endpoints essenciais

# EMISSÃO (síncrono)
POST  /SefinNacional/nfse
      Body: { "dpsXmlGZipB64": "<base64>" }
      201 → { tipoAmbiente, idDps, chaveAcesso, nfseXmlGZipB64, alertas }
      400/403/500 → { erros: [{ Codigo, Descricao, Complemento }] }

# CONSULTA por chave de acesso
GET   /SefinNacional/nfse/{chaveAcesso}
      200 → { ...dados completos da NFS-e... }

# CONSULTA por idDps (idempotência em caso de timeout)
GET   /SefinNacional/dps/{idDps}
      200 → { chaveAcesso, ... }

# EVENTOS (cancelar/substituir/etc)
POST  /SefinNacional/nfse/{chaveAcesso}/eventos
      Body: { "pedidoRegistroEventoXmlGZipB64": "<base64>" }
      201 → { eventoXmlGZipB64, ... }

# CONSULTAR EVENTO específico
GET   /SefinNacional/nfse/{chaveAcesso}/eventos/{tpEvento}/{nSeq}

# LISTAR eventos da NFS-e (acessível ao contribuinte)
GET   /contribuintes/NFSe/{chaveAcesso}/Eventos

# PDF DANFSE (instável — ver seção dedicada)
GET   /danfse/{chaveAcesso}

# CADASTRO Nacional do Contribuinte (one-shot por empresa)
POST  /cnc/CNC

# VERIFICAR se cidade é conveniada
GET   /parametrizacao/{codMun}/convenio

# CONSULTAR alíquota oficial
GET   /parametrizacao/{codMun}/{codServ}/{competencia}/aliquota

Specs OpenAPI completos

Acessíveis com mTLS:

/SefinNacional/swagger/docs/v1
/adn/swagger/v1/swagger.json
/cnc/swagger/v1/swagger.json
/danfse/swagger/v1/swagger.json
/parametrizacao/swagger/v1/swagger.json
/contribuintes/swagger/v1/swagger.json

Autenticação mTLS

Sem OAuth, sem token, sem credenciais separadas. A autenticação é mútua TLS com certificado A1 da empresa (cada empresa que vai emitir precisa do seu).

Validar localmente

# Extrair PEM do .pfx
openssl pkcs12 -in cert.pfx -clcerts -nokeys -passin "pass:$CERT_PASS" -out cert.pem
openssl pkcs12 -in cert.pfx -nocerts -nodes  -passin "pass:$CERT_PASS" -out key.pem

# Testar conexão (deve retornar 200)
curl --cert cert.pem --key key.pem \
  https://adn.producaorestrita.nfse.gov.br/docs/index.html

# Verificar se município é conveniado (BH = 3106200)
curl --cert cert.pem --key key.pem \
  https://adn.producaorestrita.nfse.gov.br/parametrizacao/3106200/convenio

No Node.js

import https from "node:https";
import forge from "node-forge";
import fs from "node:fs";

const pfxBuffer = fs.readFileSync("cert.pfx");
const p12Asn1 = forge.asn1.fromDer(pfxBuffer.toString("binary"));
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, process.env.CERT_PASS);

const certBag = p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag][0];
const keyBag = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[forge.pki.oids.pkcs8ShroudedKeyBag][0];

const certPem = forge.pki.certificateToPem(certBag.cert);
const keyPem = forge.pki.privateKeyToPem(keyBag.key);

const httpsAgent = new https.Agent({
  cert: certPem,
  key: keyPem,
  rejectUnauthorized: true,
});

Fluxo de emissão (passo a passo)

1. Carregar cert A1 → PEM
2. Montar XML do DPS conforme XSD v1.01
3. Assinar com XMLDSig SHA-256 (action: "after")
4. Comprimir gzip + base64
5. POST /SefinNacional/nfse via mTLS
6. Decodificar resposta:
   - chaveAcesso (50 pos)
   - nfseXmlGZipB64 → descomprimir → XML autorizado (juridicamente válido!)
7. Salvar XML autorizado no banco

Erros do XSD que vão te pegar

Tudo abaixo retorna RNG6110: Falha Schema Xml. Cada item custou ~1h de debug com nota real em produção.

1. Id na tag raiz <DPS> é proibido

<!-- ❌ vai dar "The 'Id' attribute is not declared" -->
<DPS xmlns="..." versao="1.00" Id="DPS...">

<!-- ✅ -->
<DPS xmlns="..." versao="1.00">
  <infDPS Id="DPS...">

TCDPS no XSD só tem o atributo versao. O Id vai SÓ no <infDPS>.

2. Pattern do Id (TSIdDPS): 45 chars exatos

DPS + cMun(7) + tpInsc(1) + CNPJ(14) + serie(5) + nDPS(15)

Pattern oficial: <xs:pattern value="DPS[0-9]{42}"/>

Cuidado: o nDPS dentro do Id deve bater exatamente com o conteúdo do elemento <nDPS>. Não dá pra ter zero-padding diferente.

3. Convenção tpInsc é diferente da NFe

Na NFS-e Nacional (SPED):

1 = CPF
2 = CNPJ

Se você vier de NFe e usar 1=CNPJ, vai dar E0004: Conteúdo do identificador informado na DPS difere. Cuidado!

4. <nDPS> não pode começar com 0

XSD TSNumDPS: [1-9]{1}[0-9]{0,14}

Mas o Id precisa de 15 dígitos. Conflito? Resolva gerando um nDPS que naturalmente tenha 15 dígitos começando com [1-9]:

const deriveNumericNDPS = (referencia: string) => {
  const hex = referencia.match(/[a-fA-F0-9]+/g)!.join("").slice(-12);
  const reduced = (BigInt(`0x${hex}`) % BigInt("100000000000000"))
    .toString(10)
    .padStart(14, "0");
  return `1${reduced}`; // sempre 15 dígitos, sempre começa com '1'
};

Bônus: é determinístico (mesmo input → mesmo nDPS), bom pra idempotência.

5. Ordem estrita dos elementos

XSD é <xs:sequence>, não <xs:all>. Você TEM que respeitar a ordem.

<toma> (TCInfoPessoa):

CNPJ|CPF|NIF|cNaoNIF → CAEPF? → IM? → xNome → end? → fone? → email?

<prest> (TCInfoPrestador):

CNPJ|CPF|... → CAEPF? → IM? → xNome? → end? → fone? → email? → regTrib (obrigatório)

Quase todo mundo coloca email antes de fone (alfabético). Errado.

6. <tribMun> não tem vBC/vISSQN/vLiq

Quem migra de Nuvem Fiscal/Focus/etc costuma trazer essa estrutura. Aqui não:

<!-- ✅ correto -->
<tribMun>
  <tribISSQN>1</tribISSQN>          <!-- 1=tributável -->
  <tpRetISSQN>1</tpRetISSQN>         <!-- 1=Não Retido -->
  <pAliq>2.00</pAliq>                <!-- opcional -->
</tribMun>

Os valores monetários vivem só em <vServPrest><vServ>.

7. <Signature> é IRMÃ de <infDPS>, não filha

XSD TCDPS:

<xs:sequence>
  <xs:element name="infDPS" type="TCInfDPS"/>
  <xs:element ref="ds:Signature" minOccurs="0"/>
</xs:sequence>

No xml-crypto:

sig.computeSignature(opts.xml, {
  location: { reference: `//*[@Id='${idDPS}']`, action: "after" },  // ← "after", não "append"
});

append coloca a Signature dentro do <infDPS> → schema invalid.

8. dhEmi com timezone errado

// ❌ fica 3h adiantado (UTC + label "-03:00")
new Date().toISOString().replace(/\.\d+Z$/, "-03:00")

// ✅ converte de verdade
import { DateTime } from "luxon";
DateTime.now()
  .setZone("America/Sao_Paulo")
  .minus({ seconds: 5 })  // margem contra clock drift do servidor
  .toFormat("yyyy-LL-dd'T'HH:mm:ssZZ")

Erro relacionado: E0008: A data de emissão da DPS não pode ser posterior à data do seu processamento.

9. <indTotTrib> x <pTotTribSN>

Para Simples Nacional (opSimpNac=3):

<!-- ❌ E0712: ME/EPP não pode usar indTotTrib -->
<totTrib><indTotTrib>0</indTotTrib></totTrib>

<!-- ✅ -->
<totTrib><pTotTribSN>6.00</pTotTribSN></totTrib>

pTotTribSN é a alíquota efetiva do Simples para o serviço.

10. Mensagens de erro vêm em PascalCase

{ "erros": [{ "Codigo": "...", "Descricao": "...", "Complemento": "..." }] }

Não confiar só em codigo/descricao — alguns endpoints retornam camelCase. Normalize:

const norm = (m: any) => ({
  codigo: m?.codigo ?? m?.Codigo,
  descricao: m?.descricao ?? m?.Descricao,
  complemento: m?.complemento ?? m?.Complemento,
});

11. vServ deve ser STRING, não Number

XSD TSDec15V2 exige pattern 0|0\.NN|D+(\.NN)?. Em JavaScript:

Number(61.20)           // 61.2 ❌ (perde o zero — rejeitado)
Number(61.2).toFixed(2) // "61.20" ✅ (string com 2 casas — aceito)

fast-xml-parser serializa o que você passar. Sempre passe string formatada:

const f2 = (v: number): string => v.toFixed(2);

// no objeto pra serializar:
valores: { vServPrest: { vServ: f2(invoice.amount / 100) } }

Se passar Number, vai falhar com:

The 'vServ' element is invalid - The value '61.2' is invalid
according to its datatype 'TSDec15V2' - The Pattern constraint failed.

Exemplos de request/response

Emissão — REQUEST

XML do DPS antes da assinatura (resumido):

<?xml version="1.0" encoding="UTF-8"?>
<DPS xmlns="http://www.sped.fazenda.gov.br/nfse" versao="1.00">
  <infDPS Id="DPS31062002505167240001600000110000000000001">
    <tpAmb>1</tpAmb>
    <dhEmi>2026-04-28T19:34:48-03:00</dhEmi>
    <verAplic>1.00</verAplic>
    <serie>00001</serie>
    <nDPS>100000000000001</nDPS>
    <dCompet>2026-04-28</dCompet>
    <tpEmit>1</tpEmit>
    <cLocEmi>3106200</cLocEmi>
    <prest>
      <CNPJ>50516724000160</CNPJ>
      <IM>14701490012</IM>
      <regTrib>
        <opSimpNac>3</opSimpNac>
        <regApTribSN>1</regApTribSN>
        <regEspTrib>0</regEspTrib>
      </regTrib>
    </prest>
    <toma>
      <CNPJ>19678493000141</CNPJ>
      <xNome>Cliente da Silva</xNome>
      <fone>1132051030</fone>
      <email>[email protected]</email>
    </toma>
    <serv>
      <locPrest><cLocPrestacao>3106200</cLocPrestacao></locPrest>
      <cServ>
        <cTribNac>010501</cTribNac>
        <cTribMun>001</cTribMun>
        <xDescServ>Assinatura SaaS — Plataforma Esthetis</xDescServ>
        <cNBS>115062100</cNBS>
      </cServ>
    </serv>
    <valores>
      <vServPrest><vServ>169.00</vServ></vServPrest>
      <trib>
        <tribMun>
          <tribISSQN>1</tribISSQN>
          <tpRetISSQN>1</tpRetISSQN>
        </tribMun>
        <totTrib>
          <pTotTribSN>6.00</pTotTribSN>
        </totTrib>
      </trib>
    </valores>
  </infDPS>
</DPS>

Após xml-crypto assinar (action: "after"), a <Signature> vai ser irmã do <infDPS>, dentro do <DPS>:

<DPS ...>
  <infDPS Id="DPS...">...</infDPS>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>...</SignedInfo>
    <SignatureValue>...</SignatureValue>
    <KeyInfo><X509Data><X509Certificate>...</X509Certificate></X509Data></KeyInfo>
  </Signature>
</DPS>

Comprime + base64 + envia:

const gz = await gzip(Buffer.from(signedXml, "utf-8"));
const dpsXmlGZipB64 = gz.toString("base64");

const response = await axios.post(
  "https://sefin.nfse.gov.br/SefinNacional/nfse",
  { dpsXmlGZipB64 },
  { httpsAgent, headers: { "Content-Type": "application/json" } },
);

Emissão — RESPONSE 201 (sucesso)

{
  "tipoAmbiente": 1,
  "versaoAplicativo": "SefinNacional_1.6.0",
  "dataHoraProcessamento": "2026-04-28T19:34:51.6810658-03:00",
  "idDps": "NFS31062002505167240001600000110000000000001",
  "chaveAcesso": "31062002250516724000160000000000002126046985535602",
  "nfseXmlGZipB64": "H4sIAAAA...AAA=",
  "alertas": null
}

chaveAcesso é a chave de 50 posições que identifica a NFS-e na ADN. nfseXmlGZipB64 é o XML autorizado (gzip+base64) — descompacte e salve:

import zlib from "node:zlib";
import { promisify } from "node:util";
const gunzip = promisify(zlib.gunzip);

const buf = Buffer.from(response.data.nfseXmlGZipB64, "base64");
const xmlAutorizado = (await gunzip(buf)).toString("utf-8");

Emissão — RESPONSE 400 (erro)

{
  "tipoAmbiente": 1,
  "versaoAplicativo": "SefinNacional_1.6.0",
  "dataHoraProcessamento": "2026-04-28T19:33:05.1286368-03:00",
  "idDPS": "DPS31062002...",
  "erros": [
    {
      "Codigo": "E0712",
      "Descricao": "Para ME/EPP o indicador de informação de valor total de tributos não pode ser informado.",
      "Complemento": null
    }
  ]
}

Consulta de status

Existem três formas de consultar:

1. Por chaveAcesso (você sabe a chave de 50 pos)

curl --cert cert.pem --key key.pem \
  https://sefin.nfse.gov.br/SefinNacional/nfse/31062002250516724000160000000000002126046985535602
const response = await axios.get(
  `https://sefin.nfse.gov.br/SefinNacional/nfse/${chaveAcesso}`,
  { httpsAgent },
);
// response.data tem TODOS os dados da NFS-e (status, valores, tomador, etc.)

2. Por idDps (você só tem o ID do DPS — usado em recovery após timeout)

curl --cert cert.pem --key key.pem \
  https://sefin.nfse.gov.br/SefinNacional/dps/DPS31062002505167240001600000110000000000001

Resposta:

{
  "idDps": "DPS...",
  "chaveAcesso": "31062002...",
  "status": "AUTORIZADA"
}

⭐ Útil quando o POST /nfse deu timeout mas o servidor processou. Você gerou o idDps localmente (determinístico), então pode tentar recuperar a chave em vez de reemitir e gerar duplicidade.

3. Listar eventos da nota (cancelamentos, substituições)

curl --cert cert.pem --key key.pem \
  https://adn.nfse.gov.br/contribuintes/NFSe/{chaveAcesso}/Eventos

Retorna lote de eventos (até 100) vinculados à chave.


Cancelamento (evento e101101)

NFS-e cancelada via evento padronizado pelo MOC. O fluxo é parecido com a emissão: monta XML, assina, gzip+base64, envia.

Endpoint

POST /SefinNacional/nfse/{chaveAcesso}/eventos
Body: { "pedidoRegistroEventoXmlGZipB64": "<base64>" }

XSD do pedRegEvento (TCPedRegEvt)

<pedRegEvento versao="1.00" xmlns="http://www.sped.fazenda.gov.br/nfse">
  <infPedReg Id="PRE...">
    <tpAmb>1|2</tpAmb>
    <verAplic>1.00</verAplic>
    <dhEvento>2026-04-28T20:27:08-03:00</dhEvento>
    <CNPJAutor>50516724000160</CNPJAutor>   <!-- ou CPFAutor -->
    <chNFSe>31062002250516724000160000000000002126046985535602</chNFSe>

    <!-- Choice: e101101 (cancelamento), e105102 (substituição), etc -->
    <e101101>
      <xDesc>Cancelamento de NFS-e</xDesc>     <!-- LITERAL fixo -->
      <cMotivo>1</cMotivo>                      <!-- 1=Erro, 2=Serv não prestado, 9=Outros -->
      <xMotivo>...descrição (15-255 chars)...</xMotivo>
    </e101101>
  </infPedReg>
  <Signature>...</Signature>   <!-- assinatura, irmã de infPedReg -->
</pedRegEvento>

Pattern do Id do evento (TSIdPedRegEvt)

PRE + chaveAcesso(50) + tpEvento(3) + nPedRegEvento(3) = 56 dígitos (59 chars total)

Pattern oficial: <xs:pattern value="PRE[0-9]{56}"/>

const idPedReg = `PRE${chaveAcesso}${"101"}${"001"}`; // 1º cancelamento

Builder completo

import { XMLBuilder } from "fast-xml-parser";
import { DateTime } from "luxon";

interface CancelInput {
  ambiente: "homologacao" | "producao";
  chaveAcesso: string;
  cnpjAutor: string;
  cMotivo: 1 | 2 | 9;
  xMotivo: string; // 15-255 chars
  nPedRegEvento?: number; // default 1
}

const xmlBuilder = new XMLBuilder({
  ignoreAttributes: false,
  attributeNamePrefix: "@_",
  format: false,
  suppressEmptyNode: true,
});

export const buildCancelEventoXml = (input: CancelInput) => {
  const tpAmb = input.ambiente === "producao" ? 1 : 2;
  const nPed = String(input.nPedRegEvento ?? 1).padStart(3, "0");
  const idPedReg = `PRE${input.chaveAcesso}101${nPed}`;

  const dhEvento = DateTime.now()
    .setZone("America/Sao_Paulo")
    .minus({ seconds: 5 })
    .toFormat("yyyy-LL-dd'T'HH:mm:ssZZ");

  const xMotivo = input.xMotivo.trim().slice(0, 255);
  if (xMotivo.length < 15) {
    throw new Error("xMotivo precisa de no mínimo 15 caracteres");
  }

  const obj = {
    pedRegEvento: {
      "@_xmlns": "http://www.sped.fazenda.gov.br/nfse",
      "@_versao": "1.00",
      infPedReg: {
        "@_Id": idPedReg,
        tpAmb,
        verAplic: "1.00",
        dhEvento,
        CNPJAutor: input.cnpjAutor.replace(/\D/g, ""),
        chNFSe: input.chaveAcesso,
        e101101: {
          xDesc: "Cancelamento de NFS-e",
          cMotivo: input.cMotivo,
          xMotivo,
        },
      },
    },
  };

  const xml = `<?xml version="1.0" encoding="UTF-8"?>${xmlBuilder.build(obj)}`;
  return { xml, idPedReg };
};

Cancelamento — REQUEST completo

const { xml, idPedReg } = buildCancelEventoXml({
  ambiente: "producao",
  chaveAcesso: "31062002250516724000160000000000002126046985535602",
  cnpjAutor: "50516724000160",
  cMotivo: 1,
  xMotivo: "Nota emitida em duplicidade durante migração para o ADN.",
});

// Mesma assinatura usada na emissão (action: "after")
const signedXml = signXmlByElementId({ xml, elementId: idPedReg, certPem, keyPem });

const gz = await gzip(Buffer.from(signedXml, "utf-8"));
const pedidoRegistroEventoXmlGZipB64 = gz.toString("base64");

const response = await axios.post(
  `https://sefin.nfse.gov.br/SefinNacional/nfse/${chaveAcesso}/eventos`,
  { pedidoRegistroEventoXmlGZipB64 },
  { httpsAgent, headers: { "Content-Type": "application/json" } },
);

Cancelamento — RESPONSE 201 (sucesso)

{
  "tipoAmbiente": 1,
  "versaoAplicativo": "SefinNacional_1.6.0",
  "dataHoraProcessamento": "2026-04-28T20:27:14.2893049-03:00",
  "eventoXmlGZipB64": "H4sIAAAA...AAA="
}

eventoXmlGZipB64 é o evento autorizado (com selo da SEFIN). Descompacte e salve junto da nota original — tem o protocolo do cancelamento.

Cancelamento — RESPONSE 400 (erro)

Mesmo formato da emissão (erros: [{ Codigo, Descricao, Complemento }]). Erros comuns:

  • E1102 — NFS-e já cancelada
  • E1101 — Prazo de cancelamento expirado (depende do município, geralmente até o último dia do mês de competência)
  • E0004 — Id do pedido de evento inválido (pattern PRE+56 dígitos errou)

Outros eventos disponíveis

CódigoTipo
e101101Cancelamento normal
e105102Cancelamento por substituição
e101103Solicitação de análise fiscal para cancelamento
e202201Confirmação do prestador
e203202Confirmação do tomador
e202205Rejeição do prestador
e203206Rejeição do tomador
e305101Cancelamento por ofício (uso da prefeitura)

A estrutura de cada evento varia. Consulte tiposEventos_v1.01.xsd.


O drama do PDF DANFSE

Sintomas

  • 502 Bad Gateway — gateway sem resposta
  • 503 Service Unavailable — sem servidor disponível
  • 504 Gateway Timeout
  • 429 Too Many Requests
  • ~60% das requests falham aleatoriamente

Diagnóstico oficial (do fórum gov.br)

"Tem sistemas robotizados buscando DANFEs para guardar, sendo que o DANFE não é documento válido juridicamente. Em razão dessas robotizações o serviço está intermitente."

Threads relevantes em forum.nfsebrasil.com.br:

  • "Erro 502 no acesso a adn.nfse.gov.br para consultar .PDF da NFSe"
  • "Geração de PDF das NFS-e não está funcionando" — 1.8k views, 29 usuários, thread aberta há 3+ meses sem resolução pelo gov.br

Solução definitiva: 2 camadas

1) Retry no PDF — com agent fresh sem keep-alive

Esse é o pulo do gato que ninguém fala: o gateway parece cachear o 502 na conexão TCP/TLS reusada. Conexão nova, request nova chance.

import https from "node:https";

const buildFreshAgent = () => new https.Agent({
  cert: certPem,
  key: keyPem,
  keepAlive: false,  // ← crítico
});

const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

async function baixarPdfDANFSE(chaveAcesso: string): Promise<Buffer> {
  const waits = [200, 400, 600, 800, 1000, 1500, 2000, 2500, 3000];

  for (let attempt = 1; attempt <= 10; attempt++) {
    const freshAgent = buildFreshAgent();
    const response = await axios.get(
      `https://adn.nfse.gov.br/danfse/${chaveAcesso}`,
      {
        httpsAgent: freshAgent,
        responseType: "arraybuffer",
        timeout: 30_000,
        validateStatus: () => true,
        headers: {
          Accept: "application/pdf",
          Connection: "close",
        },
      },
    );
    freshAgent.destroy();

    if (response.status === 200) return Buffer.from(response.data);
    if (![429, 502, 503, 504].includes(response.status)) {
      throw new Error(`HTTP ${response.status}`);
    }

    const wait = waits[Math.min(attempt - 1, waits.length - 1)] + Math.random() * 200;
    await sleep(wait);
  }
  throw new Error("PDF indisponível após 10 tentativas");
}

Taxa de sucesso medida: ~95% em 10 tentativas (worst-case ~13s).

2) Salvar o XML autorizado e oferecer como download

A SEFIN devolve o XML autorizado junto com a resposta de emissão (nfseXmlGZipB64). Salve no banco. É o documento legal.

// Após POST /SefinNacional/nfse com sucesso:
import zlib from "node:zlib";
import { promisify } from "node:util";
const gunzip = promisify(zlib.gunzip);

const data = response.data; // tem chaveAcesso + nfseXmlGZipB64

const buf = Buffer.from(data.nfseXmlGZipB64, "base64");
const xmlAutorizado = (await gunzip(buf)).toString("utf-8");

await prisma.invoice.update({
  where: { id: invoiceId },
  data: {
    chaveAcesso: data.chaveAcesso,
    authorizedXml: xmlAutorizado, // ← guarde isso
  },
});

Endpoint pra cliente baixar:

// GET /api/nfse/xml/:invoiceId
res.setHeader("Content-Type", "application/xml; charset=utf-8");
res.setHeader("Content-Disposition", `attachment; filename="NFSe-${chave}.xml"`);
res.send(invoice.authorizedXml);

E na UI:

ℹ O XML é o documento juridicamente válido. O PDF é só representação visual. Se o servidor gov.br estiver instável, use o XML.


Idempotência

NFS-e duplicada gera dor fiscal séria. Camadas mínimas:

  1. referenceKey no DB com @unique (chave de idempotência local)
  2. Antes de chamar SEFIN, verificar se já existe nota AUTHORIZED para essa referência. Se sim, retornar resultado existente sem chamar API.
  3. Em caso de timeout no POST /nfse — recuperar com GET /SefinNacional/dps/{idDps} (devolve a chave se o DPS foi processado).
async function emitir(referenceKey: string) {
  const existing = await prisma.invoice.findUnique({ where: { referenceKey } });
  if (existing?.status === "AUTHORIZED") {
    return { skipped: true, chave: existing.chaveAcesso };
  }

  try {
    const result = await emitirNfseADN(...);
    return result;
  } catch (e) {
    if (e.code === "ECONNABORTED" || e.code === "ETIMEDOUT") {
      // Recovery: o DPS pode ter sido processado mesmo com timeout
      const recovery = await axios.get(
        `https://sefin.nfse.gov.br/SefinNacional/dps/${idDpsLocal}`,
        { httpsAgent }
      ).catch(() => null);
      if (recovery?.data?.chaveAcesso) {
        return { recovered: true, chave: recovery.data.chaveAcesso };
      }
    }
    throw e;
  }
}

Gotchas adicionais

Proxy Next.js corrompe binary/XML

Se você tem um BFF/proxy entre cliente e API (comum em Next.js / Nuxt / Remix), detecte resposta não-JSON e repasse os bytes brutos:

// ❌ ERRADO — converte tudo em JSON, corrompe PDF e XML
return NextResponse.json(response.data);

// ✅ CORRETO
const isBinary =
  url.includes("/danfse-pdf/") ||
  url.includes("/adn-nfse/pdf/") ||
  url.includes("/adn-nfse/xml/");

if (isBinary) {
  return new NextResponse(response.data, {
    status: response.status,
    headers: {
      "Content-Type": response.headers["content-type"],
      "Content-Disposition": response.headers["content-disposition"] || "",
    },
  });
}
return NextResponse.json(response.data);

E no axios da chamada interna no proxy: responseType: "arraybuffer".

Configurar XMLBuilder do fast-xml-parser

const xmlBuilder = new XMLBuilder({
  ignoreAttributes: false,
  attributeNamePrefix: "@_",   // pra usar "@_Id", "@_versao", etc.
  format: false,                // sem indentação — o XSD é sensível em alguns pontos
  suppressEmptyNode: true,      // evita <tag/> vazias indesejadas
});

Logs com prefixo identificável

Facilita debug em produção. Use sempre o mesmo prefixo:

console.log(`[adn:nfse][emit] iniciando emissão ${JSON.stringify(meta)}`);
console.log(`[adn:nfse][http] → POST sefin /SefinNacional/nfse`);
console.log(`[adn:nfse][http] ✗ ← 400 ${JSON.stringify(erros)}`);

E no Kibana/Loki: grep "\[adn:nfse\].*✗" pra achar erros rapidinho.

Validade do certificado

Implemente um cron de aviso. A1 vence em 1 ano e o serviço cai 100% no D+0:

const daysLeft = Math.floor(
  (cert.validity.notAfter.getTime() - Date.now()) / (24 * 3600 * 1000)
);
if (daysLeft < 30) sendAlertToOps(`Cert vence em ${daysLeft} dias`);

Município não conveniado

const r = await axios.get(`/parametrizacao/${codMun}/convenio`, { httpsAgent });
if (r.status === 404 || r.data?.parametrosConvenio?.aderenteAmbienteNacional !== 1) {
  throw new Error(`Município ${codMun} ainda não emite NFS-e via ADN`);
}

5.482 de 5.571 municípios já estão (98,4% — abr/2026), mas alguns como Brasília-DF ainda não. Cheque antes de oferecer integração.


Como descobrir os tipos certos do XSD

Baixe o pacote oficial e leia os arquivos. Um grep bem feito economiza horas:

curl -L https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual/nfse-esquemas_xsd-v1-01-20260209.zip -o nfse-xsd.zip
unzip -d xsd nfse-xsd.zip

# Achar regra do Id
grep -nA8 "TSIdDPS" xsd/Schemas/1.01/tiposSimples_v1.01.xsd

# Achar ordem dos elementos do tomador
grep -nA50 'name="TCInfoPessoa"' xsd/Schemas/1.01/tiposComplexos_v1.01.xsd

# Estrutura de tribMun
grep -nA40 'name="TCTribMunicipal"' xsd/Schemas/1.01/tiposComplexos_v1.01.xsd

# Eventos disponíveis
grep -E 'name="e[0-9]+"' xsd/Schemas/1.01/tiposEventos_v1.01.xsd

# Pattern de qualquer tipo simples (substituir TIPO)
grep -nA5 'name="TIPO"' xsd/Schemas/1.01/tiposSimples_v1.01.xsd

Checklist pra produção

  • .pfx da empresa em local seguro + senha em .env separado
  • mTLS funcionando (testar com curl --cert cert.pem --key key.pem)
  • Verificar se município é conveniado: GET /parametrizacao/{cMun}/convenio
  • XSD v1.01 validado nos pontos críticos do item "Erros" acima
  • Salvar XML autorizado no banco (authorizedXml)
  • Endpoint próprio de download de XML (independente do gov.br)
  • Retry de PDF com agent fresh (10 tentativas)
  • referenceKey única por nota + check antes de chamar SEFIN (idempotência)
  • Recovery por idDps em caso de timeout
  • Cancelamento via evento e101101 implementado e testado
  • Cron de monitoramento da validade do certificado (alerta D-30/D-15/D-7)
  • Logs estruturados com prefixo identificável (facilita debug)
  • Proxy/BFF detecta binary/XML e repassa bytes (não converte para JSON)

Recursos úteis

  • Documentação técnica oficial: https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica
  • Lista de municípios conveniados (XLSX): https://www.gov.br/nfse/pt-br/municipios/monitoramento-adesoes
  • XSDs (v1.01): https://www.gov.br/nfse/pt-br/biblioteca/documentacao-tecnica/documentacao-atual/nfse-esquemas_xsd-v1-01-20260209.zip
  • Swagger das APIs (precisa do .pfx):
    • https://adn.nfse.gov.br/docs/index.html
    • https://sefin.nfse.gov.br/SefinNacional/docs/index
  • Fórum oficial da comunidade: https://forum.nfsebrasil.com.br/

Publicado via daash.IO