Guia de integração

Esta é a versão em texto da referência, lida do documento OpenAPI — é também o que o curl e os buscadores leem. A versão interativa carrega em seguida, se o navegador executar JavaScript.

API de NFS-e — Ext Contabilidade

Versão 1.0.0

Endereço base
https://api.extcontabilidade.com.br

Autenticação

Authorization: Bearer ext_sk_...

Chave de API da empresa (ext_sk_...).

Rotas

GET/v1/nfse

Buscar NFS-e por referência

Encontra a nota pelo reference que você enviou na emissão — o caminho de volta quando o id se perde.

Devolve um envelope de lista com no máximo uma nota, já que reference é único por empresa dentro de cada modo. Sem resultado, 200 com data vazio — nunca 404. Como toda leitura desta API, é escopada pelo MODO da chave.

Parâmetros de consulta

  • referencestringobrigatório

    O reference que você enviou na emissão. Obrigatório — esta rota é busca por referência, não listagem: sem ele a resposta é 400 invalid_request.

    Como reference é único por empresa dentro de cada modo, o resultado tem no máximo uma nota. Sem resultado, é 200 com data vazio.

    Exemplo: INV-2026-0042

Respostas

  • 200Envelope de lista. `data` traz a nota encontrada, ou vem vazio.
  • 400Payload inválido: campo mal formatado, ausente ou fora do domínio aceito. Corrigir e reenviar resolve.
  • 401Chave de API ausente, mal formada, revogada ou inexistente. Nenhuma nota é criada.
  • 403A chave é válida mas não pode fazer isto: falta o escopo exigido, ou a API pública está desligada para a empresa.
  • 429Limite de requisições por chave excedido. Aguarde o que o header `Retry-After` indicar.
  • 500Falha nossa. `retryable` é `true`: repita com a MESMA `Idempotency-Key` para não duplicar a nota, e cite o `request_id` se persistir.
POST/v1/nfse

Emitir NFS-e

Com uma chave ext_sk_live_, esta chamada emite uma NFS-e real, e desfazê-la exige cancelamento formal, que tem prazo. Para ensaiar a integração inteira use ext_sk_test_: mesmo corpo, mesmo 202, mesmo polling, com livemode: false.

Devolve 202 na hora — a emissão é assíncrona. O Location traz a rota de consulta e o Retry-After, o intervalo de polling. O 202 diz ACEITA, não autorizada: acompanhe o status até issued ou failed. Recusas de cadastro chegam aqui, de forma síncrona, antes do 202.

O que o teste não prova

A nota de teste sai na produção restrita do Emissor Nacional: outro ambiente, com cadastro municipal próprio. Ela é emitida de verdade e pode falhar — e uma rejeição de lá é informação sobre o ambiente, não sobre o seu payload. Fora do alcance do teste ficam:

  • Diferenças de cadastro municipal entre os dois ambientes: um município pode estar habilitado num e não no outro, ou exigir dados diferentes em cada um. Nota aceita no teste pode ser rejeitada em produção, e o contrário também.
  • Se o código de tributação bate com o serviço que a sua description narra: ele é resolvido pelo CNAE, nunca pelo texto. Os dois ambientes aceitam a nota do mesmo jeito.
  • Recusa de cancelamento por janela: a nota de teste é apagada em 7 dias, então ensaiar a recusa exige emitir no fim do mês e cancelar nos primeiros dias do seguinte. Fora disso a nota já expirou, e a resposta é 404, não nfse.cancel_window_closed.
  • cancellation.status: "failed" é permanente em produção — um novo POST /cancel responde 409 nfse.cancel_in_progress, sem saída pela API —, mas libera nova tentativa no modo de teste. Não use o teste para prever a produção neste ponto.
  • A RECUSA do cancelamento chega diferente: no teste, cancellation.error.message traz o motivo do SEFIN; em produção é sempre a genérica "não recebemos a confirmação", porque lá a incerteza é real. Não faça parsing da mensagem — trate pelo cancellation.status.

Cabeçalhos

  • Idempotency-Keystring

    Chave escolhida por você para tornar a emissão segura de repetir. Opcional, e altamente recomendada em qualquer cliente com retry automático. Use uma chave nova por nota.

    • Mesmo corpo + mesma chave devolve a primeira resposta, sem emitir de novo — é o que torna o retry de rede seguro.
    • Corpo diferente com a mesma chave é 409 idempotency_key_reuse, por toda a janela de 24h: a chave identifica UMA emissão.
    • Mesmo corpo, primeira ainda em voo, é 409 idempotency_key_in_use, que é repetível: aguarde e consulte antes de reenviar.
    • Janela de 24h. Passada, a chave é esquecida e o mesmo corpo emite uma segunda nota.

    Sem o header não há replay. A API ainda barra duplicata evidente — mesmo valor e mesma descrição em menos de um minuto — com 409 duplicate_suspected, mas essa rede é frouxa: não pega o retry que chega depois do minuto nem duas chamadas simultâneas, e recusa duas notas legitimamente iguais no mesmo minuto.

    Melhor ainda: mande reference. É a única proteção que não expira — sendo único por empresa, a segunda emissão com a mesma referência é 409 nfse.reference_in_use, trazendo o id da original. Os dois se somam: a Idempotency-Key REPETE a resposta no retry imediato; o reference RECUSA a segunda nota para sempre.

    Exemplo: a3f1c9e2-7b64-4d18-9f02-5c8e1d7a6b30

Corpo da requisição

  • amount_in_centsintegerobrigatório

    Valor total do serviço em centavos, inteiro. R$ 1.250,50 se envia como 125050 — nunca 1250.50, que é recusado com 400 em vez de virar R$ 12,50 em silêncio. Mínimo 100 (R$ 1,00), máximo 9007199254740991. É o valor que vai para a nota, sem retenções.

    Exemplo: 125050

  • descriptionstringobrigatório

    Descrição do serviço, impressa na nota. Entre 1 e 2000 caracteres — o teto é da nota fiscal, não nosso.

    Sai como enviado, com UMA exceção: os marcadores {{data}}, {{mes}}, {{mes_extenso}}, {{mes_anterior}}, {{mes_anterior_extenso}} e {{ano}} são substituídos na emissão, resolvidos na data em que a chamada é recebida. Texto sem {{ passa intacto.

    Exemplo: Desenvolvimento de software sob encomenda — competência 07/2026

  • customerobjeto

    Tomador do serviço. Omitir o objeto inteiro emite uma nota sem tomador, que é válida.

    Alguns municípios exigem o tomador: neles, a nota sem customer.document é recusada na hora com 422 nfse.customer_required, antes do 202. Nos demais, nota sem tomador passa normalmente.

    Tomador no exterior é exportação de serviço, e sai por aqui: mande customer.type: "foreign" com customer.address completo, a identificação fiscal (customer.tax_id ou customer.tax_id_absence_reason) e foreign_amount. Sem o type, o tomador é tratado como brasileiro — omitir customer continua significando *nota sem tomador*, e não *tomador no exterior*.

  • customer.typestring

    Onde está o tomador. br (padrão) é o comportamento de sempre — identificação por CPF/CNPJ, operação doméstica tributável. foreign faz a nota sair como exportação de serviço: sem ISS, com o grupo de comércio exterior, e exigindo customer.address completo, customer.tax_id (ou customer.tax_id_absence_reason) e foreign_amount.

    Omitir o campo é o mesmo que br. Nenhum payload que funciona hoje muda de comportamento.

    Valores: br · foreign

    Exemplo: foreign

  • customer.documentstring

    CPF (11 dígitos) ou CNPJ (14), apenas dígitos — máscara com pontos, barra ou traço é RECUSADA com 400, não limpa.

    Exemplo: 12345678000190

  • customer.namestring

    Nome do tomador. Obrigatório quando customer.document é um CPF — o nome é impresso na nota, não há cadastro público de nome de pessoa física, e sem ele a nota é 422 nfse.customer_name_required. Em CNPJ é opcional: vence o nome do cadastro da Receita, e este campo é o fallback se a consulta não resolver.

    Exemplo: Maria Souza

  • customer.tax_idstring

    Número de identificação fiscal do tomador no país dele (NIF). Exportação exige este campo ou customer.tax_id_absence_reason — nunca os dois vazios.

    Exemplo: 98-7654321

  • customer.tax_id_absence_reasonstring

    Motivo de o tomador não ter identificação fiscal informada. exempt = dispensado; not_required = não exigência no país dele. Use quando não houver customer.tax_id.

    Valores: exempt · not_required

    Exemplo: not_required

  • customer.addressobjeto

    Endereço do tomador no exterior, completo. Obrigatório quando customer.type é foreign, e recusado quando é br — no caminho doméstico o endereço vem do cadastro da Receita, não do payload.

  • customer.address.streetstringobrigatório

    Logradouro do tomador no exterior (xLgr da DPS).

    Exemplo: Portland Street

  • customer.address.numberstringobrigatório

    Número do endereço (nro da DPS).

    Exemplo: 175

  • customer.address.districtstringobrigatório

    Bairro ou distrito (xBairro da DPS). Obrigatório pelo leiaute, mesmo onde o endereço local não usa bairro.

    Exemplo: Back Bay

  • customer.address.citystringobrigatório

    Cidade (xCidade da DPS).

    Exemplo: Boston

  • customer.address.statestringobrigatório

    Estado, província ou região (xEstProvReg da DPS). Texto livre — não é a sigla de UF brasileira.

    Exemplo: MA

  • customer.address.postal_codestringobrigatório

    Código postal no formato do país de destino (cEndPost da DPS). Formato livre: não é validado como CEP brasileiro.

    Exemplo: 02114

  • customer.address.countrystringobrigatório

    País do tomador em ISO 3166-1 alpha-2 — dois caracteres, como US, PT ou MZ. Vai para endExt/cPais e para tribMun/cPaisResult, o país em que o resultado do serviço se verifica. BR não é aceito: nota com tomador no Brasil não é exportação.

    Exemplo: US

  • foreign_amountobjeto

    Valor da nota na moeda do faturamento. Obrigatório quando customer.type é foreign; recusado fora da exportação.

    Não substitui amount_in_cents. Os dois são exigidos: o valor em reais é o que vai para a nota e para a apuração, e a API não converte câmbio — a taxa é decisão sua.

  • foreign_amount.currencystringobrigatório

    Sigla ISO de três letras da moeda do faturamento — USD, EUR, GBP. Não é o código numérico do BACEN: a tradução é nossa. Moeda fora da lista aceita é 422 nfse.currency_unsupported, com as siglas no corpo do erro.

    Exemplo: EUR

  • foreign_amount.amount_in_centsintegerobrigatório

    Valor na moeda estrangeira em centavos, número inteiro. 3.600,00 se envia como 360000.

    Duas casas decimais para qualquer moeda, inclusive as que não têm centavo (iene) ou têm três casas (dinar kuwaitiano): é o que a DPS recebe, e prometer outra precisão seria prometer o que a emissão não carrega.

    Exemplo: 360000

  • referencestring

    Seu identificador para esta nota — tipicamente o id da fatura no seu sistema. Opcional, e a forma mais confiável de reencontrar a nota: o id que devolvemos se perde se o seu processo morrer antes de gravá-lo, e a Idempotency-Key é esquecida em 24h.

    • Único por empresa, dentro de cada modo. Reenviar o mesmo é 409 nfse.reference_in_use, com o id da nota que já existe na mensagem — o retry nunca vira nota duplicada, sem prazo de validade. Precisando de duas notas para a mesma fatura, use referências diferentes.
    • A mesma referência serve uma vez em teste e uma em produção: os modos não se enxergam.
    • Consultável em GET /v1/nfse?reference=....
    • Até 64 caracteres, sem espaços — espaço é recusado em vez de aparado, porque "INV-1 " e "INV-1" seriam chaves diferentes.

    Com Idempotency-Key junto, os dois se somam: o retry imediato ganha o REPLAY da chave, e só uma referência de verdade repetida cai no 409.

    Exemplo: INV-2026-0042

  • cnaestring

    CNAE em que a nota deve sair, 7 dígitos, apenas números (a máscara 6202-3/00 é recusada com 400). Opcional.

    • Omitido, a nota sai na atividade principal — o certo para a maioria.
    • Enviado, precisa estar no cadastro da empresa na Receita Federal, principal ou secundária; um que não esteja é 422 nfse.cnae_not_allowed.
    • A nota sai na linha padrão daquele CNAE; precisando de outra, mande service_code junto. CNAE sem linha padrão é 422 nfse.cnae_without_default.

    O cnae e o service_code da resposta confirmam a linha em que a nota saiu.

    Exemplo: 6203100

  • service_codestring

    Código de tributação nacional da linha, 6 dígitos, apenas números. Opcional, e o desempate do cnae — não confunda os dois: aqui são 6 dígitos (010501), não os 7 do CNAE.

    A linha da nota sai desta tabela:

    o que você mandalinha da nota
    nadaa padrão da atividade principal da empresa
    cnaea padrão daquele CNAE
    cnae + service_codea linha exata
    service_codea primeira linha com aquele código, principal antes de secundária

    Mande o par quando o seu CNAE tiver mais de uma linha e você precisar da que não é a padrão. Código fora do catálogo é 422 nfse.service_code_unknown; código que existe mas não é da empresa, 422 nfse.service_code_not_allowed.

    Exemplo: 010501

Respostas

  • 202Nota aceita e enfileirada, `status` `queued`. Uma repetição atendida pela idempotência devolve este mesmo corpo, com o status que a nota original tiver no momento.
  • 400Payload inválido: campo mal formatado, ausente ou fora do domínio aceito. Corrigir e reenviar resolve.
  • 401Chave de API ausente, mal formada, revogada ou inexistente. Nenhuma nota é criada.
  • 403A chave é válida mas não pode fazer isto: falta o escopo exigido, ou a API pública está desligada para a empresa.
  • 409Conflito com uma requisição anterior — mesma `Idempotency-Key` ainda em voo, mesma `Idempotency-Key` com outro corpo, `reference` já usada, ou nota equivalente criada há menos de um minuto. Consulte antes de reenviar.
  • 422A requisição está bem formada mas não pode prosseguir: pendência de cadastro da empresa, ou município que exige o tomador. Reenviar sem mudar nada não resolve. (A `Idempotency-Key` reaproveitada com outro corpo saiu daqui e virou `409`: ela é conflito de estado da chave, não corpo improcessável.)
  • 429Limite de requisições por chave excedido. Aguarde o que o header `Retry-After` indicar.
  • 500Falha nossa. `retryable` é `true`: repita com a MESMA `Idempotency-Key` para não duplicar a nota, e cite o `request_id` se persistir.
POST/v1/nfse/{id}/cancel

Cancelar NFS-e

Cancelamento é definitivo, e com ext_sk_live_ a nota deixa de valer como documento fiscal.

Só cancela nota emitida no mês corrente: uma de 30/08 pode ser cancelada até 31/08, e em 01/09 não mais. Leia o instante exato em cancelable_until, não recalcule a regra. Fora da janela, o suporte ainda pode cancelar.

A resposta é 202: acompanhe o objeto cancellation. Ele é pending até o desfecho, succeeded quando o status chega a canceled, e failed quando não recebemos a confirmação — aí nunca reemita, acione o suporte para conferir o estado da nota.

Parâmetros de caminho

  • idstringobrigatório

    Id da nota, com ou sem o prefixo nfse_. O UUID cru funciona; um prefixo de outro tipo de recurso é 400, não 404.

    Exemplo: nfse_9f8e7d6c-5b4a-4321-9876-0a1b2c3d4e5f

Corpo da requisição

  • reasonstring

    Motivo do cancelamento, registrado no evento fiscal. Omitido, assume other.

    Valores: issuance_error · service_not_provided · other

    Exemplo: issuance_error

  • descriptionstring

    Justificativa livre, de 15 a 255 caracteres — o limite é da nota fiscal, não nosso, e fora dele o cancelamento é recusado. Validamos aqui para a recusa chegar na hora, e não minutos depois com a nota presa. Omitida, usamos uma padrão.

    Exemplo: Valor lançado errado no pedido 4471.

Respostas

  • 202Pedido aceito. `cancellation.status` é `pending`; consulte a nota até ele virar `succeeded` ou `failed`.
  • 400Payload inválido: campo mal formatado, ausente ou fora do domínio aceito. Corrigir e reenviar resolve.
  • 401Chave de API ausente, mal formada, revogada ou inexistente. Nenhuma nota é criada.
  • 403A chave é válida mas não pode fazer isto: falta o escopo exigido, ou a API pública está desligada para a empresa.
  • 404Não existe nota com este id para a empresa da chave. Mesmo corpo para id inexistente e para nota de outra empresa.
  • 409A nota não está em estado de ser cancelada: já cancelada (`nfse.already_canceled`), com pedido em andamento (`nfse.cancel_in_progress`) ou não cancelável por esta API (`nfse.not_cancelable`). Nenhum dos três se repete.
  • 422A nota é de um mês anterior (`nfse.cancel_window_closed`). Pela API, o cancelamento só vale dentro do mês da emissão.
  • 429Limite de requisições por chave excedido. Aguarde o que o header `Retry-After` indicar.
  • 500Falha nossa. `retryable` é `true`: repita com a MESMA `Idempotency-Key` para não duplicar a nota, e cite o `request_id` se persistir.
GET/v1/nfse/{id}

Consultar NFS-e

Destino do Location do 202. Consulte até o status chegar a issued ou failed, respeitando o Retry-After.

Escopada pelo MODO da chave: a de teste enxerga só notas de teste, e a de produção só notas reais. O que não é do modo responde 404, igual a um id inexistente.

Parâmetros de caminho

  • idstringobrigatório

    Id da nota, com ou sem o prefixo nfse_. O UUID cru funciona; um prefixo de outro tipo de recurso é 400, não 404.

    Exemplo: nfse_9f8e7d6c-5b4a-4321-9876-0a1b2c3d4e5f

Respostas

  • 200Estado atual da nota.
  • 400Payload inválido: campo mal formatado, ausente ou fora do domínio aceito. Corrigir e reenviar resolve.
  • 401Chave de API ausente, mal formada, revogada ou inexistente. Nenhuma nota é criada.
  • 403A chave é válida mas não pode fazer isto: falta o escopo exigido, ou a API pública está desligada para a empresa.
  • 404Não existe nota com este id para a empresa da chave. Mesmo corpo para id inexistente e para nota de outra empresa.
  • 429Limite de requisições por chave excedido. Aguarde o que o header `Retry-After` indicar.
  • 500Falha nossa. `retryable` é `true`: repita com a MESMA `Idempotency-Key` para não duplicar a nota, e cite o `request_id` se persistir.
GET/v1/nfse/{id}/pdf

Baixar o PDF da nota

Devolve os BYTES do PDF no corpo, nunca um redirect para URL assinada. Aceita a chave de API ou o token temporário que o links.pdf da consulta já traz na query: ele vale 15 minutos, serve só esta nota, e é o que faz o link abrir para quem não tem a chave. Só existe depois de issued; antes disso, 409 nfse.not_issued. Vale nos dois modos — em livemode: false o PDF vem carimbado sem validade jurídica.

Parâmetros de caminho

  • idstringobrigatório

    Id da nota, com ou sem o prefixo nfse_. O UUID cru funciona; um prefixo de outro tipo de recurso é 400, não 404.

    Exemplo: nfse_9f8e7d6c-5b4a-4321-9876-0a1b2c3d4e5f

Respostas

  • 200PDF da nota. `Content-Disposition: attachment`, com nome derivado do id.
  • 400Payload inválido: campo mal formatado, ausente ou fora do domínio aceito. Corrigir e reenviar resolve.
  • 401Chave de API ausente, mal formada, revogada ou inexistente. Nenhuma nota é criada.
  • 403A chave é válida mas não pode fazer isto: falta o escopo exigido, ou a API pública está desligada para a empresa.
  • 404Não existe nota com este id para a empresa da chave.
  • 409A nota existe, mas ainda não foi autorizada (`nfse.not_issued`). Espere o `Retry-After` e consulte o status antes de tentar de novo.
  • 429Limite de requisições por chave excedido. Aguarde o que o header `Retry-After` indicar.
  • 500Falha nossa. `retryable` é `true`: repita com a MESMA `Idempotency-Key` para não duplicar a nota, e cite o `request_id` se persistir.
GET/v1/nfse/{id}/xml

Baixar o XML autorizado

O XML autorizado — é ele o documento fiscal; o PDF é a representação impressa. Mesma forma do PDF: só depois de issued, e vale nos dois modos. Em livemode: false ele traz tpAmb: 2, a marca de que a nota não tem validade fiscal.

Parâmetros de caminho

  • idstringobrigatório

    Id da nota, com ou sem o prefixo nfse_. O UUID cru funciona; um prefixo de outro tipo de recurso é 400, não 404.

    Exemplo: nfse_9f8e7d6c-5b4a-4321-9876-0a1b2c3d4e5f

Respostas

  • 200XML autorizado, sem reprocessamento. `Content-Disposition: attachment`, com nome derivado do id.
  • 400Payload inválido: campo mal formatado, ausente ou fora do domínio aceito. Corrigir e reenviar resolve.
  • 401Chave de API ausente, mal formada, revogada ou inexistente. Nenhuma nota é criada.
  • 403A chave é válida mas não pode fazer isto: falta o escopo exigido, ou a API pública está desligada para a empresa.
  • 404Não existe nota com este id para a empresa da chave.
  • 409A nota existe mas ainda não foi autorizada (`nfse.not_issued`) — não há XML para baixar.
  • 429Limite de requisições por chave excedido. Aguarde o que o header `Retry-After` indicar.
  • 500Falha nossa. `retryable` é `true`: repita com a MESMA `Idempotency-Key` para não duplicar a nota, e cite o `request_id` se persistir.
GET/v1/activities

Listar as atividades em que a empresa pode emitir

Os valores de cnae e service_code que o POST /v1/nfse aceita desta empresa — e só eles: toda linha desta lista é emitível.

Use is_default para saber se o cnae sozinho basta: sendo false, mande o par cnae + service_code para chegar naquela linha. A nota que não manda nenhum dos dois sai na linha que tem is_main e is_default ao mesmo tempo.

Uma atividade do seu cadastro na Receita Federal pode não aparecer aqui. Isso não significa que o seu cadastro esteja errado: a atividade ainda não está liberada para emissão, quase sempre por pendência de cadastro do nosso lado. Fale com o suporte.

Sem filtro por query string e sem paginação — a lista inteira vem numa resposta só.

Respostas

  • 200Envelope de lista. `data` vem vazio quando nenhuma atividade da empresa está liberada para emissão — resposta legítima, não erro.
  • 401Chave de API ausente, mal formada, revogada ou inexistente. Nenhuma nota é criada.
  • 403A chave é válida mas não pode fazer isto: falta o escopo exigido, ou a API pública está desligada para a empresa.
  • 429Limite de requisições por chave excedido. Aguarde o que o header `Retry-After` indicar.
  • 500Falha nossa. `retryable` é `true`: repita com a MESMA `Idempotency-Key` para não duplicar a nota, e cite o `request_id` se persistir.

Exemplo de requisição

Montado com os valores de exemplo do próprio documento — troque a chave pela sua.

curl -X POST 'https://api.extcontabilidade.com.br/v1/nfse' \
  -H 'Authorization: Bearer ext_sk_...' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: a3f1c9e2-7b64-4d18-9f02-5c8e1d7a6b30' \
  -d '{
  "amount_in_cents": 125050,
  "description": "Desenvolvimento de software sob encomenda — competência 07/2026",
  "customer": {
    "type": "foreign",
    "document": "12345678000190",
    "name": "Maria Souza",
    "tax_id": "98-7654321",
    "tax_id_absence_reason": "not_required",
    "address": {
      "street": null,
      "number": null,
      "district": null,
      "city": null,
      "state": null,
      "postal_code": null,
      "country": null
    }
  },
  "foreign_amount": {
    "currency": "EUR",
    "amount_in_cents": 360000
  },
  "reference": "INV-2026-0042",
  "cnae": "6203100",
  "service_code": "010501"
}'

Envelope de erro

  • errorobjetosempre presente

    Toda falha da API sai neste envelope, em qualquer status — não há um segundo formato para 4xx e 5xx. O único acréscimo é o errors da falha de validação, que não substitui nada.

  • error.typestringsempre presente

    Família do erro, estável e pequena o bastante para virar switch no cliente: invalid_request (o pedido está errado — corrija e reenvie), authentication_error (chave ausente, inválida ou revogada), permission_error (a chave é válida mas não pode fazer isto), rate_limit_error (excedeu a janela — respeite o Retry-After) e api_error (falha nossa).

    A lista é FECHADA, e é isso que a torna segura num switch: família nova só entra com versão nova da API. Quem cresce é o code. Ainda assim, mantenha um ramo default tratando o desconhecido como api_error — é a rede para a resposta de um intermediário que não seja nossa.

    Valores: invalid_request · authentication_error · permission_error · rate_limit_error · api_error

  • error.codestringsempre presente

    Código versionado do erro, específico dentro da família. É o valor a usar em lógica de decisão — nunca a message, que pode ser reescrita sem aviso. Nunca é o código cru do provedor.

    O conjunto é ABERTO e cresce: toda vez que uma recusa genérica ganha nome próprio, um código novo aparece, e isso não é breaking change. Trate código desconhecido pelo caminho genérico da família em type — mostrar message, obedecer a retryable, registrar o request_id. Código já publicado não é renomeado nem reaproveitado para outro significado.

  • error.messagestringsempre presente

    Explicação em português, pronta para log ou para exibição ao usuário final. Texto sujeito a melhoria; não faça parsing dele.

  • error.paramstring

    Campo do payload que causou a recusa, quando a causa é de payload. Ausente — e não null — quando o erro não aponta para um campo.

  • error.retryablebooleansempre presente

    Se repetir a MESMA requisição pode dar outro resultado. false quer dizer que insistir não resolve — reenviar sem mudar nada só queima a janela de rate limit. Em true, use Retry-After (header) para saber quando, e mande a mesma Idempotency-Key para não duplicar a nota.

  • error.livemodeboolean

    Se a operação recusada valia em PRODUÇÃO. Mesmo campo do corpo da nota, determinado pela chave usada.

    Ausente quando ainda não havia credencial401 de chave inválida, 403 de permissão e o 503 do kill switch saem sem ele. Ausência NÃO é false: dizer false ali afirmaria que a chamada recusada era de teste, o que induziria a ler como inofensiva uma recusa de produção.

  • error.provider_codestring

    Código cru do provedor (SEFIN), apenas para depuração e para citar em chamado. Nunca use em lógica: ele não é versionado e pode sumir.

  • error.request_idstringsempre presente

    Identificador desta requisição, o mesmo do header Request-Id. Cite este valor ao abrir chamado — é por ele que a EXT acha a requisição no log.

  • error.errorsarray de objeto

    Todas as causas da recusa, presente apenas em invalid_request de validação do corpo — message traz só a primeira. Ausente nos demais erros.

Guia de uso

Emissão de NFS-e para empresas da Ext Contabilidade. REST, JSON, valores em centavos. A emissão é assíncrona: 202 e depois polling, com um envelope de erro único em toda falha.

Início rápido

1. POST /v1/nfse com amount_in_cents e description. Mande também reference — o id da fatura no seu sistema —, que é por onde você reencontra a nota se o id se perder. Emitindo fora da atividade principal da empresa, mande cnae (7 dígitos) — e service_code (6) junto, se aquele CNAE tiver mais de uma linha. Omitidos, a nota sai na principal. 2. Guarde o id do 202. Ele significa ACEITA, não autorizada. 3. Consulte o header Location no intervalo do Retry-After até o status virar um desfecho. 4. Em issued, baixe GET /v1/nfse/{id}/pdf e GET /v1/nfse/{id}/xml.

Os dois tropeços mais comuns da primeira integração: amount_in_cents é em CENTAVOS (R$ 1.250,50 = 125050), e failed e indeterminate pedem ações opostas.

Autenticação

Authorization: Bearer ext_sk_...

Não há auto-contratação: quem libera a empresa é o suporte ([email protected]), e antes disso toda rota responde 403 feature_not_enabled. Liberada, as chaves saem em Minha conta → API, exibidas uma única vez.

Teste e produção

A chave carrega o modo, e ele volta em livemode em toda resposta — inclusive no envelope de erro:

  • ext_sk_test_... — a nota é emitida de verdade, na produção restrita do Emissor Nacional: o ambiente do Sistema Nacional da NFS-e sem validade fiscal. Expira em 7 dias, não é cobrada e não consome sequencial da produção. Pode terminar failed — o status vem do SEFIN, o sistema nacional que autoriza as notas, e a rejeição chega em error.message com o código E0xxx.
  • ext_sk_live_... — documento fiscal já na primeira chamada. Desfazer exige cancelamento formal, que tem prazo.

O código é o mesmo nos dois: integre com a chave de teste e troque a variável de ambiente para ir a produção. Os modos não se enxergam — o que não é do modo da chave responde 404.

O modo de teste é MAIS exigente, de propósito: ele recusa nfse.municipal_registration_missing e nfse.nbs_invalid, pendências de cadastro que a emissão real aceita para o SEFIN rejeitar depois. Em troca não prova tudo — o que fica fora do alcance dele está em POST /v1/nfse.

Ciclo de vida da nota

O 202 diz ACEITA, não autorizada. Consulte o Location até chegar a um DESFECHO — nem todo status é um:

statusDesfecho?O que fazer
queuednãoAceita, ainda não enviada. Continue consultando.
processingnãoEnvio em curso. Transitório — nunca o trate como desfecho.
issuedsimAutorizada. access_key e links existem: baixe PDF e XML.
failedsimRecusada, e nenhum documento fiscal existe. Corrija o que error aponta e emita de novo: é seguro.
indeterminatesimPode existir NFS-e autorizada sem registro aqui. Nunca reemita — acione o suporte.
canceledsimCancelamento confirmado. Nota em cancelamento ainda aparece como issued.

Reemitir uma nota já autorizada gera documento fiscal em duplicidade, que só se desfaz com cancelamento formal — e cancelamento tem prazo.

switch (nfse.status) {
  case 'queued':
  case 'processing':
    return agendarNovaConsulta(nfse);  // ainda não é desfecho

  case 'issued':
    return guardar(nfse);

  case 'failed':
    return corrigirEReemitir(nfse);    // não há documento: é seguro

  case 'indeterminate':
    return abrirChamado(nfse);         // pode existir: NÃO reemitir

  default:
    return abrirChamado(nfse);         // status desconhecido: NÃO reemitir
}

O default é a parte que importa: status novo pode aparecer, e o desfecho seguro para um desconhecido é sempre não reemitir.

Erros

Toda falha sai no mesmo envelope: type (família), code (detalhe), message, param, retryable e request_id — cite o request_id ao abrir chamado. Trate pelo code, nunca pelo status HTTP: o mesmo 422 cobre situações muito diferentes.

code é uma lista aberta: código novo nasce quando uma recusa genérica ganha nome próprio, e acrescentar um não é breaking change — por isso o seu switch precisa de um ramo default. Código já publicado nunca é renomeado nem muda de significado. type é fechado: são as famílias do enum no schema, e uma nova só entra com versão nova da API.

O catálogo completo está na seção Erros, ao fim desta referência.

Limites

60 requisições de escrita e 300 de leitura por 60 segundos, por chave, em baldes SEPARADOS: o POST da emissão gasta de um, as consultas e os downloads do outro — o polling não gasta a cota de emissão. São piso garantido, não teto.

Toda resposta traz RateLimit-Limit, RateLimit-Remaining e RateLimit-Reset; excedido o limite, 429 com Retry-After. Corpo de até 10 MB.

No Retry-After: 3 que a API sugere, cada nota em voo gasta 20 leituras por minuto: até 15 notas simultâneas cabem obedecendo à dica ao pé da letra. Acima disso, espace o polling.

NFS-e

Emissão, consulta, cancelamento e download de notas fiscais de serviço. A emissão é assíncrona: o 202 diz que a nota foi aceita, não que foi autorizada.

Erros

Todo code que a API emite hoje, com o status HTTP e o que fazer em cada um. O envelope e a regra de tratamento estão em Erros, na introdução.

codeHTTPO que fazer
unauthorized401Chave ausente, inválida ou revogada.
feature_not_enabled403A empresa não tem a API liberada. Fale com o suporte.
insufficient_scope403A chave não tem o escopo da rota. Gere outra com o escopo certo.
company.issuer_not_enabled422Emissão automática desligada para a empresa.
company.issuer_not_configured422Emissor Nacional não configurado.
company.issuer_not_approved422A empresa ainda não foi liberada para emitir. Acione o suporte.
company.certificate_missing422Certificado ausente ou vencido no cofre.
nfse.cnae_missing_ctribnac422O CNAE principal não tem código de tributação mapeado. Acione o suporte.
nfse.cnae_missing_nbs422O CNAE principal não tem código NBS mapeado. Acione o suporte.
nfse.municipality_not_covered422Cidade/UF do cadastro não resolvem para código IBGE.
nfse.cnae_not_allowed422O cnae enviado não está no cadastro da empresa na Receita, ou a linha padrão dele está bloqueada. Mande outro, ou omita o campo para sair na atividade principal.
nfse.cnae_without_default422O cnae tem linha no catálogo, mas nenhuma é a padrão. Mande o service_code da linha que você quer, ou acione o suporte.
nfse.service_code_unknown422O service_code não existe no catálogo. Ele tem 6 dígitos (010501) — o clássico é mandar aí o CNAE, que tem 7.
nfse.service_code_not_allowed422O service_code existe, mas não está entre as linhas da empresa. Mandando o par, cnae e service_code precisam ser da MESMA linha.
nfse.nbs_invalid422Só em livemode: false. O código NBS do CNAE não consta da tabela oficial. Em produção a nota é aceita e o SEFIN rejeita depois (E0316).
nfse.municipal_registration_missing422Só em livemode: false. O município exige a Inscrição Municipal do prestador e ela não está cadastrada. Em produção a nota é aceita e o SEFIN rejeita depois (E0120).
nfse.customer_document_invalid400CPF/CNPJ com máscara ou fora de 11/14 dígitos.
nfse.customer_required422O município do prestador exige a identificação do tomador e a nota veio sem customer.document. Vale nos dois modos — nos demais municípios, nota sem tomador continua válida.
nfse.customer_name_required422Tomador pessoa física (CPF) sem customer.name: o nome é impresso na nota e não pode ficar vazio, e não há cadastro público de nome de PF de onde tirá-lo. Em CNPJ é opcional — ali o nome sai do cadastro da Receita.
nfse.export_incomplete422Falta campo obrigatório da exportação. A mensagem lista todos os ausentes.
nfse.country_unsupported422País do tomador fora da Tabela de Países (ISO alpha-2).
nfse.currency_unsupported422Moeda fora da lista aceita. Use a sigla ISO (USD, EUR, GBP).
nfse.foreign_amount_invalid422Valor na moeda estrangeira ausente, zero ou negativo.
nfse.customer_type_conflict422Campo incompatível com o customer.type declarado.
nfse.cancel_window_closed422Nota de mês anterior: pela API, só dentro do mês da emissão. Fale com o suporte — ainda pode ser possível.
nfse.not_cancelable409Estado incompatível: nota não autorizada, substituída, ou anexada manualmente. Só nota autorizada por esta API é cancelável por aqui.
nfse.already_canceled409Esta nota já foi cancelada — nada a fazer.
nfse.cancel_in_progress409Já há um pedido de cancelamento em andamento. Não repita o POST: o cancelamento é irreversível, e o segundo pedido seria rejeitado. Consulte a nota para acompanhar.
nfse.not_issued409Pediu PDF ou XML de nota ainda não autorizada. Repetível quando a nota está queued ou processing — ali o envelope vem com retryable: true e Retry-After. Em failed e indeterminate vem retryable: false: o arquivo não nasce sozinho.
nfse.reference_in_use409Já existe nota desta empresa com esse reference, e a mensagem traz o id dela. É a idempotência do reference funcionando. Para uma segunda nota intencional, use outra referência.
duplicate_suspected409Requisição idêntica há menos de 60s. Reenvie com Idempotency-Key própria, ou com reference diferente, se a segunda nota é intencional.
idempotency_key_in_use409A primeira requisição com essa chave, e com o MESMO corpo, ainda está em voo. Repetível.
idempotency_key_reuse409Mesma Idempotency-Key com corpo diferente, em qualquer momento das 24h da chave. Use uma chave nova.
invalid_request400Validação do corpo: campo obrigatório ausente, tipo errado ou campo desconhecido (o clássico é amount em vez de amount_in_cents). message traz a primeira causa; errors[], todas.
invalid_id400O id não é um UUID (com ou sem o prefixo nfse_).
not_found404Não há nota com esse id para a empresa da chave.
route_not_found404Rota inexistente sob /v1.
malformed_json400Corpo não é JSON válido.
payload_too_large413Corpo acima de 10 MB.
unsupported_media_type415Falta Content-Type: application/json.
rate_limit_exceeded429Estourou o limite. Espere o Retry-After. Repetível.
internal_error500Falha nossa, não mapeada. Cite o request_id ao abrir chamado. Repetível.
service_unavailable503Indisponibilidade temporária nossa. Repetível.

As linhas marcadas como repetíveis são as únicas com retryable: true, e nfse.not_issued é CONDICIONAL, pelo status da nota. Não deduza retryable do code: leia o campo do envelope e obedeça ao Retry-After.