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.