0Antes de começar
O que você precisa ter (feito uma vez, no painel da API):
- Empresa emitente cadastrada, com certificado A1, série e ambiente (homologação para testar).
- Uma API Key / token (
nfk_…) com as permissõespreview,emitir,consultar,downloadeconfigurar(esta última só para cadastrar produtos). - Créditos na carteira (Painel → Financeiro) ou crédito ilimitado.
curlejqinstalados (jqlê e monta JSON nos exemplos).
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"
Authorization: Bearer $TOKEN (ou X-API-Key). Toda resposta traz request_id: informe-o ao suporte se algo falhar.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 }FISCAL_CONFIGURATION_REQUIRED com a lista do que falta.Outras fontes de dados
| Origem | Como recuperar |
|---|---|
| API REST (loja/ERP) | curl como acima; pagine listas com ?page=2 ou ?updated_since=… |
| Banco MySQL | mysql -N -e "SELECT JSON_OBJECT(...) FROM pedidos WHERE id=12345" > pedido.json |
| CSV/planilha | jq -R -s ou mlr --icsv --ojson cat arquivo.csv para converter em JSON |
| Webhook do seu sistema | o 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:
| Campo | Obrig. | Descrição |
|---|---|---|
codigo | sim | seu SKU (até 60); único por empresa |
descricao | sim | até 120 caracteres |
ncm | sim | 8 dígitos |
origem | sim | 0 a 8 |
unidade | sim | UN, KG, CX… |
cest, gtin, preco, categoria | não | preç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
}' | jqRetorna 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
done4Cliente (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- Pessoa física (CPF) ou não contribuinte: a API marca consumidor final automaticamente.
- Empresa contribuinte: envie
ie(inscrição estadual). - Para NFC-e (modelo 65) o cliente é opcional e não precisa de endereço.
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.jsonResultado (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 opcional | Uso |
|---|---|
itens[].desconto | desconto em R$ do item |
pagamento como lista | [{"forma":"pix","valor":100},{"forma":"dinheiro","valor":50}] |
natureza | código da natureza de operação (padrão: a da empresa) |
frete.modalidade | 0,1,2,3,4,9 (padrão 9 = sem frete) |
presenca | presencial (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)" | jqSe 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"
}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" | jq8.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 testeDetalhes 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"}' | jqJustificativa: 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"* * * * * ./emitir_nfe.sh $ID) ou chame-o a partir do webhook de "pedido pago" do seu ERP.Erros mais comuns
| HTTP / código | Significado | O que fazer |
|---|---|---|
401 UNAUTHORIZED | token ausente, inválido ou revogado | confira Authorization: Bearer |
403 FORBIDDEN / EMPRESA_NAO_AUTORIZADA | token sem a permissão ou sem acesso à empresa | ajuste as permissões/empresas no painel |
402 SALDO_INSUFICIENTE | sem créditos | recarregue e reenvie o mesmo pedido |
422 VALIDATION_ERROR | dado inválido (CPF/CNPJ, quantidade, pagamento…) | leia error.errors[] (campo + mensagem) |
422 FISCAL_CONFIGURATION_REQUIRED | falta NCM, regra tributária, cadastro, endereço… | leia error.missing[], corrija e reenvie |
422 REJEITADA | SEFAZ rejeitou | veja error.sefaz_code e a mensagem; corrija e reenvie o mesmo pedido |
409 REGISTRO_DUPLICADO | produto já cadastrado | use PUT /produtos/{id} |
409 REQUEST_IN_PROGRESS | mesmo pedido ainda processando | aguarde e consulte |
202 PENDENTE | SEFAZ demorou | nã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).