Exemplos de código
Clientes mínimos que emitem uma nota e acompanham até o estado final, nas três linguagens mais comuns em integrações. São pontos de partida: ajuste o tratamento de erros e o armazenamento ao seu sistema. Todos seguem as boas práticas: ref estável, repetição com espera crescente e tratamento do Retry-After.
Defina a base e a chave pelo ambiente:
export EMISSOR_URL="https://srv1839254.hstgr.cloud/api/v2"
export EMISSOR_CHAVE="nfe_SUA_CHAVE_AQUI"
PHP
Só com a extensão curl (sem dependências):
<?php
final class EmissorNfe
{
public function __construct(private string $base, private string $chave) {}
/** @return array{status: int, corpo: array} */
private function chamar(string $metodo, string $caminho, ?array $json = null): array
{
for ($tentativa = 0; ; $tentativa++) {
$ch = curl_init($this->base.$caminho);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $metodo,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$this->chave, 'Accept: application/json', 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $json === null ? null : json_encode($json, JSON_UNESCAPED_UNICODE),
]);
$resposta = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$tamanhoCabecalho = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
// Falha de rede, 5xx ou 429: repete com espera crescente. É seguro porque a ref é idempotente.
$repetir = $resposta === false || $status >= 500 || $status === 429;
if ($repetir && $tentativa < 6) {
preg_match('/^Retry-After:\s*(\d+)/mi', (string) $resposta, $m);
sleep(isset($m[1]) ? (int) $m[1] : min(60, 2 ** $tentativa));
continue;
}
return ['status' => $status, 'corpo' => (array) json_decode(substr((string) $resposta, $tamanhoCabecalho), true)];
}
}
/** Emite e espera o estado final. Devolve a nota. */
public function emitir(string $ref, array $venda, int $esperaMaxima = 120): array
{
$r = $this->chamar('POST', '/nfe?ref='.rawurlencode($ref), $venda);
if ($r['status'] === 422 || $r['status'] === 409) {
throw new RuntimeException($r['corpo']['codigo'].': '.$r['corpo']['mensagem']); // corrija e reenvie: não adianta repetir igual
}
$nota = $r['corpo'];
$limite = time() + $esperaMaxima;
$final = ['autorizado', 'cancelado', 'denegado', 'inutilizado', 'erro_autorizacao', 'erro_validacao'];
for ($espera = 2; ! in_array($nota['status'] ?? '', $final, true) && time() < $limite; $espera = min($espera * 2, 15)) {
sleep($espera);
$nota = $this->chamar('GET', '/nfe/'.rawurlencode($ref).'?cnpj_emitente='.$venda['cnpj_emitente'])['corpo'];
}
return $nota;
}
}
$api = new EmissorNfe(getenv('EMISSOR_URL'), getenv('EMISSOR_CHAVE'));
$nota = $api->emitir('PEDIDO-1001', [
'cnpj_emitente' => '11222333000181',
'nome_destinatario' => 'Maria da Silva', 'cpf_destinatario' => '529.982.247-25',
'logradouro_destinatario' => 'Av Atlantica', 'numero_destinatario' => '500', 'bairro_destinatario' => 'Copacabana',
'municipio_destinatario' => 'Rio de Janeiro', 'codigo_municipio_destinatario' => '3304557', 'uf_destinatario' => 'RJ', 'cep_destinatario' => '22010-000',
'items' => [[
'codigo_produto' => 'CAMISA-P', 'descricao' => 'Camiseta basica P', 'codigo_ncm' => '61091000', 'icms_origem' => 0,
'quantidade_comercial' => 1, 'valor_unitario_comercial' => 100.00,
]],
'formas_pagamento' => [['forma_pagamento' => '17']],
]);
echo $nota['status'].' '.($nota['chave_nfe'] ?? '').PHP_EOL;
Node.js
Node 18 ou mais novo (usa o fetch nativo):
const BASE = process.env.EMISSOR_URL;
const CHAVE = process.env.EMISSOR_CHAVE;
const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
const FINAIS = ['autorizado', 'cancelado', 'denegado', 'inutilizado', 'erro_autorizacao', 'erro_validacao'];
async function chamar(metodo, caminho, corpo) {
for (let tentativa = 0; ; tentativa++) {
let resposta;
try {
resposta = await fetch(BASE + caminho, {
method: metodo,
headers: { Authorization: `Bearer ${CHAVE}`, Accept: 'application/json', 'Content-Type': 'application/json' },
body: corpo ? JSON.stringify(corpo) : undefined,
signal: AbortSignal.timeout(30_000),
});
} catch (e) {
resposta = null; // falha de rede ou tempo esgotado
}
// Falha de rede, 5xx ou 429: repete com espera crescente (seguro: a ref é idempotente)
if ((resposta === null || resposta.status >= 500 || resposta.status === 429) && tentativa < 6) {
const retryAfter = Number(resposta?.headers.get('Retry-After'));
await dormir((retryAfter || Math.min(60, 2 ** tentativa)) * 1000);
continue;
}
if (resposta === null) throw new Error('sem resposta da API');
return { status: resposta.status, corpo: await resposta.json() };
}
}
async function emitir(ref, venda, esperaMaximaSeg = 120) {
const r = await chamar('POST', `/nfe?ref=${encodeURIComponent(ref)}`, venda);
if (r.status === 422 || r.status === 409) throw new Error(`${r.corpo.codigo}: ${r.corpo.mensagem}`); // corrija e reenvie
let nota = r.corpo;
const limite = Date.now() + esperaMaximaSeg * 1000;
for (let espera = 2; !FINAIS.includes(nota.status) && Date.now() < limite; espera = Math.min(espera * 2, 15)) {
await dormir(espera * 1000);
nota = (await chamar('GET', `/nfe/${encodeURIComponent(ref)}?cnpj_emitente=${venda.cnpj_emitente}`)).corpo;
}
return nota;
}
emitir('PEDIDO-1001', {
cnpj_emitente: '11222333000181',
nome_destinatario: 'Maria da Silva', cpf_destinatario: '529.982.247-25',
logradouro_destinatario: 'Av Atlantica', numero_destinatario: '500', bairro_destinatario: 'Copacabana',
municipio_destinatario: 'Rio de Janeiro', codigo_municipio_destinatario: '3304557', uf_destinatario: 'RJ', cep_destinatario: '22010-000',
items: [{ codigo_produto: 'CAMISA-P', descricao: 'Camiseta basica P', codigo_ncm: '61091000', icms_origem: 0, quantidade_comercial: 1, valor_unitario_comercial: 100.0 }],
formas_pagamento: [{ forma_pagamento: '17' }],
}).then((nota) => console.log(nota.status, nota.chave_nfe));
Python
Com a biblioteca requests (pip install requests):
import os
import time
import requests
BASE = os.environ['EMISSOR_URL']
CABECALHOS = {'Authorization': f"Bearer {os.environ['EMISSOR_CHAVE']}", 'Accept': 'application/json'}
FINAIS = {'autorizado', 'cancelado', 'denegado', 'inutilizado', 'erro_autorizacao', 'erro_validacao'}
def chamar(metodo, caminho, corpo=None):
for tentativa in range(7):
try:
r = requests.request(metodo, BASE + caminho, json=corpo, headers=CABECALHOS, timeout=30)
except requests.RequestException:
r = None # falha de rede ou tempo esgotado
# Falha de rede, 5xx ou 429: repete com espera crescente (seguro: a ref é idempotente)
if (r is None or r.status_code >= 500 or r.status_code == 429) and tentativa < 6:
retry_after = r.headers.get('Retry-After') if r is not None else None
time.sleep(int(retry_after) if retry_after else min(60, 2 ** tentativa))
continue
if r is None:
raise RuntimeError('sem resposta da API')
return r.status_code, r.json()
def emitir(ref, venda, espera_maxima=120):
status, nota = chamar('POST', f'/nfe?ref={ref}', venda)
if status in (422, 409):
raise RuntimeError(f"{nota['codigo']}: {nota['mensagem']}") # corrija e reenvie: não adianta repetir igual
limite, espera = time.time() + espera_maxima, 2
while nota['status'] not in FINAIS and time.time() < limite:
time.sleep(espera)
espera = min(espera * 2, 15)
_, nota = chamar('GET', f"/nfe/{ref}?cnpj_emitente={venda['cnpj_emitente']}")
return nota
nota = emitir('PEDIDO-1001', {
'cnpj_emitente': '11222333000181',
'nome_destinatario': 'Maria da Silva', 'cpf_destinatario': '529.982.247-25',
'logradouro_destinatario': 'Av Atlantica', 'numero_destinatario': '500', 'bairro_destinatario': 'Copacabana',
'municipio_destinatario': 'Rio de Janeiro', 'codigo_municipio_destinatario': '3304557', 'uf_destinatario': 'RJ', 'cep_destinatario': '22010-000',
'items': [{'codigo_produto': 'CAMISA-P', 'descricao': 'Camiseta basica P', 'codigo_ncm': '61091000', 'icms_origem': 0,
'quantidade_comercial': 1, 'valor_unitario_comercial': 100.00}],
'formas_pagamento': [{'forma_pagamento': '17'}],
})
print(nota['status'], nota.get('chave_nfe'))
Baixar o XML autorizado
Com a nota autorizado, use o link devolvido em caminho_xml_nota_fiscal (ele exige a mesma chave de API). Guarde o arquivo: é o documento fiscal.
curl -s -o "NFe-PEDIDO-1001.xml" "$EMISSOR_URL/nfe/PEDIDO-1001/xml?cnpj_emitente=11222333000181" -H "Authorization: Bearer $EMISSOR_CHAVE"
curl -s -o "DANFE-PEDIDO-1001.pdf" "$EMISSOR_URL/nfe/PEDIDO-1001/danfe?cnpj_emitente=11222333000181" -H "Authorization: Bearer $EMISSOR_CHAVE"
Cancelar e corrigir
# cancelar (nota autorizada, dentro do prazo)
curl -s -X DELETE "$EMISSOR_URL/nfe/PEDIDO-1001?cnpj_emitente=11222333000181" -H "Authorization: Bearer $EMISSOR_CHAVE" -H "Content-Type: application/json" \
-d '{"justificativa": "Cliente desistiu da compra antes do envio", "confirmo": true}'
# carta de correção
curl -s -X POST "$EMISSOR_URL/nfe/PEDIDO-1001/carta_correcao?cnpj_emitente=11222333000181" -H "Authorization: Bearer $EMISSOR_CHAVE" -H "Content-Type: application/json" \
-d '{"correcao": "Correcao do complemento do endereco de entrega para Apto 12"}'
Receber webhooks
Os receptores prontos (PHP, Node.js e Python) com a conferência da assinatura estão em Webhooks.