Guias da API

Como integrar a emissão de NF-e, NFC-e e NFS-e do CertFiscal. A referência completa de cada endpoint está no OpenAPI. Base da API: https://api.certfiscal.com.br/v1.

Autenticação & ambientes

Toda requisição é autenticada por API key no header Authorization, no formato Bearer:

# confirma a chave e o ambiente
curl https://api.certfiscal.com.br/v1/me \
  -H "Authorization: Bearer SUA_CHAVE"

O prefixo da chave indica o ambiente:

PrefixoAmbienteValor fiscal
cf_test_…Sandbox (homologação)Não — notas de teste
cf_live_…ProduçãoSim — nota fiscal real

Crie e gerencie chaves no painel (API / Desenvolvedores). A chave aparece uma única vez na criação — guarde em variável de ambiente, nunca no código versionado.

Escopos

Cada chave carrega escopos que limitam o que ela pode fazer. A emissão exige o escopo de escrita do tipo correspondente:

EscopoPermite
nfce:write / nfce:readEmitir / ler NFC-e
nfe:write / nfe:readEmitir / cancelar / ler NF-e (e DANFE, XML, busca)
nfse:write / nfse:readEmitir / ler NFS-e
Para emitir notas de teste no sandbox, cadastre uma empresa em ambiente de homologação com seu certificado A1 — as notas não têm valor fiscal.

Quickstart: emitir uma NFC-e

A emissão parte de um pedido de venda. Dois passos:

1. Crie o pedido

curl -X POST https://api.certfiscal.com.br/v1/sales-orders \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "companyId": "comp_...",
    "items": [{ "description": "Produto 1", "unit": "UN", "quantity": 1, "unitPriceCents": 1000 }]
  }'

Itens usam unitPriceCents (centavos). Veja todos os campos em Emissão por tipo de nota.

2. Emita a NFC-e

curl -X POST https://api.certfiscal.com.br/v1/nfce \
  -H "Authorization: Bearer SUA_CHAVE" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "salesOrderId": "so_...",
    "payments": [{ "tipo": "01", "valor": 10.00 }]
  }'

A resposta traz a chave de acesso, o protocolo de autorização e o status. Recupere o DANFE em PDF (GET /v1/nfe/{id}/danfe) e o XML autorizado (GET /v1/nfe/{id}/xml).

NF-e (modelo 55) e NFS-e seguem o mesmo padrão em POST /v1/nfe e POST /v1/nfse.

Idempotência

A numeração fiscal é sequencial: um retry de rede sem proteção duplicaria a nota. Por isso toda emissão exige o header Idempotency-Key (um valor único por operação, ex.: um UUID).

Gere uma chave de idempotência nova por nota. Reaproveitar a mesma para notas diferentes faz a segunda retornar a primeira.

Erros

Erros vêm com status HTTP adequado e um corpo padronizado:

{ "error": { "code": "missing_scope", "message": "..." } }
HTTPcodeSignificado
401api_key_required / api_key_invalidChave ausente, inválida ou revogada
403public_api_not_enabledOrganização sem plano de API ativo
403missing_scopeA chave não tem o escopo exigido pelo endpoint
400idempotency_key_requiredEmissão sem o header Idempotency-Key
400validation_errorCorpo inválido (a mensagem detalha o campo)
429rate_limit_exceededExcedeu o limite de requisições (veja Limites)

Limites & planos

O rate limit é por chave e definido pelo plano da organização. As respostas trazem os headers:

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
Retry-After: 12   # presente no 429
PlanoMensalDocumentos/mêsExcedenteReq/min
StarterR$ 79200R$ 0,12120
GrowthR$ 2491.500R$ 0,10300
ScaleR$ 5996.000R$ 0,08600

CNPJs ilimitados em todos os planos. Acompanhe o consumo do mês (franquia, excedente) na aba Uso & Planos do painel ou em GET /v1/usage.

Webhooks

Registre uma URL https para receber um POST a cada evento de emissão. Eventos: nfce.issued, nfe.issued, nfse.issued.

Assinatura

Cada entrega traz o header X-CertFiscal-Signature no formato t=<timestamp>,v1=<hex>, onde v1 é o HMAC-SHA256 de timestamp + "." + corpo usando o secret retornado no cadastro. Valide antes de confiar no payload:

// Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function valido(corpoRaw, header, secret) {
  const [t, v1] = header.split(",").map(p => p.split("=")[1]);
  const esperado = createHmac("sha256", secret).update(t + "." + corpoRaw).digest("hex");
  return timingSafeEqual(Buffer.from(esperado), Buffer.from(v1));
}

Entrega & retry

A URL precisa ser pública e https. Endereços internos (localhost, IPs privados) são rejeitados por segurança.