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:

flowchart TD A["Pedido em preparing+"] --> B{"Sua loja emite<br/>NF-e?"} B -->|sim| C["Emite no ERP da loja"] C --> D{"Tem o XML<br/>autorizado?"} D -->|"sim, em mãos"| E["POST /fiscal/nfe/key<br/>com invoice_xml"] D -->|"XML em arquivo separado"| G["POST /invoice/upload-xml<br/>multipart"] E --> H["DANFE gerado<br/>em background"] G --> H B -->|"não, loja PF"| I["POST /fiscal/declaration"] I --> J["Declaração gerada<br/>em background"] H --> K["GET /fiscal: polling até has_pdf=true"] J --> K K --> L["GET /fiscal/&lcub;invoiceId&rcub;/pdf<br/>URL S3 15 min"]

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 feature fiscal_declaration ativa. Se vier false, 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-eO que precisaDe onde tirar
transp/modFrete2 — por conta de terceirosnfe_requirements.mod_frete
transp/transporta/CNPJCNPJ da transportadora que vai coletarnfe_requirements.carrier_document
infAdic/infCplprecisa citar o CNPJ do agregadornfe_requirements.hub_document
transp/vol/qVolmesma contagem de volumes da cotaçãonfe_requirements.volumes

shipping.on_demand.nfe_requirements continua existindo e não vai sumir, com exatamente o mesmo conteúdo (sem o source). Se a sua integração já lê de lá, nada quebra. Para código novo prefira shipping.nfe_requirements: é o mesmo campo para os dois tipos de frete, e o source diz 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.qVol tem 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 de nfe_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-eO que precisaDe onde tirar
transp/modFrete0 (CIF) ou 1 (FOB), conforme foi negociadonfe_requirements.mod_frete
transp/transporta/CNPJCNPJ do transportador informado na vendanfe_requirements.carrier_document
transp/transporta/xNomerazão social do transportadornfe_requirements.carrier_name
vFreteo valor somente quando freight_value_in_total é truenfe_requirements.freight_value

Em FOB o vFrete vai zerado.freight_value_in_total: false quer dizer que o frete não foi cobrado no pedido: quem contrata e paga a transportadora é o destinatário, e o total_order do pedido já vem sem ele. O freight_value continua 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>"
}
CampoObrigatórioO que é
typesimSempre nfe por enquanto.
invoice_keysimChave 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_xmlsimXML autorizado. A partir dele a plataforma gera o DANFE em background.
invoice_numbernãoNúmero da nota, só para exibição.
invoice_valuenãoValor 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=false na sua loja. Peça ativação da feature fiscal_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:

sequenceDiagram autonumber participant I as Integrador participant A as API participant W as Worker I->>A: POST /fiscal/declaration<br/>(ou /fiscal/nfe/key com XML) A-->>I: 202 / 201 A->>W: dispara job Note over W: gera PDF loop polling (a cada N segundos) I->>A: GET /fiscal A-->>I: { invoices: [{ has_pdf: false }] } end W-->>A: PDF pronto I->>A: GET /fiscal A-->>I: { invoices: [{ has_pdf: true }] } I->>A: GET /fiscal/{invoiceId}/pdf A-->>I: { url, expires_in: "15 minutos" }

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:

FlagRotaO que faz
can_retryPOST /fiscal/{invoiceId}/retryRefaz 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_discardDELETE /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 409 destas 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 campo error, que é o código estável. O que mudou de valor é o message: ele agora repete o código, e a frase em português foi para message_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.

CampoComo mandaO que é
reasonsemprePor que está trocando (3 a 255 caracteres). Fica em replace_reason e na auditoria do pedido.
xmlmultipart/form-dataArquivo .xml autorizado, máx 1 MB. A chave sai do próprio arquivo.
invoice_keyJSONChave 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.

sequenceDiagram autonumber participant I as Integrador participant A as API participant V as Validação I->>A: POST /fiscal/&lcub;invoiceId&rcub;/replace<br/>(reason + xml | invoice_key) A-->>I: 200 com order_invoice_id + chave Note over A: nota nova nasce com<br/>replaces_invoice_id = antiga.<br/>A antiga segue vigente. A->>V: valida a nota nova alt validou V-->>A: ok Note over A: antiga ganha replaced_at<br/>+ replaced_by_invoice_id else reprovou V-->>A: erro Note over A: nada muda: a antiga segue<br/>valendo e o rascunho é descartável end I->>A: GET /fiscal (polling)

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):

StatuserrorQuando
409invoice_not_replaceableO documento não é NF-e validada e vigente, ou já é histórico. Rascunho se descarta, não se troca.
409invoice_replacement_in_flightJá existe uma troca em aberto para esse documento.
409chave_already_usedA chave nova já está vinculada a outro pedido.
422same_invoice_keyA 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 tem replaced_at. Lá a nota substituída aparece indistinguível da atual (e has_fiscal_document conta as duas). Quem precisa saber qual vale usa GET /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/fiscal

Se declaration_available=true, dispara:

POST /api/v1/sellers/orders/ORD-000123/fiscal/declaration

Polling → 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.