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.
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
nfeProccom 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ção | Valores | Para quê |
|---|---|---|
layout | a4 (padrão) ou simplificado | A4 para arquivo e e-mail; simplificado 10×15 para imprimir na térmica de etiquetas. |
merge | false (padrão) ou true | true 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"import { openAsBlob } from 'node:fs';
const form = new FormData();
form.append('files', await openAsBlob('nota.xml'), 'nota.xml');
form.append('layout', 'a4'); // ou "simplificado" (10×15)
const res = await fetch('https://convertaja.com.br/v1/tools/xml-nfe-para-pdf?wait=true', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.CONVERTAJA_KEY}` },
body: form,
});
const job = await res.json();
console.log(job.status, job.info, job.outputs?.[0]?.name);import os
import requests
with open("nota.xml", "rb") as f:
res = requests.post(
"https://convertaja.com.br/v1/tools/xml-nfe-para-pdf",
params={"wait": "true"},
headers={"Authorization": f"Bearer {os.environ['CONVERTAJA_KEY']}"},
files=[("files", ("nota.xml", f, "application/xml"))],
data={"layout": "a4"}, # ou "simplificado" (10×15)
timeout=90,
)
job = res.json()
print(job["status"], job.get("info"), job["outputs"][0]["name"] if job.get("outputs") else "")<?php
$ch = curl_init('https://convertaja.com.br/v1/tools/xml-nfe-para-pdf?wait=true');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CONVERTAJA_KEY')],
CURLOPT_POSTFIELDS => [
'files' => new CURLFile('nota.xml', 'application/xml'),
'layout' => 'a4', // ou 'simplificado' (10×15)
],
]);
$job = json_decode(curl_exec($ch), true);
echo "{$job['status']} {$job['info']}\n";
{
"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}'
{
"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.pdfPasso 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}`);
}# danfe_lote.py: gera os DANFEs de todos os XMLs de uma pasta
# Uso: python danfe_lote.py ./xml a4 (ou: python danfe_lote.py ./xml simplificado)
import json
import os
import sys
import time
import requests
BASE = "https://convertaja.com.br/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CONVERTAJA_KEY']}"}
PASTA = sys.argv[1] if len(sys.argv) > 1 else "./xml"
LAYOUT = sys.argv[2] if len(sys.argv) > 2 else "a4"
def api(method, path, **kwargs):
res = requests.request(method, BASE + path, headers=HEADERS, timeout=120, **kwargs)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['error']['code']}: {data['error']['message']}")
return data
# Quantos arquivos cabem numa tarefa depende do plano (10 no Sandbox, 50 no Start…)
me = api("GET", "/me")
por_tarefa = me["limits"]["max_files_per_job"]
xmls = sorted(n for n in os.listdir(PASTA) if n.lower().endswith(".xml"))
print(f"{len(xmls)} XMLs · {por_tarefa} por tarefa · {me['credits']['remaining']} créditos disponíveis")
for lote, i in enumerate(range(0, len(xmls), por_tarefa), start=1):
abertos = [open(os.path.join(PASTA, n), "rb") for n in xmls[i:i + por_tarefa]]
try:
job = api("POST", "/tools/xml-nfe-para-pdf", params={"wait": "true"},
files=[("files", (os.path.basename(f.name), f, "application/xml")) for f in abertos],
data={"options": json.dumps({"layout": LAYOUT, "merge": False})})
finally:
for f in abertos:
f.close()
while job["status"] not in ("done", "error"): # lotes grandes podem passar de 60 s
time.sleep(2)
job = api("GET", f"/jobs/{job['id']}")
if job["status"] == "error":
print(f"Lote {lote}: {job['error']['message']} (créditos devolvidos)")
continue
# download_url: o PDF, se for um só, ou um ZIP com todos
res = requests.get(job["download_url"], headers=HEADERS, timeout=120)
destino = job["outputs"][0]["name"] if len(job["outputs"]) == 1 else f"danfes-lote-{lote}.zip"
with open(destino, "wb") as f:
f.write(res.content)
print(f"Lote {lote}: {job['info']} → {destino}")<?php
// danfe-lote.php: gera os DANFEs de todos os XMLs de uma pasta
// Uso: php danfe-lote.php ./xml a4 (ou: php danfe-lote.php ./xml simplificado)
const BASE = 'https://convertaja.com.br/v1';
$pasta = $argv[1] ?? './xml';
$layout = $argv[2] ?? 'a4';
function api(string $method, string $path, $body = null): array {
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CONVERTAJA_KEY')],
]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) throw new RuntimeException("$status {$data['error']['code']}: {$data['error']['message']}");
return $data;
}
// Quantos arquivos cabem numa tarefa depende do plano (10 no Sandbox, 50 no Start…)
$me = api('GET', '/me');
$porTarefa = $me['limits']['max_files_per_job'];
$xmls = array_values(array_filter(glob(rtrim($pasta, '/') . '/*'), fn($f) => strtolower(pathinfo($f, PATHINFO_EXTENSION)) === 'xml'));
echo count($xmls) . " XMLs · $porTarefa por tarefa · {$me['credits']['remaining']} créditos disponíveis\n";
foreach (array_chunk($xmls, $porTarefa) as $n => $grupo) {
$lote = $n + 1;
$campos = ['options' => json_encode(['layout' => $layout, 'merge' => false])];
foreach ($grupo as $i => $arquivo) {
$campos["files[$i]"] = new CURLFile($arquivo, 'application/xml', basename($arquivo));
}
$job = api('POST', '/tools/xml-nfe-para-pdf?wait=true', $campos);
while (!in_array($job['status'], ['done', 'error'], true)) { // lotes grandes podem passar de 60 s
sleep(2);
$job = api('GET', "/jobs/{$job['id']}");
}
if ($job['status'] === 'error') {
echo "Lote $lote: {$job['error']['message']} (créditos devolvidos)\n";
continue;
}
// download_url: o PDF, se for um só, ou um ZIP com todos
$destino = count($job['outputs']) === 1 ? $job['outputs'][0]['name'] : "danfes-lote-$lote.zip";
$fp = fopen($destino, 'wb');
$ch = curl_init($job['download_url']);
curl_setopt_array($ch, [CURLOPT_FILE => $fp, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('CONVERTAJA_KEY')]]);
curl_exec($ch);
fclose($fp);
echo "Lote $lote: {$job['info']} → $destino\n";
}
$ 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:
{
"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.