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
- Documento OpenAPI
- https://api.extcontabilidade.com.br/v1/openapi.json
Autenticação
Authorization: Bearer ext_sk_...Chave de API da empresa (ext_sk_...).
Rotas
- GET/v1/nfseBuscar NFS-e por referência
- POST/v1/nfseEmitir NFS-e
- POST/v1/nfse/{id}/cancelCancelar NFS-e
- GET/v1/nfse/{id}Consultar NFS-e
- GET/v1/nfse/{id}/pdfBaixar o PDF da nota
- GET/v1/nfse/{id}/xmlBaixar o XML autorizado
- GET/v1/activitiesListar as atividades em que a empresa pode emitir
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órioO
referenceque 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, é200comdatavazio.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.
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
descriptionnarra: 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ãonfse.cancel_window_closed. cancellation.status: "failed"é permanente em produção — um novoPOST /cancelresponde409 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.messagetraz 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 pelocancellation.status.
Cabeçalhos
Idempotency-KeystringChave 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 oidda original. Os dois se somam: aIdempotency-KeyREPETE a resposta no retry imediato; oreferenceRECUSA a segunda nota para sempre.Exemplo: a3f1c9e2-7b64-4d18-9f02-5c8e1d7a6b30
Corpo da requisição
amount_in_centsintegerobrigatórioValor total do serviço em centavos, inteiro. R$ 1.250,50 se envia como
125050— nunca1250.50, que é recusado com 400 em vez de virar R$ 12,50 em silêncio. Mínimo100(R$ 1,00), máximo9007199254740991. É o valor que vai para a nota, sem retenções.Exemplo: 125050
descriptionstringobrigatórioDescriçã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
customerobjetoTomador 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 com422 nfse.customer_required, antes do202. Nos demais, nota sem tomador passa normalmente.Tomador no exterior é exportação de serviço, e sai por aqui: mande
customer.type: "foreign"comcustomer.addresscompleto, a identificação fiscal (customer.tax_idoucustomer.tax_id_absence_reason) eforeign_amount. Sem otype, o tomador é tratado como brasileiro — omitircustomercontinua significando *nota sem tomador*, e não *tomador no exterior*.customer.typestringOnde está o tomador.
br(padrão) é o comportamento de sempre — identificação por CPF/CNPJ, operação doméstica tributável.foreignfaz a nota sair como exportação de serviço: sem ISS, com o grupo de comércio exterior, e exigindocustomer.addresscompleto,customer.tax_id(oucustomer.tax_id_absence_reason) eforeign_amount.Omitir o campo é o mesmo que
br. Nenhum payload que funciona hoje muda de comportamento.Valores: br · foreign
Exemplo: foreign
customer.documentstringCPF (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.namestringNome 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_idstringNúmero de identificação fiscal do tomador no país dele (
NIF). Exportação exige este campo oucustomer.tax_id_absence_reason— nunca os dois vazios.Exemplo: 98-7654321
customer.tax_id_absence_reasonstringMotivo 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 houvercustomer.tax_id.Valores: exempt · not_required
Exemplo: not_required
customer.addressobjetoEndereç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órioLogradouro do tomador no exterior (
xLgrda DPS).Exemplo: Portland Street
customer.address.numberstringobrigatórioNúmero do endereço (
nroda DPS).Exemplo: 175
customer.address.districtstringobrigatórioBairro ou distrito (
xBairroda DPS). Obrigatório pelo leiaute, mesmo onde o endereço local não usa bairro.Exemplo: Back Bay
customer.address.citystringobrigatórioCidade (
xCidadeda DPS).Exemplo: Boston
customer.address.statestringobrigatórioEstado, província ou região (
xEstProvRegda DPS). Texto livre — não é a sigla de UF brasileira.Exemplo: MA
customer.address.postal_codestringobrigatórioCódigo postal no formato do país de destino (
cEndPostda DPS). Formato livre: não é validado como CEP brasileiro.Exemplo: 02114
customer.address.countrystringobrigatórioPaís do tomador em ISO 3166-1 alpha-2 — dois caracteres, como
US,PTouMZ. Vai paraendExt/cPaise paratribMun/cPaisResult, o país em que o resultado do serviço se verifica.BRnão é aceito: nota com tomador no Brasil não é exportação.Exemplo: US
foreign_amountobjetoValor 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órioSigla 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órioValor 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
referencestringSeu identificador para esta nota — tipicamente o id da fatura no seu sistema. Opcional, e a forma mais confiável de reencontrar a nota: o
idque devolvemos se perde se o seu processo morrer antes de gravá-lo, e aIdempotency-Keyé esquecida em 24h.- Único por empresa, dentro de cada modo. Reenviar o mesmo é
409 nfse.reference_in_use, com oidda 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-Keyjunto, os dois se somam: o retry imediato ganha o REPLAY da chave, e só uma referência de verdade repetida cai no409.Exemplo: INV-2026-0042
- Único por empresa, dentro de cada modo. Reenviar o mesmo é
cnaestringCNAE 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_codejunto. CNAE sem linha padrão é422 nfse.cnae_without_default.
O
cnaee oservice_codeda resposta confirmam a linha em que a nota saiu.Exemplo: 6203100
service_codestringCó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ê manda linha da nota nada a 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.
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órioId da nota, com ou sem o prefixo
nfse_. O UUID cru funciona; um prefixo de outro tipo de recurso é400, não404.Exemplo: nfse_9f8e7d6c-5b4a-4321-9876-0a1b2c3d4e5f
Corpo da requisição
reasonstringMotivo do cancelamento, registrado no evento fiscal. Omitido, assume
other.Valores: issuance_error · service_not_provided · other
Exemplo: issuance_error
descriptionstringJustificativa 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.
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órioId da nota, com ou sem o prefixo
nfse_. O UUID cru funciona; um prefixo de outro tipo de recurso é400, não404.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.
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órioId da nota, com ou sem o prefixo
nfse_. O UUID cru funciona; um prefixo de outro tipo de recurso é400, não404.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.
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órioId da nota, com ou sem o prefixo
nfse_. O UUID cru funciona; um prefixo de outro tipo de recurso é400, não404.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.
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 presenteToda falha da API sai neste envelope, em qualquer status — não há um segundo formato para 4xx e 5xx. O único acréscimo é o
errorsda falha de validação, que não substitui nada.error.typestringsempre presenteFamília do erro, estável e pequena o bastante para virar
switchno 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 oRetry-After) eapi_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 é ocode. Ainda assim, mantenha um ramodefaulttratando o desconhecido comoapi_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 presenteCó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— mostrarmessage, obedecer aretryable, registrar orequest_id. Código já publicado não é renomeado nem reaproveitado para outro significado.error.messagestringsempre presenteExplicaçã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.paramstringCampo 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 presenteSe repetir a MESMA requisição pode dar outro resultado.
falsequer dizer que insistir não resolve — reenviar sem mudar nada só queima a janela de rate limit. Emtrue, useRetry-After(header) para saber quando, e mande a mesmaIdempotency-Keypara não duplicar a nota.error.livemodebooleanSe a operação recusada valia em PRODUÇÃO. Mesmo campo do corpo da nota, determinado pela chave usada.
Ausente quando ainda não havia credencial —
401de chave inválida,403de permissão e o503do kill switch saem sem ele. Ausência NÃO éfalse: dizerfalseali afirmaria que a chamada recusada era de teste, o que induziria a ler como inofensiva uma recusa de produção.error.provider_codestringCó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 presenteIdentificador 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 objetoTodas as causas da recusa, presente apenas em
invalid_requestde validação do corpo —messagetraz 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 terminarfailed— ostatusvem do SEFIN, o sistema nacional que autoriza as notas, e a rejeição chega emerror.messagecom o códigoE0xxx.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:
status | Desfecho? | O que fazer |
|---|---|---|
queued | não | Aceita, ainda não enviada. Continue consultando. |
processing | não | Envio em curso. Transitório — nunca o trate como desfecho. |
issued | sim | Autorizada. access_key e links existem: baixe PDF e XML. |
failed | sim | Recusada, e nenhum documento fiscal existe. Corrija o que error aponta e emita de novo: é seguro. |
indeterminate | sim | Pode existir NFS-e autorizada sem registro aqui. Nunca reemita — acione o suporte. |
canceled | sim | Cancelamento 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.
code | HTTP | O que fazer |
|---|---|---|
unauthorized | 401 | Chave ausente, inválida ou revogada. |
feature_not_enabled | 403 | A empresa não tem a API liberada. Fale com o suporte. |
insufficient_scope | 403 | A chave não tem o escopo da rota. Gere outra com o escopo certo. |
company.issuer_not_enabled | 422 | Emissão automática desligada para a empresa. |
company.issuer_not_configured | 422 | Emissor Nacional não configurado. |
company.issuer_not_approved | 422 | A empresa ainda não foi liberada para emitir. Acione o suporte. |
company.certificate_missing | 422 | Certificado ausente ou vencido no cofre. |
nfse.cnae_missing_ctribnac | 422 | O CNAE principal não tem código de tributação mapeado. Acione o suporte. |
nfse.cnae_missing_nbs | 422 | O CNAE principal não tem código NBS mapeado. Acione o suporte. |
nfse.municipality_not_covered | 422 | Cidade/UF do cadastro não resolvem para código IBGE. |
nfse.cnae_not_allowed | 422 | O 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_default | 422 | O 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_unknown | 422 | O 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_allowed | 422 | O 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_invalid | 422 | Só 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_missing | 422 | Só 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_invalid | 400 | CPF/CNPJ com máscara ou fora de 11/14 dígitos. |
nfse.customer_required | 422 | O 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_required | 422 | Tomador 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_incomplete | 422 | Falta campo obrigatório da exportação. A mensagem lista todos os ausentes. |
nfse.country_unsupported | 422 | País do tomador fora da Tabela de Países (ISO alpha-2). |
nfse.currency_unsupported | 422 | Moeda fora da lista aceita. Use a sigla ISO (USD, EUR, GBP). |
nfse.foreign_amount_invalid | 422 | Valor na moeda estrangeira ausente, zero ou negativo. |
nfse.customer_type_conflict | 422 | Campo incompatível com o customer.type declarado. |
nfse.cancel_window_closed | 422 | Nota 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_cancelable | 409 | Estado incompatível: nota não autorizada, substituída, ou anexada manualmente. Só nota autorizada por esta API é cancelável por aqui. |
nfse.already_canceled | 409 | Esta nota já foi cancelada — nada a fazer. |
nfse.cancel_in_progress | 409 | Já 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_issued | 409 | Pediu 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_use | 409 | Já 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_suspected | 409 | Requisiçã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_use | 409 | A primeira requisição com essa chave, e com o MESMO corpo, ainda está em voo. Repetível. |
idempotency_key_reuse | 409 | Mesma Idempotency-Key com corpo diferente, em qualquer momento das 24h da chave. Use uma chave nova. |
invalid_request | 400 | Validaçã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_id | 400 | O id não é um UUID (com ou sem o prefixo nfse_). |
not_found | 404 | Não há nota com esse id para a empresa da chave. |
route_not_found | 404 | Rota inexistente sob /v1. |
malformed_json | 400 | Corpo não é JSON válido. |
payload_too_large | 413 | Corpo acima de 10 MB. |
unsupported_media_type | 415 | Falta Content-Type: application/json. |
rate_limit_exceeded | 429 | Estourou o limite. Espere o Retry-After. Repetível. |
internal_error | 500 | Falha nossa, não mapeada. Cite o request_id ao abrir chamado. Repetível. |
service_unavailable | 503 | Indisponibilidade 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.