convertajá

Guia 4 de 8

NF-e em lote: do XML ao DANFE em A4 ou 10×15

Seu ERP ou loja recebe os XMLs das notas; a API devolve os DANFEs prontos para arquivar, mandar por e-mail ou imprimir na térmica junto com a etiqueta.

12 min · Intermediário · atualizado em 16/10/2026

O que você vai construir

Uma rotina que pega todos os XMLs de uma pasta, divide em lotes do tamanho do seu plano, gera o DANFE de cada nota (A4 ou simplificado 10×15) e salva os PDFs.

Antes de começar

  • Uma chave de API em CONVERTAJA_KEY (veja Primeiros passos).
  • XMLs autorizados de NF-e (modelo 55) ou NFC-e (modelo 65). Pode ser o nfeProc com o protocolo de autorização.

Como funciona

A ferramenta é a xml-nfe-para-pdf. Ela aceita arquivos .xml e .zip (com XMLs dentro) e tem duas opções:

OpçãoValoresPara quê
layouta4 (padrão) ou simplificadoA4 para arquivo e e-mail; simplificado 10×15 para imprimir na térmica de etiquetas.
mergefalse (padrão) ou truetrue junta todas as notas num PDF só (DANFEs.pdf), bom para imprimir de uma vez.

Custo: 1 crédito por arquivo enviado. Notas repetidas (mesma chave de acesso) são geradas uma vez só, e o info avisa. Num ZIP, cada XML de dentro conta como um arquivo (50 notas num ZIP custam o mesmo que 50 XMLs soltos) e respeita o limite de arquivos por tarefa do seu plano.

Passo 1: Gere o DANFE de uma nota

curl -X POST "https://convertaja.com.br/v1/tools/xml-nfe-para-pdf?wait=true" \
  -H "Authorization: Bearer $CONVERTAJA_KEY" \
  -F "files=@nota.xml" \
  -F "layout=a4"
Resposta 200
{
  "id": "tPwE4oD7MJL-",
  "tool": "xml-nfe-para-pdf",
  "operation": "nfe-danfe",
  "status": "done",
  "progress": 100,
  "message": "Pronto!",
  "created_at": "2026-10-16T20:24:22.767Z",
  "finished_at": "2026-10-16T20:24:23.368Z",
  "expires_at": "2026-10-16T21:24:22.767Z",
  "credits_charged": 1,
  "credits_refunded": 0,
  "test": false,
  "outputs": [
    {
      "index": 0,
      "name": "NF-e 4521 - DISTRIBUIDORA PAULISTA DE FERRAGENS E MA.pdf",
      "size": 6291,
      "url": "https://convertaja.com.br/v1/jobs/tPwE4oD7MJL-/files/0"
    }
  ],
  "download_url": "https://convertaja.com.br/v1/jobs/tPwE4oD7MJL-/download",
  "info": "1 nota convertida"
}

O nome do arquivo segue o padrão NF-e {número} - {emitente}.pdf (ou NFC-e …). No layout simplificado, termina em - etiqueta.pdf.

Passo 2: Várias notas num PDF só

Repita o campo files e use merge: true:

# Repita -F "files=@…" para cada XML; merge=true junta tudo num PDF só
curl -X POST "https://convertaja.com.br/v1/tools/xml-nfe-para-pdf?wait=true" \
  -H "Authorization: Bearer $CONVERTAJA_KEY" \
  -F "files=@nota-4521.xml" \
  -F "files=@nota-4522.xml" \
  -F 'options={"layout":"a4","merge":true}'
Resposta 200
{
  "id": "vtuPW1LiyloD",
  "tool": "xml-nfe-para-pdf",
  "operation": "nfe-danfe",
  "status": "done",
  "progress": 100,
  "message": "Pronto!",
  "created_at": "2026-10-16T20:37:07.851Z",
  "finished_at": "2026-10-16T20:37:08.343Z",
  "expires_at": "2026-10-16T21:37:07.851Z",
  "credits_charged": 2,
  "credits_refunded": 0,
  "test": false,
  "outputs": [
    {
      "index": 0,
      "name": "DANFEs.pdf",
      "size": 11821,
      "url": "https://convertaja.com.br/v1/jobs/vtuPW1LiyloD/files/0"
    }
  ],
  "download_url": "https://convertaja.com.br/v1/jobs/vtuPW1LiyloD/download",
  "info": "2 notas convertidas"
}

Passo 3: Um ZIP com o mês inteiro

Sem merge, cada nota vira um PDF e download_url entrega um ZIP com todos:

# Um ZIP com vários XMLs; sem merge, cada nota vira um PDF
curl -s -X POST "https://convertaja.com.br/v1/tools/xml-nfe-para-pdf?wait=true" \
  -H "Authorization: Bearer $CONVERTAJA_KEY" \
  -F "files=@notas-outubro.zip" \
  -F "layout=simplificado" > tarefa.json

# download_url devolve um ZIP quando há mais de um PDF (requer jq)
curl -fs -o danfes.zip "$(jq -r .download_url tarefa.json)" \
  -H "Authorization: Bearer $CONVERTAJA_KEY"
$ unzip -l danfes.zip
   2589  NF-e 4521 - DISTRIBUIDORA PAULISTA DE FERRAGENS E MA - etiqueta.pdf
   2590  NF-e 4522 - DISTRIBUIDORA PAULISTA DE FERRAGENS E MA - etiqueta.pdf

Passo 4: Rotina para o ERP: uma pasta inteira, em lotes

O script lê o limite de arquivos por tarefa do seu plano em /v1/me, divide os XMLs em lotes, espera cada um (continuando a consultar se passar de 60 s) e salva o resultado:

// danfe-lote.mjs: gera os DANFEs de todos os XMLs de uma pasta
// Uso: node danfe-lote.mjs ./xml a4      (ou: node danfe-lote.mjs ./xml simplificado)
import { openAsBlob } from 'node:fs';
import { readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';

const BASE = 'https://convertaja.com.br/v1';
const headers = { Authorization: `Bearer ${process.env.CONVERTAJA_KEY}` };
const PASTA = process.argv[2] || './xml';
const LAYOUT = process.argv[3] || 'a4';

async function api(p, options = {}) {
  const res = await fetch(BASE + p, { ...options, headers });
  const data = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${data.error.code}: ${data.error.message}`);
  return data;
}

// Quantos arquivos cabem numa tarefa depende do plano (10 no Sandbox, 50 no Start…)
const me = await api('/me');
const porTarefa = me.limits.max_files_per_job;
const xmls = (await readdir(PASTA)).filter((n) => n.toLowerCase().endsWith('.xml'));
console.log(`${xmls.length} XMLs · ${porTarefa} por tarefa · ${me.credits.remaining} créditos disponíveis`);

for (let i = 0, lote = 1; i < xmls.length; i += porTarefa, lote++) {
  const form = new FormData();
  for (const nome of xmls.slice(i, i + porTarefa)) form.append('files', await openAsBlob(path.join(PASTA, nome)), nome);
  form.append('options', JSON.stringify({ layout: LAYOUT, merge: false }));

  let job = await api('/tools/xml-nfe-para-pdf?wait=true', { method: 'POST', body: form });
  while (job.status !== 'done' && job.status !== 'error') { // lotes grandes podem passar de 60 s
    await new Promise((r) => setTimeout(r, 2000));
    job = await api(`/jobs/${job.id}`);
  }
  if (job.status === 'error') {
    console.error(`Lote ${lote}: ${job.error.message} (créditos devolvidos)`);
    continue;
  }
  // download_url: o PDF, se for um só, ou um ZIP com todos
  const res = await fetch(job.download_url, { headers });
  const destino = job.outputs.length === 1 ? job.outputs[0].name : `danfes-lote-${lote}.zip`;
  await writeFile(destino, Buffer.from(await res.arrayBuffer()));
  console.log(`Lote ${lote}: ${job.info} → ${destino}`);
}
$ node danfe-lote.mjs ./xml simplificado
3 XMLs · 50 por tarefa · 1889 créditos disponíveis
Lote 1: 3 notas convertidas → danfes-lote-1.zip

Dica Para rodar todo dia, agende com cron (Linux), o Agendador de Tarefas (Windows) ou a fila do seu ERP. Se preferir não esperar, troque ?wait=true por um webhook_url e baixe quando o aviso chegar.

Bônus: os mesmos XMLs numa planilha

A ferramenta nfe-para-excel junta as notas numa planilha (.xlsx) com notas, impostos e itens, também 1 crédito por arquivo:

curl -s -X POST "https://convertaja.com.br/v1/tools/nfe-para-excel?wait=true" \
  -H "Authorization: Bearer $CONVERTAJA_KEY" \
  -F "files=@notas-outubro.zip" > planilha.json

curl -fs -o notas-fiscais.xlsx "$(jq -r .download_url planilha.json)" \
  -H "Authorization: Bearer $CONVERTAJA_KEY"
"info": "2 notas e 6 itens na planilha"

Erros e o que fazer

Um arquivo que não é NF-e não derruba a validação de entrada (a extensão é aceita), mas a tarefa termina com erro e os créditos voltam:

Tarefa com erro
{
  "id": "lrzqWwqfYv4a",
  "tool": "xml-nfe-para-pdf",
  "operation": "nfe-danfe",
  "status": "error",
  "progress": 0,
  "message": "Nenhuma NF-e encontrada. Envie os arquivos XML das notas (ou um ZIP com eles). (1 arquivo ignorado por não ser NF-e)",
  "created_at": "2026-10-16T20:24:25.824Z",
  "finished_at": "2026-10-16T20:24:25.998Z",
  "expires_at": "2026-10-16T21:24:25.824Z",
  "credits_charged": 1,
  "credits_refunded": 1,
  "test": false,
  "outputs": [],
  "error": {
    "code": "job_failed",
    "message": "Nenhuma NF-e encontrada. Envie os arquivos XML das notas (ou um ZIP com eles). (1 arquivo ignorado por não ser NF-e)"
  }
}

Num lote misturado, os arquivos que não são NF-e são ignorados e o info avisa quantos. Outros erros comuns: 415 unsupported_file_type (extensão diferente de .xml/.zip), 400 too_many_files (passou do limite do plano: divida em lotes, como no passo 4) e 402 insufficient_credits.

Próximos passos

Todos os endpoints e opções: referência da API. Dúvidas? Fale com a gente e informe o X-Request-Id da chamada.