# API de NFS-e — Ext Contabilidade — referência completa Versão 1.0.0 · OpenAPI 3.1.0 Documento OpenAPI (a fonte deste arquivo): https://api.extcontabilidade.com.br/v1/openapi.json 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 (`suporte@extcontabilidade.com.br`), e antes disso toda rota responde `403 feature_not_enabled`. Liberada, as chaves saem em [Minha conta → API](https://extcontabilidade.com.br/app/user/my-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: | `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. ```js 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. 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. 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`. ## Operações ### `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: - `reference` (query, obrigatório) Respostas: 200, 400, 401, 403, 429, 500. ### `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`. Parâmetros: - `Idempotency-Key` (header, opcional) Respostas: 202, 400, 401, 403, 409, 422, 429, 500. ### `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: - `id` (path, obrigatório) Respostas: 202, 400, 401, 403, 404, 409, 422, 429, 500. ### `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: - `id` (path, obrigatório) Respostas: 200, 400, 401, 403, 404, 429, 500. ### `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: - `id` (path, obrigatório) Respostas: 200, 400, 401, 403, 404, 409, 429, 500. ### `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: - `id` (path, obrigatório) Respostas: 200, 400, 401, 403, 404, 409, 429, 500. ### `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: 200, 401, 403, 429, 500. ## Schemas ### CreateNfseCustomerAddressDto - `street` (string, obrigatório) Logradouro do tomador no exterior (`xLgr` da DPS). - `number` (string, obrigatório) Número do endereço (`nro` da DPS). - `district` (string, obrigatório) Bairro ou distrito (`xBairro` da DPS). Obrigatório pelo leiaute, mesmo onde o endereço local não usa bairro. - `city` (string, obrigatório) Cidade (`xCidade` da DPS). - `state` (string, obrigatório) Estado, província ou região (`xEstProvReg` da DPS). Texto livre — não é a sigla de UF brasileira. - `postal_code` (string, obrigatório) Código postal no formato do país de destino (`cEndPost` da DPS). Formato livre: não é validado como CEP brasileiro. - `country` (string, obrigató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. ### CreateNfseCustomerDto - `type` (string, opcional) 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`. - `document` (string, opcional) CPF (11 dígitos) ou CNPJ (14), **apenas dígitos** — máscara com pontos, barra ou traço é RECUSADA com 400, não limpa. - `name` (string, opcional) 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. - `tax_id` (string, opcional) 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. - `tax_id_absence_reason` (string, opcional) 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`. - `address` (CreateNfseCustomerAddressDto, opcional) 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. ### CreateNfseForeignAmountDto - `currency` (string, obrigató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. - `amount_in_cents` (integer, obrigató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. ### CreateNfseDto - `amount_in_cents` (integer, obrigató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. - `description` (string, obrigató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. - `customer` (CreateNfseCustomerDto, opcional) 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*. - `foreign_amount` (CreateNfseForeignAmountDto, opcional) 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. - `reference` (string, opcional) 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`. - `cnae` (string, opcional) 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. - `service_code` (string, opcional) 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ê manda | linha da nota | | --- | --- | | nada | a padrão da atividade **principal** da empresa | | `cnae` | a padrão **daquele CNAE** | | `cnae` + `service_code` | a linha **exata** | | `service_code` | a 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`. ### NfseResponseCustomerAddressDto - `street` (string, opcional) Logradouro. - `number` (string, opcional) Número. - `district` (string, opcional) Bairro ou distrito. - `city` (string, opcional) Cidade. - `state` (string, opcional) Estado, província ou região. - `postal_code` (string, opcional) Código postal. - `country` (string, opcional) País em ISO 3166-1 alpha-2. ### NfseResponseCustomerDto - `document` (string, opcional) CPF ou CNPJ do tomador, apenas dígitos. Ausente em nota sem tomador. - `name` (string, opcional) Nome do tomador como gravado na nota. Em CNPJ vem do cadastro da Receita, não do que foi enviado. - `type` (string, opcional) Onde está o tomador: `br` (operação doméstica) ou `foreign` (exportação de serviço). Valores: `br`, `foreign`. - `tax_id` (string, opcional) NIF do tomador no exterior, quando informado. - `tax_id_absence_reason` (string, opcional) Motivo de não haver NIF: `exempt` (dispensado) ou `not_required` (não exigência). Valores: `exempt`, `not_required`. - `address` (NfseResponseCustomerAddressDto, opcional) Endereço do tomador no exterior. Ausente em nota doméstica. ### NfseResponseForeignAmountDto - `currency` (string, obrigatório) Sigla ISO da moeda do faturamento. - `amount_in_cents` (integer, obrigatório) Valor na moeda estrangeira, em centavos. ### NfseResponseErrorDto - `code` (string, obrigatório) Código estável do motivo da falha, num vocabulário PRÓPRIO — disjunto dos códigos de erro HTTP. Em `error` da nota: `issue_failed` e `nfse.not_processed` vêm com `status: failed`; `nfse.issue_interrupted` vem com `status: indeterminate` e é o único que **não** admite reenvio. Em `cancellation.error`, `nfse.cancel_failed` é o único valor. - `message` (string, obrigatório) Mensagem em português, já pronta para ser mostrada ao usuário final. Nunca contém a resposta crua do SEFIN. ### NfseResponseCancellationDto - `status` (string, obrigatório) Estado do PEDIDO de cancelamento, diferente do `status` da nota. `pending` = enviado, sem desfecho; `succeeded` = a nota está cancelada (o `status` também é `canceled`); `failed` = não recebemos a confirmação — **não emita outra nota**, acione o suporte. **Com `ext_sk_live_`, `failed` é permanente**: um novo `POST /cancel` sobre a mesma nota responde `409 nfse.cancel_in_progress`, e não há caminho pela API para sair desse estado. **Com `ext_sk_test_` o mesmo `failed` libera nova tentativa** — o modo de teste existe para ensaiar a recusa do SEFIN, não a trava permanente da produção. Valores: `pending`, `succeeded`, `failed`. - `requested_at` (string, obrigatório) Quando o cancelamento foi pedido. ISO 8601 em UTC. - `reason` (string, opcional) O `reason` enviado no pedido. Um pedido sem `reason` é registrado como `other`. Valores: `issuance_error`, `service_not_provided`, `other`. - `error` (NfseResponseErrorDto, opcional) Presente só em `failed`. Nunca manda reemitir: o desfecho real é incerto, e reemitir sobre nota que já pode ter sido cancelada geraria duplicidade. ### NfseResponseLinksDto - `pdf` (string, obrigatório) URL absoluta do PDF da nota, com um token de download (`?t=`) que vale **15 minutos** e serve só esta nota. Dentro desse prazo o link abre sozinho — dá para entregá-lo a quem não tem a chave. Vencido, consulte a nota de novo e use o link novo; a resposta traz um a cada leitura. A rota também aceita a chave de API (escopo `nfse:read`), como sempre aceitou. - `xml` (string, obrigatório) URL absoluta do XML autorizado. Mesmo token temporário e mesma chave do PDF. ### NfseResponseDto - `object` (string, obrigatório) Discriminador do tipo do recurso. Sempre `nfse` neste corpo — permite roteamento genérico no cliente. - `id` (string, obrigatório) Identificador da nota, sempre prefixado (`nfse_`) — é o que se usa nas rotas de consulta e download, que também aceitam o UUID cru. - `livemode` (boolean, obrigatório) Se a operação valeu em PRODUÇÃO. `true` é nota fiscal de verdade. `false` é nota de teste: nasce de uma chave `ext_sk_test_`, sai no ambiente sem validade fiscal, não é cobrada e expira em 7 dias. Sai em TODA resposta de `/v1/nfse`, erro incluído, e é determinado pela chave — nunca pelo corpo enviado. - `status` (string, obrigatório) Situação da nota: - `queued` — aceita, ainda não enviada. Continue consultando. - `processing` — envio em curso. Estado **transitório**: não pare o polling nele. - `issued` — autorizada. `access_key` e `links` existem; é o único estado em que PDF e XML baixam. - `failed` — recusada, e **nenhum documento existe**. Corrigir o que `error` aponta e reemitir é seguro. - `indeterminate` — a emissão começou sem desfecho confirmado: **pode existir NFS-e autorizada** sem registro aqui. **Nunca reemita** — daria nota em duplicidade. Acione o suporte. - `canceled` — cancelada. Nota em cancelamento ainda aparece `issued`. Desfechos: `issued`, `failed`, `indeterminate` e `canceled`. Valor desconhecido, trate como `indeterminate`. Valores: `queued`, `processing`, `issued`, `failed`, `indeterminate`, `canceled`. - `amount_in_cents` (integer, obrigatório) Valor total **em centavos**, inteiro, como gravado na nota: `125050` são R$ 1.250,50. Mesma unidade da ida — o contrato nunca troca de unidade. - `description` (string, opcional) Descrição **exatamente como gravada na nota**. Notas antigas criadas pela interface web podem trazer marcadores não expandidos (`{{mes_extenso}}`): a leitura não os expande, senão publicaria texto diferente do impresso na nota. - `competence` (string, opcional) Competência — o mês fiscal da nota —, data pura `YYYY-MM-DD`. Você não a envia: é a data em que a chamada foi recebida, no fuso do Brasil. **Ausente** apenas em nota criada pela interface da EXT sem competência gravada. - `customer` (NfseResponseCustomerDto, opcional) Tomador da nota. Ausente em nota sem tomador. - `foreign_amount` (NfseResponseForeignAmountDto, opcional) Valor da nota na moeda do faturamento, na exportação. Ausente em nota doméstica. O `amount_in_cents` da raiz continua sendo o valor em reais que foi para a nota. - `reference` (string, opcional) O `reference` que você enviou na emissão, intacto. Ausente quando a nota não tem um — inclusive em toda nota criada pela interface da EXT. - `service_code` (string, opcional) Código de tributação nacional, 6 dígitos: a linha em que a nota saiu — sempre a que foi **usada**, tenha você mandado o campo ou não. Ausente em nota antiga e em nota criada pela interface da EXT sem escolha explícita de atividade. - `cnae` (string, opcional) CNAE (7 dígitos) da linha em que a nota saiu — o par do `service_code`, com a mesma convenção. Omitindo `cnae` no `POST`, volta o da atividade principal da empresa. - `access_key` (string, opcional) Chave de acesso da NFS-e, 50 dígitos. Só existe depois de autorizada, e **sobrevive ao cancelamento**: o cancelamento é evento à parte no leiaute nacional e não apaga a autorização, então a chave continua sendo a de consulta da nota no portal. Ausente em quem nunca chegou a ser autorizada. - `failed_attempts` (number, obrigatório) Quantas tentativas de emissão FALHARAM. `0` nunca significa "nada foi enviado": em `indeterminate` a emissão começou e morreu sem registrar a falha. **Quem decide se pode reemitir é o `status`**, nunca este número. - `created_at` (string, opcional) Quando a nota foi aceita pela API. ISO 8601 em UTC. - `issued_at` (string, opcional) Quando a nota foi autorizada na prefeitura. ISO 8601 em UTC, presente em `issued` e em `canceled` — cancelar não muda a data em que a nota saiu. Pode faltar em nota `canceled` anterior a agosto/2026, quando o carimbo passou a ser gravado. - `cancelable_until` (string, opcional) Até quando esta nota pode ser cancelada por esta API: o fim do mês da emissão, no fuso de Brasília. **A PRESENÇA do campo é a resposta**: ele existe quando, e só quando, um `POST /v1/nfse/{id}/cancel` seria aceito agora. Ausente, a rota recusa — `422` fora da janela, `409` se já houver pedido. Leia o instante daqui em vez de recalcular: a regra é medida em Brasília, e comparar o mês em UTC erra as últimas três horas de todo dia 31 — justamente quando a janela fecha. Fora dela, o suporte ainda pode cancelar. - `cancellation` (NfseResponseCancellationDto, opcional) Estado do pedido de cancelamento, ausente enquanto nenhum foi feito. Fica FORA do `status` porque descreve o PEDIDO: uma nota com cancelamento pendente continua `issued`, porque continua valendo. - `failed_at` (string, opcional) Quando a falha foi constatada. ISO 8601 em UTC, **ausente** em nota que não está `failed` nem `indeterminate`. Em falha por espera esgotada é o vencimento da janela, não um carimbo de erro. - `error` (NfseResponseErrorDto, opcional) Detalhe da falha, **ausente** quando a nota não está `failed` nem `indeterminate`. **Programe pelo `status`, não por este campo** — ele existe para você saber o PORQUÊ e escrever a mensagem ao seu usuário. | `error.code` | `status` | O que aconteceu | O que fazer | | --- | --- | --- | --- | | `issue_failed` | `failed` | Falha ANTES da autorização — o caso dominante. Nada foi emitido. | Confira o payload e emita de novo. | | `nfse.not_processed` | `failed` | Nenhuma tentativa chegou a ser registrada. | Acione o suporte: a causa é de habilitação na EXT, não do seu payload. | | `nfse.issue_interrupted` | `indeterminate` | A emissão começou e o resultado não pôde ser confirmado. | **Nunca reenvie.** A nota pode existir sem que o registro aqui se completasse, e reemitir gera documento fiscal em duplicidade. | **A nota nunca fica em polling infinito.** Pendente sem nenhuma tentativa por 15 minutos vira `failed` (`nfse.not_processed`); em processamento sem sinal por 60 minutos vira `indeterminate` (`nfse.issue_interrupted`) — essa é a que você não pode reemitir. - `links` (NfseResponseLinksDto, opcional) URLs de download do PDF e do XML, presentes em `issued` e em `canceled` — em nota cancelada o PDF vem com a tarja CANCELADA, que é o documento a arquivar. Nos demais estados não há arquivo, e o download é `409 nfse.not_issued`. **No modo de teste (`ext_sk_test_`) o campo some em `canceled`**, e é a única diferença de corpo entre os modos: lá o DANFSe é re-renderizado na hora do download e sairia sem a tarja. ### PublicApiErrorItemDto - `code` (string, obrigatório) Sempre `invalid_request`, o mesmo `code` do envelope: o item tem a forma do erro, mas não diferencia as causas entre si. - `message` (string, obrigatório) A causa, como o validador a descreve. ### PublicApiErrorDetailDto - `type` (string, obrigatório) 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`. - `code` (string, obrigatório) 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. - `message` (string, obrigatório) 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. - `param` (string, opcional) 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. - `retryable` (boolean, obrigatório) 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. - `livemode` (boolean, opcional) 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 credencial** — `401` 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. - `provider_code` (string, opcional) 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. - `request_id` (string, obrigatório) 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. - `errors` (array, opcional) Todas as causas da recusa, presente apenas em `invalid_request` de validação do corpo — `message` traz só a primeira. Ausente nos demais erros. ### PublicApiErrorDto - `error` (PublicApiErrorDetailDto, obrigatório) 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. ### CancelNfseDto - `reason` (string, opcional) Motivo do cancelamento, registrado no evento fiscal. Omitido, assume `other`. Valores: `issuance_error`, `service_not_provided`, `other`. - `description` (string, opcional) 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. ### NfseListResponseDto - `object` (string, obrigatório) Discriminador do tipo do recurso. Sempre `list` neste corpo — os itens de `data` trazem o próprio `object: "nfse"`. - `has_more` (boolean, obrigatório) Se há mais resultados além dos devolvidos. **Sempre `false` no v1**, já que a única busca é por `reference`, único por empresa. O campo existe desde já para o seu código não precisar mudar quando a paginação chegar. - `data` (array, obrigatório) As notas encontradas, no mesmo formato de `GET /v1/nfse/{id}`. Vazio quando nenhuma nota da empresa tem o `reference` consultado. ### ActivityResponseDto - `cnae` (string, obrigatório) CNAE da linha, 7 dígitos, **apenas números** — exatamente o formato que o `cnae` do `POST /v1/nfse` aceita. Mande este valor como veio, sem a máscara do CONCLA. - `service_code` (string, obrigatório) Código de tributação nacional da linha, 6 dígitos — o `service_code` do `POST /v1/nfse`. Só é necessário quando `is_default` é `false`. - `description` (string, obrigatório) A linha por extenso: item da LC 116, descrição e o CNAE com máscara. Serve para você reconhecer a atividade — **não é campo de envio**, e o CNAE com máscara que aparece aqui é recusado com `400` no campo `cnae`. - `is_main` (boolean, obrigatório) Se o CNAE é a atividade **principal** da empresa na Receita Federal. A nota que não manda `cnae` sai na linha que tem `is_main` e `is_default` ao mesmo tempo. - `is_default` (boolean, obrigatório) Se é a linha **padrão** do seu CNAE. Sendo `true`, mandar só o `cnae` chega nela. Sendo `false`, é preciso mandar `cnae` **e** `service_code` juntos — sem o par ela é inalcançável, porque a linha padrão é global por CNAE e não por empresa. ### ActivityListResponseDto - `object` (string, obrigatório) Discriminador do tipo do recurso. Sempre `list` neste corpo. - `has_more` (boolean, obrigatório) Se há mais resultados além dos devolvidos. **Sempre `false` no v1** — a lista é o cadastro da empresa, que cabe inteiro numa resposta. O campo existe desde já para o seu código não precisar mudar se a paginação chegar. - `data` (array, obrigatório) As atividades em que a empresa pode emitir. Vazio quando nenhuma está liberada — o que é resposta legítima, não erro.