Fiscal
Sem documento fiscal, o pedido não sai da loja: é o gate que antecede a geração de etiqueta. Este guia cobre NF-e (modelo 55), Declaração de Conteúdo, registro de XML, troca de nota emitida errada e download de DANFE/PDF. invoices[] é uma lista, e ela guarda o histórico. Nada no código limita o pedido a um documento, e a ordem não é garantida. Reenviar uma chave de NF-e diferente no mesmo pedido cria um segundo documento; o anterior só some se você descartar antes. E documento com replaced_at preenchido foi trocado: continua na lista, mas não representa mais o pedido. Quem lê "a NF-e do pedido" filtra replaced_at == null.
Conceitos
O objeto que GET /orders/{id}/fiscal devolve: o(s) documento(s) já registrado(s) e a disponibilidade da Declaração:
{
"invoices": [ // lista; 0, 1 ou mais documentos
{
"id": "5b4f…",
"type": "nfe", // ← nfe | declaration
"status": "completed", // ← pending | validating | valid | invalid | processing | completed | error
"invoice_key": "3526…", // chave de acesso (44 dígitos, NF-e)
"invoice_number": "1234", // só display
"invoice_value": 249.9, // só display
"has_pdf": true, // ← DANFE/PDF pronto p/ download (olhe aqui, não no status)
"error_message": null, // motivo quando a validação fiscal falha (status error/invalid)
"created_at": "2026-04-26T15:10:00-03:00",
"updated_at": "2026-04-26T15:10:40-03:00", // última mudança; útil pra detectar documento parado
"can_retry": false, // ← pode reprocessar (POST /fiscal/{invoiceId}/retry)
"can_discard": false, // ← pode descartar (DELETE /fiscal/{invoiceId})
"can_replace": true, // ← pode trocar (POST /fiscal/{invoiceId}/replace); só NF-e validada e vigente
"replaced_at": null, // ← ≠ null: documento HISTÓRICO, já foi trocado e não vale mais
"replaced_by_invoice_id": null, // id da nota que substituiu esta
"replaces_invoice_id": null, // id da nota que esta substituiu
"replace_reason": null // motivo informado na troca
}
],
"declaration_available": true // loja pode emitir Declaração de Conteúdo
}Quem emite o quê
Quem emite a NF-e é a sua loja, não a plataforma, e isso faz sentido: a nota nasce dos seus dados fiscais, do seu CNPJ e do seu regime tributário, então ela sai do seu ERP e é autorizada pela SEFAZ no seu nome. O papel da plataforma vem depois que a nota existe: guardar a chave (e, idealmente, o XML), gerar o DANFE a partir do XML, e disponibilizar para download quando o cliente ou o transportador precisar. Ou seja, você cuida da emissão; a gente cuida de hospedar e entregar.
Loja que vende ≠ loja que emite. Se a sua operação tem sublojas em modo espelho, quem emite é a matriz: as duas dividem a mesma empresa. O detalhe do pedido resolve isso para você no bloco emitente (CNPJ, IE, razão social e regime a usar na nota) e em emitente.store. Ver Pedidos de sublojas.
A Declaração de Conteúdo é diferente: a plataforma gera o PDF dela para você, em background, a partir dos dados do pedido. É uma alternativa à NF-e para casos onde a loja não emite nota fiscal (típico em PF, MEI sem inscrição estadual, ou em pedidos de baixo valor).
Você usa um ou outro por pedido:
Pré-condições
O pedido precisa estar num estado fiscal-elegível: qualquer estado de produção (production_started, awaiting_materials, production_in_progress, production_finished) ou preparing em diante (ready_to_ship, shipped, delivered, ready_for_pickup, picked_up). Tentar emitir em paid retorna 422:
{
"success": false,
"code": 422,
"message_code": "VALIDATION_ERROR",
"errors": {
"status": ["O pedido precisa estar em preparação antes de emitir documentos fiscais. Status atual: paid"]
}
}Declaração de Conteúdo exige feature da loja. A primeira chamada que você deve fazer é a consulta:
GET/api/v1/sellers/orders/{id}/fiscal
{
"data": {
"invoices": [],
"declaration_available": true
}
}invoices[]: documentos já emitidos para esse pedido (vazio se for o primeiro).declaration_available: se sua loja tem a featurefiscal_declarationativa. Se vierfalse, só resta NF-e.
Frete contratado pela plataforma: o que a NF-e precisa conter
Alguns pedidos têm o frete cotado e contratado pela plataforma junto a um agregador de transportadoras (hoje, Central do Frete). Nesses casos a contratação do frete acontece no momento em que você registra a NF-e, e o agregador valida a nota antes de aceitar. Nota fora do padrão dele = frete recusado e carga parada, mesmo com a NF-e autorizada pela SEFAZ.
A contratação acontece uma vez. Registrar outro documento no mesmo pedido — reprocessar ou trocar a nota, emitir a Declaração depois da NF-e — não recontrata remessa que já está integrada com a transportadora. É proposital: a segunda contratação sairia como frete duplicado, cobrado de verdade.
Você identifica esse pedido pelo bloco shipping.nfe_requirements com source: "central_frete". Ele já traz o que escrever na nota:
{
"shipping": {
"nfe_requirements": {
"source": "central_frete",
"mod_frete": "2", // → transp/modFrete
"mod_frete_label": "Por conta de terceiros",
"carrier_document": "48740351000132",// → transp/transporta/CNPJ
"carrier_name": "Braspress",
"hub_document": "22531311000110", // → precisa ser citado em infAdic/infCpl
"hub_name": "Central do Frete",
"volumes": 3 // → transp/vol/qVol
},
"on_demand": {
"provider": "central_frete",
"provider_label": "Central do Frete",
"chosen_option": {
"carrier": "Braspress", // transportadora real que vai coletar
"carrier_document": "48740351000132" // CNPJ dela
},
"nfe_requirements": { … } // mesmo objeto acima, sem o `source`
}
}
}Quatro regras, e as quatro são verificadas antes do envio ao agregador:
| Campo da NF-e | O que precisa | De onde tirar |
|---|---|---|
transp/modFrete | 2 — por conta de terceiros | nfe_requirements.mod_frete |
transp/transporta/CNPJ | CNPJ da transportadora que vai coletar | nfe_requirements.carrier_document |
infAdic/infCpl | precisa citar o CNPJ do agregador | nfe_requirements.hub_document |
transp/vol/qVol | mesma contagem de volumes da cotação | nfe_requirements.volumes |
shipping.on_demand.nfe_requirementscontinua existindo e não vai sumir, com exatamente o mesmo conteúdo (sem osource). Se a sua integração já lê de lá, nada quebra. Para código novo prefirashipping.nfe_requirements: é o mesmo campo para os dois tipos de frete, e osourcediz qual deles você recebeu.
A transportadora real muda por pedido — é a que a cotação escolheu. Não fixe um CNPJ no template da sua NF-e: leia carrier_document a cada pedido.
Quando a nota não atende, o registro do documento fiscal funciona normalmente (a NF-e é sua, e é válida), mas o envio fica com falha de integração e a mensagem diz qual campo corrigir, item a item:
{
"code": "central_frete_nfe_invalida",
"nfe_problemas": [
"Modalidade do frete: a NF-e está com modFrete=1 e a Central exige 2 (por conta de terceiros).",
"Transportadora: a NF-e não traz CNPJ no grupo transp/transporta.",
"Informações complementares: a NF-e precisa citar o CNPJ da Central do Frete (22.531.311/0001-10) em infAdic/infCpl.",
"Volumes: a NF-e declara 2 e a cotação foi feita com 1. Os dois têm que bater."
]
}Corrigido o motivo, o operador reenvia pelo painel sem precisar refazer a nota — desde que a nota nova traga os campos certos.
Volumes é o item que mais pega.
qVoltem de bater com o que foi cotado, não com quantas caixas você embalou. Se a embalagem mudou depois da venda, o número certo é o denfe_requirements.volumes, que acompanha a remessa.
Frete negociado pelo vendedor: o que a NF-e precisa conter
Em pedidos do canal de vendas a transportadora pode ter sido negociada direto pelo vendedor, que informa o CNPJ dela e o valor combinado. Não há agregador no meio, ninguém valida a sua nota — mas o CNPJ e a modalidade precisam sair nela do mesmo jeito, e é a plataforma que sabe quais são.
Você identifica esse pedido pelo shipping.nfe_requirements com source: "custom_freight":
{
"shipping": {
"carrier_name": "Transportes Silva LTDA",
"nfe_requirements": {
"source": "custom_freight",
"mod_frete": "1", // → transp/modFrete
"mod_frete_label": "Por conta do destinatário (FOB)",
"carrier_document": "27865757000102", // → transp/transporta/CNPJ
"carrier_name": "Transportes Silva LTDA",
"freight_value": 450.00,
"freight_value_in_total": false // ← leia este antes do vFrete
},
"on_demand": null
}
}| Campo da NF-e | O que precisa | De onde tirar |
|---|---|---|
transp/modFrete | 0 (CIF) ou 1 (FOB), conforme foi negociado | nfe_requirements.mod_frete |
transp/transporta/CNPJ | CNPJ do transportador informado na venda | nfe_requirements.carrier_document |
transp/transporta/xNome | razão social do transportador | nfe_requirements.carrier_name |
vFrete | o valor somente quando freight_value_in_total é true | nfe_requirements.freight_value |
Em FOB o
vFretevai zerado.freight_value_in_total: falsequer dizer que o frete não foi cobrado no pedido: quem contrata e paga a transportadora é o destinatário, e ototal_orderdo pedido já vem sem ele. Ofreight_valuecontinua publicado para conferência — é o valor que o vendedor combinou com o cliente, não um valor a faturar. Somá-lo à nota cobraria duas vezes.
Em CIF (mod_frete: "0") é o contrário: freight_value_in_total vem true, o valor está dentro do totals.shipping do pedido e é ele que vai no vFrete.
A modalidade muda por pedido — é o que foi negociado naquela venda, com aquele cliente. Não fixe modFrete no template da sua NF-e: leia mod_frete a cada pedido.
NF-e: dois caminhos
A NF-e nasce no seu ERP. O que muda é o que você tem em mãos quando vai registrar:
Caminho 1: XML completo em string
Se o seu ERP devolveu o XML autorizado junto com a chave, mande tudo de uma vez:
POST/api/v1/sellers/orders/{id}/fiscal/nfe/key
{
"type": "nfe",
"invoice_key": "35260411222333000144550010000012341123456781",
"invoice_number": "1234",
"invoice_value": 249.90,
"invoice_xml": "<?xml version=\"1.0\"?><nfeProc>...</nfeProc>"
}| Campo | Obrigatório | O que é |
|---|---|---|
type | sim | Sempre nfe por enquanto. |
invoice_key | sim | Chave de acesso de 44 caracteres (aqui a validação é só de tamanho; o dígito mod-11 é conferido nos fluxos de lookup/upload de XML). |
invoice_xml | sim | XML autorizado. A partir dele a plataforma gera o DANFE em background. |
invoice_number | não | Número da nota, só para exibição. |
invoice_value | não | Valor total, só para exibição. |
Retorno 201 Created. O DANFE entra na fila e fica pronto em alguns segundos.
Caminho 2: upload manual do XML
Quando você tem o arquivo .xml em mãos mas não tem ainda a chave registrada na plataforma (cenário típico: XML salvo do email do contador, exportado de outro sistema). Mande o arquivo direto:
POST/api/v1/sellers/orders/{id}/invoice/upload-xml
multipart/form-data com o campo xml contendo o arquivo (máx 1 MB, extensão .xml):
POST /api/v1/sellers/orders/ORD-000123/invoice/upload-xml
Content-Type: multipart/form-data; boundary=----X
------X
Content-Disposition: form-data; name="xml"; filename="nfe.xml"
Content-Type: application/xml
<?xml version="1.0"?>...
------X--A chave é extraída do próprio XML (atributo Id="NFe..." em infNFe) e validada por mod-11. Resposta igual ao caminho 1.
Erros típicos:
- 422: XML sem chave, chave inválida, arquivo vazio, extensão errada.
- 409
chave_already_used: essa NF-e já está vinculada a outro pedido. - 409
invoice_already_validated: refaça o invoice antes de reenviar.
Declaração de Conteúdo
Para lojas sem NF-e. A plataforma gera o PDF a partir dos dados do pedido; você só dispara:
POST/api/v1/sellers/orders/{id}/fiscal/declaration
Sem body. Resposta 202 Accepted:
{
"success": true,
"data": {
"invoice": { "id": "8c7e...", "type": "declaration", "status": "pending", "has_pdf": false }
}
}A partir daí, polling até has_pdf=true no GET /fiscal.
Erros típicos:
- 403:
declaration_available=falsena sua loja. Peça ativação da featurefiscal_declaration. - 422: pedido em status que não permite emissão fiscal (típico:
paid).
Polling do PDF
NF-e (com XML) e Declaração geram PDF em background. O padrão é o mesmo para os dois:
Cadência recomendada: primeiros 30 segundos a cada 3s, depois a cada 10s, com timeout em 2 minutos. PDFs simples ficam prontos em ~5s; declarações com muitos itens podem levar mais.
Baixar o PDF
GET/api/v1/sellers/orders/{id}/fiscal/{invoiceId}/pdf
{invoiceId} é o id que veio em invoices[] na resposta de /fiscal. A rota é a mesma para DANFE e Declaração; o filename já vem com o prefixo certo (danfe_ ou declaracao_).
Mande
Accept: application/json. Com esse cabeçalho, a rota devolve o JSON com a URL assinada (abaixo). É o que uma integração quer. Sem ele, a resposta é um redirect direto para o arquivo, pensado para o navegador. Se o armazenamento não suportar URL temporária, os dois caminhos caem para o PDF em stream (Content-Type: application/pdf).
Quando o PDF está pronto:
{
"success": true,
"data": {
"url": "https://s3.amazonaws.com/.../danfe_ORD-000123.pdf?X-Amz-Signature=...",
"filename": "danfe_ORD-000123.pdf",
"expires_in": "15 minutos"
}
}A url é S3 assinada, válida por 15 minutos. Se expirar, chame a rota de novo, que gera uma nova URL. Não armazene a URL.
Quando ainda não está pronto, retorna 202 Accepted com o status atual (não é um erro, é só sinal de "tente de novo").
DANFE em ZPL (impressora Zebra)
GET/api/v1/sellers/orders/{id}/fiscal/{invoiceId}/zpl
Mesma mecânica do PDF, mas devolve o DANFE simplificado em ZPL, pronto para impressora térmica Zebra. Existe só para type: "nfe". Declaração de Conteúdo não tem versão ZPL. Vale o mesmo cabeçalho Accept: application/json para receber a URL assinada em vez do arquivo.
Consulta de dados (sem PDF)
GET/api/v1/sellers/orders/{id}/fiscal/{invoiceId}/view
Devolve os dados completos do invoice (chave, número, valor, status, metadata da declaração) sem o PDF. Útil para telas de detalhe fiscal no painel.
Reprocessar ou descartar um documento
Um documento pode ficar parado em pending/processing ou terminar em error/invalid (XML rejeitado, PDF que não gerou, chave que a SEFAZ não reconheceu). Cada item de invoices[] traz dois flags pra você não precisar adivinhar o que fazer:
| Flag | Rota | O que faz |
|---|---|---|
can_retry | POST /fiscal/{invoiceId}/retry | Refaz o processamento sem reenviar nada. Declaração é gerada de novo a partir do pedido; NF-e é revalidada a partir do XML já guardado. Limpa o error_message, volta o documento pra processing e responde 202. Depois é o mesmo polling em GET /fiscal. |
can_discard | DELETE /fiscal/{invoiceId} | Remove a tentativa que não concluiu. Responde 200 com o id, e o documento some de invoices[]. |
Os dois valem só pra documento não concluído. Documento valid/completed é registro fiscal definitivo: as duas rotas devolvem 409 (invoice_not_retryable e invoice_already_validated). Para corrigir uma NF-e já validada existe a troca, não o descarte. can_retry também é false pra NF-e sem XML armazenado; nesse caso, registre a nota de novo com /fiscal/nfe/key ou /invoice/upload-xml.
Mudou o corpo do
409destas duas rotas (potencialmente breaking). Os códigos (invoice_not_retryable,invoice_already_validated) e os status continuam iguais, mas a resposta passou a sair no envelope padrão da API em vez do JSON cru{error, message}. Leia sempre o campoerror, que é o código estável. O que mudou de valor é omessage: ele agora repete o código, e a frase em português foi paramessage_code.{ "success": false, "message_code": "Documento fiscal já validado não pode ser descartado. Para corrigi-lo, use a troca de documento.", "data": "invoice_already_validated", "message": "invoice_already_validated", "error": "invoice_already_validated" }
updated_at ajuda a decidir: documento em processing há mais de alguns minutos sem mudar é candidato a retry.
Trocar a NF-e do pedido
DELETE limpa rascunho. Para uma NF-e já validada — nota autorizada com valor errado, item errado, destinatário errado — o caminho é outro: você cancela a nota na SEFAZ, emite a certa no seu ERP e registra a troca aqui.
POST/api/v1/sellers/orders/{id}/fiscal/{invoiceId}/replace
{invoiceId} é o documento que vai sair (o vigente). Chame só quando o can_replace dele vier true no GET /fiscal.
| Campo | Como manda | O que é |
|---|---|---|
reason | sempre | Por que está trocando (3 a 255 caracteres). Fica em replace_reason e na auditoria do pedido. |
xml | multipart/form-data | Arquivo .xml autorizado, máx 1 MB. A chave sai do próprio arquivo. |
invoice_key | JSON | Chave de 44 dígitos, só números. A plataforma busca o XML nos providers, igual ao /invoice/lookup-xml. |
Mande xmlouinvoice_key — um dos dois é obrigatório:
POST /api/v1/sellers/orders/ORD-000123/fiscal/01K8NVCE0000000000000NFE1/replace
Content-Type: application/json
{ "reason": "Valor unitário errado na nota anterior", "invoice_key": "35260411222333000144550010000012351123456799" }Ao contrário da emissão e do retry, esta rota não exige o pedido em preparing. Nota errada quase sempre aparece depois do despacho, e é esse o caso que ela atende.
A troca só se efetiva quando a nota nova valida
A resposta 200 diz apenas que a nota nova foi registrada e entrou em validação — o corpo é o mesmo de /invoice/upload-xml (order_invoice_id, provider, chave, xml_url). Até a nota nova validar, a antiga continua vigente: é o que impede o pedido de ficar sem documento no caminho e, com ele, sem etiqueta e sem despacho.
Acompanhe por GET /fiscal até replaced_at aparecer preenchido no documento antigo. Se a nota nova reprovar, descarte-a com DELETE /fiscal/{invoiceId} e tente de novo.
Uma troca por vez. Enquanto houver tentativa em aberto para o mesmo documento, a segunda chamada devolve 409 invoice_replacement_in_flight. Tentativa que terminou em error/invalid não bloqueia: pode tentar de novo.
Recusas (o código estável vai no campo error):
| Status | error | Quando |
|---|---|---|
409 | invoice_not_replaceable | O documento não é NF-e validada e vigente, ou já é histórico. Rascunho se descarta, não se troca. |
409 | invoice_replacement_in_flight | Já existe uma troca em aberto para esse documento. |
409 | chave_already_used | A chave nova já está vinculada a outro pedido. |
422 | same_invoice_key | A chave enviada é a mesma que já está no pedido. |
Depois da troca, quem lê precisa filtrar
O documento antigo não some: ele fica em invoices[] com replaced_at e replaced_by_invoice_id preenchidos, e a nota nova aponta de volta em replaces_invoice_id. Para achar a nota vigente do pedido, filtre replaced_at == null.
O
invoices[]do detalhe e da listagem de pedidos não temreplaced_at. Lá a nota substituída aparece indistinguível da atual (ehas_fiscal_documentconta as duas). Quem precisa saber qual vale usaGET /orders/{id}/fiscal.
Frete: troca depois do despacho não recontrata
Se a remessa já foi integrada com a transportadora, a troca não recontrata o frete: a transportadora ficou com a nota anterior, e recontratar automaticamente sairia como segunda cobrança. A plataforma abre uma ocorrência no pedido para a decisão não se perder — se a mercadoria ainda não postou, é caso de cancelar a etiqueta e recriar a remessa.
Antes do despacho a troca é transparente: a remessa é contratada normalmente com a nota nova.
Cenários comuns
"Acabei de emitir a NF-e no ERP, quero registrar agora"
ERP devolveu chave + XML:
POST /api/v1/sellers/orders/ORD-000123/fiscal/nfe/key
{ "type": "nfe", "invoice_key": "...", "invoice_xml": "<?xml...?>" }Depois faça polling do /fiscal até has_pdf=true e baixe.
"Loja é MEI, vai usar Declaração"
Confirme primeiro:
GET /api/v1/sellers/orders/ORD-000123/fiscalSe declaration_available=true, dispara:
POST /api/v1/sellers/orders/ORD-000123/fiscal/declarationPolling → download.
"O cliente está reclamando que não recebeu o DANFE, quero reenviar"
A URL expirou. Faça GET /fiscal/{invoiceId}/pdf de novo, que vai gerar uma URL nova.
"Errei a NF-e, emiti outra no ERP, quero corrigir"
Depende do que aconteceu com a nota errada.
Ela já tinha validado (statusvalid/completed, can_replace: true): use a troca. Cancele a nota na SEFAZ, emita a certa e mande POST /fiscal/{invoiceId}/replace com o reason e a nota nova. A anterior vira histórico quando a nova validar; até lá o pedido não fica sem documento.
Ela nunca concluiu (presa em pending/processing, ou em error/invalid): é rascunho. Descarte com DELETE /fiscal/{invoiceId} e registre a nota certa por /fiscal/nfe/key ou /invoice/upload-xml.
Fora desses dois caminhos, a rota /fiscal/nfe/key também aceita ser chamada de novo no mesmo pedido, desde que a chave seja outra — mas isso só empilha um segundo documento, não substitui o primeiro. A chave de acesso é única na plataforma inteira: reenviar uma chave já usada devolve 422 em errors.invoice_key, com "Esta chave de acesso já foi utilizada em outro pedido".
O código invoice_already_validated (409) não existe nesta rota: ele só aparece em /invoice/lookup-xml, /invoice/upload-xml e no DELETE /fiscal/{invoiceId}.
"O documento travou em processing ou deu erro"
Olhe can_retry no item de invoices[]. Se for true, dispare POST /fiscal/{invoiceId}/retry e volte pro polling. Se for false e o documento não está concluído, é NF-e sem XML guardado: descarte com DELETE /fiscal/{invoiceId} e registre de novo com /fiscal/nfe/key (com invoice_xml) ou /invoice/upload-xml. Ver Reprocessar ou descartar um documento.
Cuidados
Não chame /fiscal em loop curto. Para acompanhar geração de PDF, cadência de 3 a 10 segundos é suficiente. Loop em ms gasta quota sem benefício.
invoice_xml é obrigatório. Esquecer o XML no /fiscal/nfe/key retorna 422. Se você ainda não tem o XML registrado na plataforma, use /invoice/upload-xml para enviar o arquivo.
Escolha um caminho por pedido: NF-e ou Declaração. O documento fiscal é o que acompanha a mercadoria, e dois documentos válidos competindo pelo mesmo pedido geram ambiguidade na hora de imprimir, despachar e mostrar pro cliente. A API não impede o segundo, então a disciplina é sua: tentativa que não concluiu você limpa com DELETE /fiscal/{invoiceId}; NF-e já validada não se apaga, se troca — e a troca preserva a anterior como histórico em vez de sumir com ela.
Não armazene as URLs S3. Elas têm assinatura curta. Sempre chame /fiscal/{invoiceId}/pdf na hora de servir o download, pois a URL anterior pode estar expirada.

