Integração de NF-e passo a passo (cURL)

Do sistema externo (ERP, loja, PDV) até a nota autorizada: buscar pedido, cliente e produtos, cadastrar na API, emitir, acompanhar e baixar XML/DANFE.

0Antes de começar

O que você precisa ter (feito uma vez, no painel da API):

Variáveis usadas em todos os passos:

export API="https://www.fiscal.versianecode.com.br/api/v1"        # URL da API desta instalação
export TOKEN="nfk_SEU_TOKEN"                    # nunca coloque o token em código público
export EMPRESA="EMPRESA_001"                    # código da empresa (omita se o token enxerga só uma)
export ERP="https://erp.exemplo.com/api"        # API do SEU sistema (loja/ERP) - exemplo
export ERP_TOKEN="token-do-seu-erp"
Todas as chamadas usam Authorization: Bearer $TOKEN (ou X-API-Key). Toda resposta traz request_id: informe-o ao suporte se algo falhar.
Segurança: chame a API sempre do seu servidor (cron, backend, fila). Nunca do navegador do cliente final: o token ficaria exposto.

1Testar acesso, saldo e configuração

# saldo de créditos
curl -s "$API/financeiro/saldo" -H "Authorization: Bearer $TOKEN" | jq

# a SEFAZ está respondendo?
curl -s "$API/sefaz/status" -H "Authorization: Bearer $TOKEN" | jq

# a empresa está pronta para emitir? (emitente, certificado, natureza, tributação)
curl -s "$API/configuracao-fiscal/validar?empresa=$EMPRESA" -H "Authorization: Bearer $TOKEN" | jq

Se validar responder 422 CONFIG_FISCAL_INCOMPLETA, a lista error.details diz exatamente o que configurar antes de emitir.

2Ler pedido, cliente e produtos do sistema externo

A API fiscal não busca nada sozinha: seu sistema envia os dados comerciais e ela monta o documento fiscal. Os exemplos abaixo usam um ERP/loja fictício com API REST; troque URLs e nomes de campos pelos do seu sistema (WooCommerce, Bling, Tiny, Omie, ERP próprio, banco de dados etc.).

2.1 Pedido (inclui itens e comprador)

PEDIDO_ID=12345
curl -s "$ERP/pedidos/$PEDIDO_ID" -H "Authorization: Bearer $ERP_TOKEN" -o pedido.json
jq . pedido.json

Resposta típica do sistema externo (exemplo):

{
  "id": 12345,
  "cliente_id": 77,
  "forma_pagamento": "pix",
  "itens": [
    { "sku": "CAMISA-AZUL-M", "quantidade": 2, "preco": 59.90 },
    { "sku": "BONE-PRETO",    "quantidade": 1, "preco": 35.00 }
  ]
}

2.2 Cliente do pedido

CLIENTE_ID=$(jq -r '.cliente_id' pedido.json)
curl -s "$ERP/clientes/$CLIENTE_ID" -H "Authorization: Bearer $ERP_TOKEN" -o cliente.json
jq . cliente.json
{
  "nome": "Maria da Silva", "cpf_cnpj": "123.456.789-09", "email": "maria@exemplo.com", "telefone": "(31) 99999-0000",
  "ie": "", "logradouro": "Rua das Flores", "numero": "100", "bairro": "Centro",
  "cod_municipio_ibge": "3106200", "cidade": "Belo Horizonte", "uf": "MG", "cep": "30110-000"
}

2.3 Produtos de cada item

mkdir -p produtos
for SKU in $(jq -r '.itens[].sku' pedido.json); do
  curl -s "$ERP/produtos/$SKU" -H "Authorization: Bearer $ERP_TOKEN" -o "produtos/$SKU.json"
done
jq . produtos/CAMISA-AZUL-M.json
{ "sku": "CAMISA-AZUL-M", "nome": "Camisa Azul M", "ncm": "61091000", "origem": 0, "unidade": "UN", "ean": "7891234567895", "preco": 59.90 }
Dado fiscal obrigatório: para cada produto a API precisa de NCM, origem (0 a 8) e unidade. Se o seu sistema não guarda o NCM, você precisa cadastrá-lo antes. A API não adivinha tributação: se faltar algo, responde FISCAL_CONFIGURATION_REQUIRED com a lista do que falta.

Outras fontes de dados

OrigemComo recuperar
API REST (loja/ERP)curl como acima; pagine listas com ?page=2 ou ?updated_since=…
Banco MySQLmysql -N -e "SELECT JSON_OBJECT(...) FROM pedidos WHERE id=12345" > pedido.json
CSV/planilhajq -R -s ou mlr --icsv --ojson cat arquivo.csv para converter em JSON
Webhook do seu sistemao ERP chama o seu servidor quando o pedido é pago; seu servidor executa os passos 3 a 7

3Cadastrar / atualizar os produtos na API

Os itens da nota referenciam o produto pelo codigo. Cadastre (uma vez por produto) com POST /produtos:

CampoObrig.Descrição
codigosimseu SKU (até 60); único por empresa
descricaosimaté 120 caracteres
ncmsim8 dígitos
origemsim0 a 8
unidadesimUN, KG, CX…
cest, gtin, preco, categorianãopreço é usado se o item do pedido não informar valor

3.1 Um produto

curl -s -X POST "$API/produtos" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "empresa": "'"$EMPRESA"'",
    "codigo": "CAMISA-AZUL-M", "descricao": "Camisa Azul M", "ncm": "61091000",
    "origem": "0", "unidade": "UN", "gtin": "7891234567895", "preco": 59.90
  }' | jq

Retorna 201 com o id. Se o código já existir: 409 REGISTRO_DUPLICADO (use o PUT abaixo).

3.2 Sincronizar todos os produtos do pedido (cria ou atualiza)

# mapa codigo -> id dos produtos já cadastrados na API
curl -s "$API/produtos?empresa=$EMPRESA" -H "Authorization: Bearer $TOKEN" \
  | jq 'reduce .itens[] as $p ({}; .[$p.codigo] = $p.id)' > produtos_api.json

for ARQ in produtos/*.json; do
  # converte o formato do seu sistema para o da API
  BODY=$(jq -c --arg e "$EMPRESA" '{empresa:$e, codigo:.sku, descricao:(.nome[0:120]), ncm:.ncm,
                                    origem:(.origem|tostring), unidade:.unidade, gtin:.ean, preco:.preco}' "$ARQ")
  COD=$(echo "$BODY" | jq -r .codigo)
  ID=$(jq -r --arg c "$COD" '.[$c] // empty' produtos_api.json)
  if [ -z "$ID" ]; then
    curl -s -X POST "$API/produtos" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY" | jq -c '{acao:"criado",codigo:.codigo,id:.id}'
  else
    curl -s -X PUT "$API/produtos/$ID" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$BODY" | jq -c '{acao:"atualizado",id:.id}'
  fi
done
Dica de performance: rode este passo fora do fluxo de emissão (ao cadastrar/editar o produto no seu sistema, ou uma vez por noite). No dia a dia a emissão só precisa dos passos 4 a 7.

4Cliente (destinatário)

Na NF-e o cliente pode ser enviado dentro do próprio pedido (recomendado para integrações: você não precisa cadastrar nada antes). Se o CPF/CNPJ já existir no cadastro da empresa, nome e endereço faltantes são completados por ele.

A NF-e exige endereço completo: logradouro, numero, bairro, cod_municipio (IBGE, 7 dígitos), municipio, uf, cep.

# converte o cliente do seu sistema para o formato da API (só dígitos em CPF/CNPJ/CEP)
jq -c '{
  cpf_cnpj: (.cpf_cnpj | gsub("[^0-9]";"")),
  nome: .nome, email: .email, fone: (.telefone | gsub("[^0-9]";"")),
  ie: (.ie // ""),
  endereco: { logradouro: .logradouro, numero: .numero, bairro: .bairro,
              cod_municipio: .cod_municipio_ibge, municipio: .cidade, uf: .uf,
              cep: (.cep | gsub("[^0-9]";"")) }
}' cliente.json > cliente_api.json
cat cliente_api.json

5Montar o JSON do pedido fiscal

O mesmo JSON serve para /fiscal/preview, /nfe e /nfce. Você envia só o comercial; CFOP, CST/CSOSN, ICMS, PIS, COFINS, IBS/CBS, totais, numeração e série vêm da configuração fiscal e do Motor Tributário.

# junta pedido + cliente + itens no formato da API
jq -c --arg e "$EMPRESA" --slurpfile c cliente_api.json '{
  empresa: $e,
  pedido: ("PED-" + (.id|tostring)),            # sua chave de idempotência: o mesmo pedido nunca gera 2 notas
  cliente: $c[0],
  itens: [ .itens[] | { codigo: .sku, quantidade: .quantidade, valor: .preco } ],
  pagamento: .forma_pagamento,                  # pix, dinheiro, cartao_credito, boleto...
  observacoes: ("Pedido " + (.id|tostring))
}' pedido.json > nfe.json
jq . nfe.json

Resultado (nfe.json):

{
  "empresa": "EMPRESA_001",
  "pedido": "PED-12345",
  "cliente": { "cpf_cnpj": "12345678909", "nome": "Maria da Silva", "email": "maria@exemplo.com",
               "endereco": { "logradouro": "Rua das Flores", "numero": "100", "bairro": "Centro",
                             "cod_municipio": "3106200", "municipio": "Belo Horizonte", "uf": "MG", "cep": "30110000" } },
  "itens": [ { "codigo": "CAMISA-AZUL-M", "quantidade": 2, "valor": 59.9 },
             { "codigo": "BONE-PRETO",    "quantidade": 1, "valor": 35 } ],
  "pagamento": "pix",
  "observacoes": "Pedido 12345"
}
Campo opcionalUso
itens[].descontodesconto em R$ do item
pagamento como lista[{"forma":"pix","valor":100},{"forma":"dinheiro","valor":50}]
naturezacódigo da natureza de operação (padrão: a da empresa)
frete.modalidade0,1,2,3,4,9 (padrão 9 = sem frete)
presencapresencial (padrão), internet, telefone, entrega

6Pré-visualizar (sem emitir, sem cobrar)

Valida tudo e mostra os impostos calculados. Nada é enviado à SEFAZ e nenhum crédito é consumido.

curl -s -X POST "$API/fiscal/preview" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "$(jq -c '. + {modelo: 55}' nfe.json)" | jq

Se estiver tudo certo vem "ready_to_issue": true com totais e impostos. Se faltar algo:

{ "success": false, "ready_to_issue": false,
  "error": { "code": "FISCAL_CONFIGURATION_REQUIRED",
             "missing": [ "NCM do produto BONE-PRETO", "produto XYZ nao cadastrado" ] } }

7Emitir a NF-e

7.1 Emissão direta (síncrona)

curl -s -X POST "$API/nfe" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: PED-12345" \
  -d @nfe.json | tee resposta.json | jq
{
  "success": true, "status": "AUTORIZADA", "id": "43", "numero": 1, "serie": 1,
  "chave": "3126096505750400019355001000000001...", "protocolo": "131260152908273",
  "xml": "/api/v1/nfe/43/xml", "danfe": "/api/v1/nfe/43/danfe", "request_id": "req_40d1822f3cdb8073b965"
}
Idempotência: reenviar o mesmo pedido (mesmo Idempotency-Key) devolve a nota já emitida, nunca uma segunda. Em caso de timeout, repita a mesma chamada com segurança.

7.2 Emissão assíncrona (recomendada para pedidos grandes ou alto volume)

A API aceita o pedido, devolve na hora e processa em segundo plano (com retentativas e consulta antes de reenviar à SEFAZ). Você é avisado por webhook ou consultando.

curl -s -X POST "$API/nfe" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: PED-12345" -H "Prefer: respond-async" \
  -d @nfe.json | tee resposta.json | jq
# 202 { "solicitacao_id": "sol_...", "status": "RECEBIDA", ... }

SOL=$(jq -r '.solicitacao_id' resposta.json)
curl -s "$API/fiscal/solicitacoes/$SOL" -H "Authorization: Bearer $TOKEN" | jq '{status, descricao_status, documento_id, chave}'

Estados finais: AUTORIZADA, REJEITADA, ACAO_NECESSARIA, CANCELADA. Os demais (RECEBIDA, TRANSMITINDO, AGUARDANDO_RETORNO, FALHA_TEMPORARIA…) significam "ainda processando": não reenvie com outro pedido.

8Acompanhar o resultado

8.1 Consulta direta

ID=$(jq -r '.id' resposta.json)
curl -s "$API/nfe/$ID" -H "Authorization: Bearer $TOKEN" | jq '{status, numero, chave, protocolo}'
curl -s "$API/nfe/$ID/eventos" -H "Authorization: Bearer $TOKEN" | jq

8.2 Espera simples (polling) até o resultado final

for i in $(seq 1 30); do
  ST=$(curl -s "$API/fiscal/solicitacoes/$SOL" -H "Authorization: Bearer $TOKEN" | jq -r .status)
  echo "tentativa $i: $ST"
  case "$ST" in AUTORIZADA|REJEITADA|ACAO_NECESSARIA|CANCELADA) break;; esac
  sleep 5
done

8.3 Webhook (melhor que polling)

Cadastre uma URL HTTPS do seu servidor para receber nfe.authorized, nfe.rejected, nfe.cancelled e fiscal.document.*, assinados com HMAC-SHA256:

curl -s -X POST "$API/webhooks" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"url":"https://seu-sistema.com/webhooks/fiscal","eventos":["nfe.authorized","nfe.rejected","nfe.cancelled"]}' | jq
# guarde o "segredo" devolvido (aparece uma única vez): valida a assinatura HMAC-SHA256 de "timestamp.corpo"

curl -s -X POST "$API/webhooks/ID/testar" -H "Authorization: Bearer $TOKEN" | jq   # envia um evento de teste

Detalhes da assinatura e do receptor: documentação de Webhooks.

9Baixar XML e DANFE (PDF)

curl -s "$API/nfe/$ID/xml"   -H "Authorization: Bearer $TOKEN" -o "nfe-$ID.xml"
curl -s "$API/nfe/$ID/danfe" -H "Authorization: Bearer $TOKEN" -o "danfe-$ID.pdf"

Envie o XML e o DANFE ao cliente por e-mail e grave o chave e o protocolo no pedido do seu sistema (para NFC-e use /nfce/ID/danfce).

Devolver o resultado ao sistema externo

curl -s -X PUT "$ERP/pedidos/12345" -H "Authorization: Bearer $ERP_TOKEN" -H "Content-Type: application/json" \
  -d "$(jq -c '{nfe_chave:.chave, nfe_numero:.numero, nfe_status:.status}' resposta.json)"

10Cancelar e Carta de Correção (CC-e)

# cancelamento (justificativa de 15 a 255 caracteres; prazo definido pela SEFAZ/UF)
curl -s -X POST "$API/nfe/$ID/cancelamento" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"justificativa":"Pedido cancelado a pedido do cliente antes da entrega"}' | jq

# carta de correção (não altera valores nem destinatário; até 20 por nota)
curl -s -X POST "$API/nfe/$ID/cce" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"correcao":"Corrigir o complemento do endereco de entrega para: Apto 302"}' | jq

# inutilizar uma faixa de números não usada
curl -s -X POST "$API/inutilizacao" -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"modelo":55,"serie":1,"numero_inicial":10,"numero_final":12,"justificativa":"Numeracao pulada por falha no sistema"}' | jq

Justificativa: 15 a 255 caracteres. Correção da CC-e: 15 a 1000 caracteres. Inutilização aceita modelo (55/65), serie (padrão da empresa), numero_inicial, numero_final e justificativa. O prazo de cancelamento depende da SEFAZ/UF.

11Script completo (pedido → nota autorizada)

Junta todos os passos: lê o pedido do sistema externo, sincroniza produtos, emite de forma assíncrona, aguarda e baixa XML + DANFE. Salve como emitir_nfe.sh e execute ./emitir_nfe.sh 12345.

#!/usr/bin/env bash
set -euo pipefail
: "${API:?}" "${TOKEN:?}" "${ERP:?}" "${ERP_TOKEN:?}"
EMPRESA="${EMPRESA:-}"
PEDIDO_ID="${1:?uso: $0 ID_DO_PEDIDO}"
H=(-H "Authorization: Bearer $TOKEN")
J=(-H "Content-Type: application/json")
E=(-H "Authorization: Bearer $ERP_TOKEN")
mkdir -p work && cd work

echo "1) lendo pedido, cliente e produtos no sistema externo"
curl -fsS "$ERP/pedidos/$PEDIDO_ID" "${E[@]}" -o pedido.json
curl -fsS "$ERP/clientes/$(jq -r .cliente_id pedido.json)" "${E[@]}" -o cliente.json
mkdir -p produtos
for SKU in $(jq -r '.itens[].sku' pedido.json); do
  curl -fsS "$ERP/produtos/$SKU" "${E[@]}" -o "produtos/$SKU.json"
done

echo "2) sincronizando produtos na API fiscal"
curl -fsS "$API/produtos${EMPRESA:+?empresa=$EMPRESA}" "${H[@]}" \
  | jq 'reduce .itens[] as $p ({}; .[$p.codigo] = $p.id)' > produtos_api.json
for ARQ in produtos/*.json; do
  BODY=$(jq -c --arg e "$EMPRESA" '{empresa:$e, codigo:.sku, descricao:(.nome[0:120]), ncm:.ncm,
        origem:(.origem|tostring), unidade:.unidade, gtin:.ean, preco:.preco}' "$ARQ")
  ID=$(jq -r --arg c "$(echo "$BODY" | jq -r .codigo)" '.[$c] // empty' produtos_api.json)
  if [ -z "$ID" ]; then curl -fsS -X POST "$API/produtos" "${H[@]}" "${J[@]}" -d "$BODY" >/dev/null
  else curl -fsS -X PUT "$API/produtos/$ID" "${H[@]}" "${J[@]}" -d "$BODY" >/dev/null; fi
done

echo "3) montando o JSON da NF-e"
jq -c '{cpf_cnpj:(.cpf_cnpj|gsub("[^0-9]";"")), nome, email, fone:(.telefone|gsub("[^0-9]";"")), ie:(.ie//""),
  endereco:{logradouro, numero, bairro, cod_municipio:.cod_municipio_ibge, municipio:.cidade, uf, cep:(.cep|gsub("[^0-9]";""))}}' cliente.json > cliente_api.json
jq -c --arg e "$EMPRESA" --slurpfile c cliente_api.json '{empresa:$e, pedido:("PED-"+(.id|tostring)), cliente:$c[0],
  itens:[.itens[]|{codigo:.sku, quantidade, valor:.preco}], pagamento:.forma_pagamento}' pedido.json > nfe.json

echo "4) pré-visualizando"
PRE=$(curl -sS -X POST "$API/fiscal/preview" "${H[@]}" "${J[@]}" -d "$(jq -c '.+{modelo:55}' nfe.json)")
if [ "$(echo "$PRE" | jq -r '.ready_to_issue // false')" != "true" ]; then
  echo "NAO ESTA PRONTO PARA EMITIR:"; echo "$PRE" | jq '.error'; exit 2
fi

echo "5) emitindo (assíncrono)"
curl -sS -X POST "$API/nfe" "${H[@]}" "${J[@]}" -H "Idempotency-Key: PED-$PEDIDO_ID" -H "Prefer: respond-async" -d @nfe.json > resposta.json
SOL=$(jq -r '.solicitacao_id // empty' resposta.json)

echo "6) aguardando o resultado"
DOC_ID=""
if [ -n "$SOL" ]; then
  for i in $(seq 1 40); do
    R=$(curl -sS "$API/fiscal/solicitacoes/$SOL" "${H[@]}"); ST=$(echo "$R" | jq -r .status)
    echo "   $ST"
    case "$ST" in
      AUTORIZADA) DOC_ID=$(echo "$R" | jq -r .documento_id); break;;
      REJEITADA|ACAO_NECESSARIA|CANCELADA) echo "$R" | jq; exit 3;;
    esac
    sleep 5
  done
else
  DOC_ID=$(jq -r .id resposta.json)   # a API respondeu direto (sincrono)
fi
[ -n "$DOC_ID" ] || { echo "sem resultado final ainda: consulte depois a solicitacao $SOL"; exit 4; }

echo "7) baixando XML e DANFE"
curl -fsS "$API/nfe/$DOC_ID/xml"   "${H[@]}" -o "nfe-$PEDIDO_ID.xml"
curl -fsS "$API/nfe/$DOC_ID/danfe" "${H[@]}" -o "danfe-$PEDIDO_ID.pdf"
curl -fsS "$API/nfe/$DOC_ID" "${H[@]}" | jq '{status, numero, chave, protocolo}'
echo "OK: work/nfe-$PEDIDO_ID.xml e work/danfe-$PEDIDO_ID.pdf"
Esse script é um ponto de partida: ajuste os nomes dos campos do seu sistema (passos 2.x) e agende com cron/fila (* * * * * ./emitir_nfe.sh $ID) ou chame-o a partir do webhook de "pedido pago" do seu ERP.

Erros mais comuns

HTTP / códigoSignificadoO que fazer
401 UNAUTHORIZEDtoken ausente, inválido ou revogadoconfira Authorization: Bearer
403 FORBIDDEN / EMPRESA_NAO_AUTORIZADAtoken sem a permissão ou sem acesso à empresaajuste as permissões/empresas no painel
402 SALDO_INSUFICIENTEsem créditosrecarregue e reenvie o mesmo pedido
422 VALIDATION_ERRORdado inválido (CPF/CNPJ, quantidade, pagamento…)leia error.errors[] (campo + mensagem)
422 FISCAL_CONFIGURATION_REQUIREDfalta NCM, regra tributária, cadastro, endereço…leia error.missing[], corrija e reenvie
422 REJEITADASEFAZ rejeitouveja error.sefaz_code e a mensagem; corrija e reenvie o mesmo pedido
409 REGISTRO_DUPLICADOproduto já cadastradouse PUT /produtos/{id}
409 REQUEST_IN_PROGRESSmesmo pedido ainda processandoaguarde e consulte
202 PENDENTESEFAZ demorounão use outro número de pedido; consulte GET /nfe/{id} ou reenvie o mesmo pedido

Teste sempre em homologação antes de apontar para produção. Documentação completa no portal da API (/api-docs).