Ir para o conteúdo
OpenNota Desenvolvedores

OpenNota — API de emissão fiscal (v1)#

Referência de máquina: openapi.json (OpenAPI 3.1, gerado do código; os exemplos deste guia passam na validação real da API).

Estado desta versão — leia primeiro#

Recurso Situação
Autenticação, sandbox, idempotência, validação completa do pedido pronto (POST /v1/emissoes/validar)
Consulta e segunda via do XML pronto (GET /v1/emissoes…)
Transmissão ao órgão habilitada por modelo, nesta ordem: NFS-e, NFCom, NF-e. Até lá POST /v1/emissoes valida tudo e responde 503 MODELO_NAO_HABILITADO sem consumir número
PDF (DANFSe, DANFE-COM, DANFE) e link de segunda via para o consumidor junto com cada modelo
Cancelamento junto com cada modelo

O contrato abaixo é o definitivo: dá para escrever o cliente inteiro agora e testá-lo contra o /validar; quando cada modelo for habilitado, nada muda do seu lado.


1. Em uma frase#

Um endpoint só. POST https://api.opennota.com.br/v1/emissoes com modelo no corpo (NFCOM, NFSE ou NFE), o CNPJ do emitente e os dados do destinatário. O OpenNota monta o XML, assina com o A1 do emitente (guardado no cofre do OpenNota), transmite, guarda a nota e devolve número, chave, protocolo e XML.

O OpenNota não lê nem grava o banco do seu sistema. Você grava a nota no seu banco a partir da resposta. E o OpenNota também guarda tudo: a segunda via sai dele a qualquer momento (§8).

2. Emitente: por que só o CNPJ#

O emitente é cadastrado uma vez no OpenNota (plano administrativo): razão social, IE, IM, regime (CRT/Simples), endereço, padrões fiscais por modelo (CFOP, cClass, ICMS, FUST/FUNTTEL, cTribNac…), séries e o certificado A1 com a senha, cifrados. Por isso o pedido manda só emitente.cnpj:

A sua credencial recebe permissão para os CNPJs que ele pode usar. CNPJ fora da permissão responde 404 (igual a um CNPJ que não existe).

3. Ambientes e sandbox#

Mesmo endereço, credenciais diferentes. O ambiente é da credencial, nunca do corpo:

Sandbox Produção
Credencial key_id + segredo de homologação key_id + segredo de produção
Ambiente fiscal sempre HOMOLOGACAO (SVRS homologação / Sefin produção restrita) — sem valor fiscal PRODUCAO
Série própria de homologação (ex.: 99) — nunca queima número de produção a do emitente
O que enxerga só emissões de homologação só emissões de produção

A credencial de sandbox não abre produção: um .env de teste copiado por engano não emite nota de verdade. Não existe campo ambiente no corpo.

4. Autenticação (todas as rotas)#

Header Valor
X-OpenNota-Key key_id da credencial (público)
X-OpenNota-Timestamp epoch em segundos (janela ±300 s — mantenha o NTP em dia)
X-OpenNota-Nonce UUID v4 novo a cada requisição (reuso → 401 NONCE_REPETIDO)
X-OpenNota-Signature sha256= + hex(HMAC-SHA256(segredo, mensagem))
Content-Type application/json (só com corpo)
Idempotency-Key obrigatório em POST /v1/emissoes (§5)

Mensagem assinada — cinco partes separadas por ponto:

{timestamp}.{nonce}.{MÉTODO}.{path}.{corpo}

Nonce, método e path dentro da assinatura impedem reenviar uma requisição capturada e reaproveitar uma assinatura em outra rota.

Cliente Laravel (pronto para usar)#

// config/services.php
'opennota' => [
    'base'   => env('OPENNOTA_BASE', 'https://api.opennota.com.br'),
    'key_id' => env('OPENNOTA_KEY_ID'),       // sandbox ou produção
    'secret' => env('OPENNOTA_SECRET'),       // NUNCA no repositório nem em log
],
// app/Services/OpenNotaClient.php
namespace App\Services;

use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class OpenNotaClient
{
    private function headers(string $metodo, string $pathComQuery, string $corpo): array
    {
        $ts    = (string) time();
        $nonce = (string) Str::uuid();               // UUID v4
        $msg   = "{$ts}.{$nonce}." . strtoupper($metodo) . ".{$pathComQuery}.{$corpo}";
        $sig   = hash_hmac('sha256', $msg, config('services.opennota.secret'));
        return [
            'X-OpenNota-Key'       => config('services.opennota.key_id'),
            'X-OpenNota-Timestamp' => $ts,
            'X-OpenNota-Nonce'     => $nonce,
            'X-OpenNota-Signature' => "sha256={$sig}",
        ];
    }

    private function get(string $pathComQuery): Response
    {
        return Http::withHeaders($this->headers('GET', $pathComQuery, ''))
            ->timeout(20)
            ->get(config('services.opennota.base') . $pathComQuery);
    }

    /** Emite. $idempotencyKey: estável por nota, ex.: "fin-{$financeiro->id}-nfcom". */
    public function emitir(array $pedido, string $idempotencyKey): Response
    {
        $corpo = json_encode($pedido, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        return Http::withHeaders($this->headers('POST', '/v1/emissoes', $corpo) + ['Idempotency-Key' => $idempotencyKey])
            ->withBody($corpo, 'application/json')   // envia EXATAMENTE a string assinada
            ->timeout(90)                            // órgãos são síncronos e podem demorar
            ->post(config('services.opennota.base') . '/v1/emissoes');
    }

    /** Valida sem emitir (não consome número). */
    public function validar(array $pedido): Response
    {
        $corpo = json_encode($pedido, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        return Http::withHeaders($this->headers('POST', '/v1/emissoes/validar', $corpo))
            ->withBody($corpo, 'application/json')->timeout(20)
            ->post(config('services.opennota.base') . '/v1/emissoes/validar');
    }

    public function ping(): Response                     { return $this->get('/v1/ping'); }
    public function consultar(string $id): Response       { return $this->get("/v1/emissoes/{$id}"); }
    public function xml(string $id): Response             { return $this->get("/v1/emissoes/{$id}/xml"); }
    public function porReferencia(string $ref): Response
    {
        return $this->get('/v1/emissoes?referenciaExterna=' . rawurlencode($ref));
    }
}

Na query, assine o path já codificado (o mesmo texto que vai na URL). rawurlencode dos dois lados resolve.

Prova do signer (antes de chamar a API)#

Com o segredo de teste 0123456789abcdef0123456789abcdef (só para este teste), timestamp 1788200000, nonce 11111111-1111-4111-8111-111111111111, GET /v1/ping, corpo vazio:

mensagem:  1788200000.11111111-1111-4111-8111-111111111111.GET./v1/ping.
assinatura: sha256=b8eac8d2bd190ea28ef83881ede730d94a29eb5605d072a9c183de6a9aa649ed
printf '%s' '1788200000.11111111-1111-4111-8111-111111111111.GET./v1/ping.' \
  | openssl dgst -sha256 -hmac '0123456789abcdef0123456789abcdef' | sed 's/^.* //'

curl#

BASE=https://api.opennota.com.br; KEY='<key_id>'; SECRET='<segredo>'   # segredo vem do cofre
TS=$(date +%s); NONCE=$(uuidgen | tr 'A-Z' 'a-z'); P=/v1/ping
SIG=$(printf '%s' "$TS.$NONCE.GET.$P." | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -sS "$BASE$P" -H "X-OpenNota-Key: $KEY" -H "X-OpenNota-Timestamp: $TS" \
  -H "X-OpenNota-Nonce: $NONCE" -H "X-OpenNota-Signature: sha256=$SIG"

GET /v1/ping responde o ambiente da credencial, os emitentes permitidos (com documentos e operações) e as séries daquele ambiente. Use no início do command: se o ping falhar, não percorra a fila.

5. Idempotência — a garantia contra nota duplicada#

Idempotency-Key é obrigatório no POST /v1/emissoes (até 128 caracteres visíveis). Use um valor estável por nota, ex.: fin-3816978-nfcom.

Situação Resposta
chave nova processa normalmente
mesma chave + mesmo corpo devolve a resposta original (200 no lugar de 201), header Idempotent-Replay: true — não emite de novo
mesma chave + corpo diferente 409 IDEMPOTENCY_CONFLICT
mesma chave, primeira ainda processando 409 IDEMPOTENCY_IN_PROGRESS — espere e consulte
primeira terminou em 5xx a chave fica livre: pode repetir com o mesmo corpo

A ordem dos campos do JSON não importa para "mesmo corpo". Com isso, timeout + retentativa não gera nota duplicada (o problema que existia sem idempotência).

6. POST /v1/emissoes — o pedido#

Campos comuns (os três modelos)#

Campo Tipo Obrigatório Descrição
modelo "NFCOM" | "NFSE" | "NFE" sim NFCom 62 · NFS-e Nacional (DPS) · NF-e 55
emitente.cnpj 14 dígitos sim emitente cadastrado no OpenNota
destinatario objeto sim dados completos ou { "id": "…" } ou { "codigoExterno": "…" }
referenciaExterna texto ≤ 100 não livre; volta em toda resposta e permite buscar a nota (ex.: fin-3816978)
observacoes até 12 linhas ≤ 500 não informação complementar

Destinatário completo:

Campo Obrigatório Regra
documento sim CPF (11) ou CNPJ (14), só dígitos, DV conferido
nome sim ≤ 60 (o OpenNota trunca no XML conforme o leiaute de cada modelo)
nomeFantasia não
ie não só dígitos ou ISENTO
im não só para o DANFSe
email não
telefone sim na NFCOM DDD + número, só dígitos (10 ou 11)
endereco.logradouro, numero, bairro, municipio sim numero = "S/N" se não houver
endereco.complemento não
endereco.ibge sim 7 dígitos, da mesma UF
endereco.uf sim sigla
endereco.cep sim 8 dígitos
codigoExterno não id do cliente no seu sistema. Na NFCom vira o iCodAssinante. Com ele o OpenNota mantém o cadastro do destinatário, e os pedidos seguintes podem mandar só { "codigoExterno": "…" }

Um destinatário pertence a um emitente: o mesmo codigoExterno em dois emitentes são dois cadastros independentes. A nota guarda uma cópia do destinatário no momento da emissão — mudar o cadastro depois não altera nota emitida.

NFCOM (mensalidade de acesso — modelo 62)#

Campo Obrigatório Descrição
competencia sim mês de referência AAAA-MM
vencimento sim AAAA-MM-DD
contrato.numero / inicio / fim não padrão: documento do destinatário e 1º/último dia da competência
tipoServicoUtilizado não tpServUtil (4 = internet). Padrão do cadastro do emitente
itens[] sim (1–990)
itens[].codigo, descricao sim
itens[].cClass sim 7 dígitos (mensalidade de internet: 0400401)
itens[].unidade não 1 minuto · 2 MB · 3 GB · 4 UN (padrão)
itens[].quantidade, valorUnitario sim valor em reais, 2 casas
itens[].icms {cst, aliquota, fcp} não padrão do emitente (hoje CST 90, 20 %, FCP 4 %)
itens[].fust, funttel não padrão do emitente (1 % e 0,5 %; base líquida de ICMS, calculada pelo OpenNota)
fatura.codigoBarras não linha digitável do boleto (vai no gFat)
fatura.pix não copia-e-cola

Exemplo (o mesmo de docs/openapi.json):

{
  "modelo": "NFCOM",
  "emitente": { "cnpj": "11222333000181" },
  "destinatario": {
    "documento": "52998224725", "nome": "Fulano de Tal",
    "email": "fulano@example.com", "telefone": "21999998888",
    "endereco": { "logradouro": "Rua do Teste", "numero": "100", "complemento": "Casa 2",
                  "bairro": "Centro", "ibge": "3304557", "municipio": "Rio de Janeiro",
                  "uf": "RJ", "cep": "20040002" },
    "codigoExterno": "169162"
  },
  "referenciaExterna": "fin-3816978",
  "competencia": "2026-10",
  "vencimento": "2026-10-10",
  "contrato": { "numero": "169162", "inicio": "2026-10-01", "fim": "2026-10-31" },
  "tipoServicoUtilizado": 4,
  "itens": [{ "codigo": "3816978", "descricao": "Plano de acesso a Internet", "cClass": "0400401",
              "unidade": 4, "quantidade": 1, "valorUnitario": 99.9,
              "icms": { "cst": "90", "aliquota": 20, "fcp": 4 }, "fust": 1, "funttel": 0.5 }],
  "fatura": { "codigoBarras": "34191790010104351004791020150008291070026000" }
}

NFSE (serviço com ISS — NFS-e Nacional)#

Campo Obrigatório Descrição
competencia sim data AAAA-MM-DD
servico.cTribNac sim 6 dígitos. Rio, acesso e acessórios (instalação, visita, cabeamento, mudança de ponto): 010301
servico.cTribMun não Rio, acesso: 002
servico.cNBS não 9 dígitos
servico.descricao sim ≤ 2000
servico.municipioPrestacao sim IBGE do local da prestação
valores.servico sim > 0
valores.desconto não ≤ valor do serviço
valores.iss.retido sim
valores.iss.tributacao não tribISSQN, padrão 1
valores.iss.aliquota não só emitente não optante do Simples (o cadastro do emitente decide; optante usa pTotTribSN)
valores.federais não retenções PIS/COFINS/IRRF/CSLL/CP (tomador PJ)

Para outros serviços, informe o cTribNac e o cTribMun do seu serviço (confirme com a sua contabilidade).

NFE (mercadoria — modelo 55)#

naturezaOperacao, tipo (0/1), finalidade (1–4), consumidorFinal, presenca (indPres), destinoOperacao (1 interna · 2 interestadual · 3 exterior), itens[] (codigo, descricao, ncm 8 dígitos, cest, cfop, unidade, quantidade, valorUnitario, icms com cst OU csosn, ipi, pis.cst, cofins.cst), transporte.modalidade (9 = sem frete), pagamento[] (tPag 90 = sem pagamento, para remessa) e referencias[] (chaves de NF-e). Regras conferidas: CFOP coerente com tipo e destino (saída interna começa com 5, interestadual com 6…), soma dos pagamentos = total dos itens (exceto tPag 90), DV das chaves referenciadas.

7. Respostas#

Sucesso#

201 — autorizada:

{
  "emissao": {
    "id": "5b2c…", "modelo": "NFCOM", "ambiente": "PRODUCAO", "status": "AUTORIZADA",
    "emitenteCnpj": "…", "destinatarioId": "…", "serie": "2", "numero": "123",
    "chave": "33…", "protocolo": "3…", "valor": 99.9,
    "referenciaExterna": "fin-3816978", "idempotencyKey": "fin-3816978-nfcom",
    "origemFalha": null, "motivo": null,
    "criadaEm": "…", "autorizadaEm": "…",
    "links": { "self": "/v1/emissoes/5b2c…", "xml": "/v1/emissoes/5b2c…/xml" }
  },
  "xml": "<nfcomProc …>…</nfcomProc>"
}

202 — INDETERMINADA: a rede caiu depois de transmitir. O OpenNota consulta o órgão sozinho até ter certeza. Consulte GET /v1/emissoes/{id}; não reenvie com outra chave.

Erros#

Formato único:

{ "erro": { "codigo": "VALIDACAO_LOCAL", "mensagem": "…", "correlationId": "…",
            "origem": "VALIDACAO_LOCAL",
            "campos": [ { "campo": "/destinatario/telefone",
                          "mensagem": "telefone obrigatório na NFCom (NroTermPrinc)",
                          "correcao": "informe DDD + número, só dígitos" } ] } }
HTTP codigo Significado O que fazer
400 JSON_INVALIDO, PAYLOAD_INVALIDO, IDEMPOTENCY_KEY_OBRIGATORIA, FILTRO_OBRIGATORIO pedido malformado corrigir o código; não repetir
401 ASSINATURA_INVALIDA, TIMESTAMP_FORA_DA_JANELA, NONCE_REPETIDO, NONCE_INVALIDO, CREDENCIAL_AUSENTE autenticação conferir segredo, relógio, nonce novo
403 SEM_PERMISSAO, IP_NAO_PERMITIDO credencial sem a operação/modelo, ou IP fora da lista falar com o OpenNota
404 NAO_ENCONTRADO inexistente ou fora do escopo da credencial (emitente, emissão) conferir CNPJ/id
409 IDEMPOTENCY_CONFLICT, IDEMPOTENCY_IN_PROGRESS, XML_INDISPONIVEL ver §5 / nota sem XML
413 / 415 CORPO_GRANDE_DEMAIS / TIPO_DE_CONTEUDO_NAO_SUPORTADO > 256 KB / sem application/json
422 VALIDACAO_LOCAL cadastro a corrigir — campos[] diz qual e como. Nenhum número consumido corrigir o cadastro e reenviar
422 código do órgão (ex.: E0312, cStat) com origem: "ORGAO" rejeição da Sefin/SVRS corrigir; um número foi consumido (o OpenNota trata a lacuna)
429 LIMITE_DE_TAXA, CREDENCIAL_BLOQUEADA muitas requisições / muitas falhas de assinatura esperar Retry-After
503 MODELO_NAO_HABILITADO e indisponibilidades antes de transmitir nada foi transmitido repetir depois com a mesma chave
500 ERRO_INTERNO falha nossa informe o correlationId

Regra de retentativa: só 429, 503 e 202 (consultando) são transitórios. 4xx nunca se repete sem mudar algo. Com a mesma Idempotency-Key, repetir é sempre seguro.

8. Consulta e segunda via#

As notas ficam gravadas no OpenNota (XML autorizado, protocolo, eventos), com guarda legal de 5 anos. A segunda via sai daqui a qualquer momento, mesmo que o seu sistema perca o arquivo.

Rota Retorno
GET /v1/emissoes/{id} situação (status, número, chave, motivo da rejeição…)
GET /v1/emissoes/{id}/xml XML autorizado como anexo (application/xml) — nota AUTORIZADA ou CANCELADA; senão 409
GET /v1/emissoes?referenciaExterna=… (ou idempotencyKey=…, chave=…) lista, mais recente primeiro (limite até 50)
GET /v1/emissoes/{id}/pdf com cada modelo: DANFSe / DANFE-COM / DANFE gerado na hora
link público https://api.opennota.com.br/v1/d/{token} com cada modelo: link assinado e com validade para enviar ao consumidor

Cada credencial só enxerga as emissões do próprio cliente, dos emitentes que pode ler e do próprio ambiente (sandbox não vê produção e vice-versa).

9. Limites e regras de uso#