API de integração — emissão de NFS-e

Emita notas fiscais automaticamente a partir do seu sistema (por exemplo, após a confirmação de um pagamento).

Visão geral

Cada empresa tem as suas chaves de API (em Configurações → Integração via API). A nota é emitida com o certificado, o ambiente e os dados da empresa dona da chave; uma chave nunca acessa dados de outra empresa.

Atenção: em produção a nota tem valor fiscal e entra na apuração da empresa. Teste primeiro com a empresa em homologação (ambiente de testes da SEFIN). O cancelamento de notas ainda não está disponível pela API (faça pelo portal nacional).

Autenticação

Envie a chave no cabeçalho Authorization. Guarde-a como segredo (não a coloque em código público nem em aplicativos de celular/navegador).

Authorization: Bearer enfse_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Também é aceito o cabeçalho X-API-Key: enfse_... (só a chave, sem Bearer). Chave ausente, inválida, revogada ou de empresa desativada retorna 401; chamada sem HTTPS, 403 https_obrigatorio; plano sem acesso à API, 403 plano_sem_api.

Endpoints

MétodoCaminhoDescrição
GET/pingSaúde do serviço (sem autenticação).
GET/meConfere a chave e mostra a situação da empresa (ambiente, certificado, pendências de cadastro) e da assinatura (plano, limite de notas, se já pode emitir em produção e o motivo quando não pode).
GET/codigos-municipais?codigo_tributacao_nacional=Códigos de tributação municipal válidos para o serviço, no município da empresa (veja Código de tributação municipal).
POST/nfseCria e emite a NFS-e (201). Com o cabeçalho Prefer: respond-async entra na fila de emissão e responde 202 (recomendado para lotes).
GET/nfse?external_id=...Consulta pelo seu identificador.
GET/nfse?status=&limit=&offset=Lista as mais recentes (limit até 100, padrão 20). status: emitida, erro, processando, na_fila, rascunho ou cancelada.
GET/nfse/{id}Consulta uma nota pelo ID.
GET/nfse/{id}/xmlXML da NFS-e autorizada.
POST/nfse/{id}/enviarEnvia a NFS-e emitida ao tomador por e-mail e/ou WhatsApp, com as integrações da própria empresa (veja Enviar a nota ao cliente).
GET/relatorios/mensal?mes=AAAA-MM&ambiente=producaoTotais do mês com e sem retenção de ISS e a lista das notas emitidas (para conciliação e contabilidade). ambiente: producao (padrão), homologacao ou todos. Com &formato=csv devolve a planilha.

Emitir uma NFS-e — POST /nfse

O mínimo é o seu identificador do pedido, o tomador e o serviço:

curl -X POST https://e-nfse.com.br/api/v1/nfse \
  -H "Authorization: Bearer $CHAVE" \
  -H "Content-Type: application/json" \
  -d '{
    "external_id": "pedido-12345",
    "tomador": { "cpf_cnpj": "217.981.762-20", "nome": "Antonio Brito Lima" },
    "servico": {
      "descricao": "Serviço de suporte técnico em informática",
      "valor": 150.00,
      "codigo_tributacao_nacional": "01.07.01"
    }
  }'

Com tomador pessoa jurídica (endereço obrigatório), código municipal, desconto e retenções:

{
  "external_id": "pedido-12346",
  "tomador": {
    "cpf_cnpj": "11.222.333/0001-81",
    "nome": "Cliente Exemplo Ltda",
    "email": "financeiro@cliente.com.br",
    "telefone": "(92) 99999-2222",
    "endereco": {
      "cep": "69082-551", "logradouro": "Rua Exemplo", "numero": "29",
      "bairro": "Coroado", "codigo_ibge": "1302603"
    }
  },
  "servico": {
    "descricao": "Suporte técnico - setembro/2026",
    "valor": 1000.00,
    "desconto": 100.00,
    "codigo_tributacao_nacional": "01.07.01",
    "codigo_municipal": "100",
    "pedido": "PED-98765"
  },
  "retencao_iss": true,
  "aliquota_iss": 2.5,
  "retencoes_federais": { "irrf": 1.5, "csll": 1, "pis": 0.65, "cofins": 3, "inss": 11 }
}

Campos

CampoObrigatórioDescrição
external_idsimIdentificador do seu pedido: 1 a 100 caracteres (letras, números, . - _ :); acima disso é recusado. Garante que a nota não seja emitida duas vezes se você repetir a chamada. Não diferencia maiúsculas de minúsculas (PED-1 e ped-1 são o mesmo pedido). Pode vir no cabeçalho Idempotency-Key quando o corpo não traz o campo.
tomador.cpf_cnpjsimCPF (11 dígitos) ou CNPJ (14), com ou sem máscara; os dígitos verificadores são conferidos. Envie como texto (um número JSON perde os zeros à esquerda).
tomador.nomesimNome ou razão social, até 150 caracteres.
tomador.emailnãoE-mail válido, até 80 caracteres. Vai na nota e é usado no envio automático ao cliente.
tomador.telefonenãoCom DDD, de 6 a 20 dígitos (a máscara é ignorada).
tomador.inscricao_municipalnãoSó os números são usados (até 15 dígitos). Um texto sem números, como "Isento", conta como não informado.
tomador.enderecoCNPJ: sim
CPF: não
Objeto com cep (8 dígitos), logradouro (até 255), numero (até 60), bairro (até 60), complemento (até 156; fica no cadastro, não sai na nota) e o município: codigo_ibge (7 dígitos, conferido na lista oficial do IBGE) ou cidade + uf (sigla). Sem codigo_ibge, a cidade é procurada pelo nome na lista oficial (acentos, hífens e maiúsculas não importam); sem uf, só quando o nome é único no país. Com CNPJ, cep, logradouro, numero, bairro e o município são obrigatórios em toda emissão (regra E0235 da SEFIN). Com CPF o endereço é opcional; se vier com cep, logradouro, numero e bairro, ele vai na nota e o município também é obrigatório. O cadastro do cliente fica com o nome e a UF oficiais do município.
servico.descricaosimDescrição do serviço, até 2000 caracteres.
servico.valorsimValor em reais, maior que zero, com até 2 casas decimais (mais que isso é recusado): número (150.5) ou texto ("150.50" ou "150,50"; sem separador de milhar).
servico.descontonãoDesconto incondicionado, no mesmo formato e menor que o valor (vazio ou null = sem desconto). Reduz a base do ISS e das retenções.
servico.codigo_tributacao_nacionalsimCódigo de tributação nacional de 6 dígitos ("010301" ou "01.03.01").
servico.codigo_municipalnão (recomendado)Código de tributação municipal de 3 dígitos ("100"; também aceita "01.03.01.100"). Cada prefeitura define o seu: se omitido, o sistema descobre e usa o código do município quando houver um só; se informado, é conferido (veja abaixo).
servico.nbsnãoCódigo NBS de 9 dígitos (com ou sem pontos).
servico.informacoes_complementares (até 2000), pedido (até 60), documento_referencia (até 255)nãoTextos que saem na nota.
retencao_issnãotrue se o tomador retém o ISS; ausente = false. Aceita true/false (também 1/0 e "true"/"false"; vazio ou null = false). Informe em cada nota: a API não usa a marcação "retém ISS" do cadastro do cliente no painel.
aliquota_issnãoAlíquota do ISS em %, de 0 a 5 (2.5, "2.5" ou "2,5"). Para ME/EPP com retenção, vai na nota; omitida ou 0 = 5%, como no emissor nacional. Sem retenção (MEI, ME e EPP do Simples Nacional) o ISS é recolhido no DAS, a nota não leva alíquota e o campo é ignorado. MEI nunca leva alíquota.
retencoes_federaisnãoTributos federais retidos pelo tomador, em % sobre o valor do serviço menos o desconto: {"irrf": 1.5, "csll": 1, "pis": 0.65, "cofins": 3, "inss": 11}. Só essas chaves; de 0 a 30 cada, com até 2 casas decimais; informe só os retidos (vazio ou null = não retido). Vale para qualquer regime. A soma de todas as retenções (federais + ISS retido) não pode passar do valor do serviço menos o desconto (422 dados_invalidos).
assincrononãotrue = modo assíncrono (o mesmo que o cabeçalho Prefer: respond-async).
retencao_iss, aliquota_iss, retencoes_federais e assincrono ficam na raiz do JSON, fora de servico (dentro de servico são recusados com 400). Valores acima do limite ou de tipo errado são recusados com 400, nunca cortados. Campos desconhecidos são ignorados.

Retenções

Os valores retidos são calculados sobre o valor do serviço menos o desconto e vão no leiaute da DPS (tpRetISSQN/pAliq do ISS; vRetIRRF, vRetCSLL, vRetCP e o grupo PIS/COFINS com o tipo de retenção). A resposta traz os valores em retencoes (R$; 0.0 quando não há). No segundo exemplo acima (valor 1.000,00, desconto 100,00, base 900,00), para uma ME/EPP:

"retencoes": { "iss": 22.5, "irrf": 13.5, "csll": 9.0, "pis": 5.85, "cofins": 27.0, "inss": 99.0 }

Para MEI com retencao_iss: true, a nota sai como retida pelo tomador, sem alíquota, e retencoes.iss é 0.0.

O que vem do cadastro (não envie)

Cadastro do tomador

Cada emissão cria ou atualiza o tomador no cadastro de clientes da empresa (pelo CPF/CNPJ) com os campos informados. Campos omitidos mantêm o que já está no cadastro e também vão na nota (por exemplo, o e-mail ou o telefone cadastrados no painel, ou o endereço de um tomador com CPF). Tomador com CNPJ precisa do endereço completo em toda emissão. Um município novo informado substitui o anterior.

Código de tributação municipal

Cada prefeitura define um código de 3 dígitos para cada serviço (em Manaus, 01.03.01 → 100 e 01.03.02 → 200; em São Paulo, 01.07.01 → 001 ou 002) e a SEFIN recusa (E0314) um código que o município não administra. O sistema consulta a base nacional de parâmetros municipais e guarda o resultado; por isso:

Para saber os códigos antes de emitir: GET /codigos-municipais?codigo_tributacao_nacional=010701. A primeira consulta de um serviço pode levar alguns segundos; as seguintes vêm do catálogo.

{
  "municipio": { "ibge": "1302603", "nome": "Manaus", "uf": "AM" },
  "codigo_tributacao_nacional": "010701",
  "situacao": "encontrado",
  "validacao_ativa": true,
  "codigos_municipais": [ { "codigo": "100", "aliquota": 5.0 } ],
  "sugerido": "100",
  "motivo": null
}

situacao: encontrado, nao_encontrado (nenhum código nas combinações mais comuns; o serviço pode não usar código municipal), indisponivel (veja motivo), mei ou sem_municipio.

Resposta de sucesso — 201 Created

{
  "id": 42,
  "external_id": "pedido-12345",
  "status": "emitida",
  "valor": 150.0,
  "retencoes": { "iss": 0.0, "irrf": 0.0, "csll": 0.0, "pis": 0.0, "cofins": 0.0, "inss": 0.0 },
  "numero_dps": "31",
  "serie_dps": "1",
  "origem": "e-nfse",
  "criada_em": "2026-09-30T14:20:11-03:00",
  "nfse": {
    "numero": "74",
    "chave": "13026032204038227000187000000000007426093101234567",
    "data_emissao": "2026-09-30T14:20:12-03:00",
    "ambiente": "producao"
  },
  "links": {
    "danfse": "https://e-nfse.com.br/d/AbCd...",
    "xml": "https://e-nfse.com.br/x/AbCd...",
    "consulta_publica": "https://www.nfse.gov.br/ConsultaPublica?tpc=1&chave=..."
  }
}

valor é o valor bruto do serviço. Quando houver, vem também "avisos": [...] (por exemplo, código municipal não escolhido). Os links danfse e xml são públicos (não pedem login) e difíceis de adivinhar: podem ser enviados ao cliente por e-mail. Para conferir os demais dados emitidos, use GET /nfse/{id}/xml.

Repetindo a chamada (idempotência)

Se você repetir o POST com o mesmo external_id de uma nota já emitida, recebe 200 com a mesma nota e "ja_emitida": true — sem emitir outra e sem alterar nada, mesmo que o corpo seja diferente. É seguro repetir em caso de timeout de rede, mesmo que depois o certificado tenha vencido. Os dados do corpo são conferidos antes dessa verificação: repita com o mesmo corpo da primeira chamada.

Rejeição pela SEFIN

{
  "id": 43,
  "external_id": "pedido-12347",
  "status": "erro",
  "valor": 150.0,
  "retencoes": { "iss": 0.0, "irrf": 0.0, "csll": 0.0, "pis": 0.0, "cofins": 0.0, "inss": 0.0 },
  "numero_dps": "32",
  "serie_dps": "1",
  "origem": "e-nfse",
  "criada_em": "2026-09-30T14:21:40-03:00",
  "nfse": null,
  "links": null,
  "erros": [
    { "codigo": "E0314", "descricao": "O código de tributação municipal informado não existe ...", "complemento": "" }
  ],
  "mensagem": "E0314: O código de tributação municipal informado não existe ..."
}

O corpo é a própria nota e o HTTP indica o tipo de falha: 422 (a SEFIN rejeitou; status erro com erros), 502 (falha de comunicação ou erro interno da SEFIN; status erro) ou 409 (a mesma nota já estava sendo enviada por outra chamada; consulte em instantes). A nota fica salva. Corrija os dados e envie o mesmo external_id novamente: a mesma nota (mesmo número de DPS) é atualizada e reenviada, inclusive se a correção for o tomador. Nota cancelada não pode ser reenviada (422 dados_invalidos). Se o envio foi interrompido (a nota ficou em processando por mais de 10 minutos), reenviar com o mesmo external_id retoma a mesma nota.

Emissão em massa: modo assíncrono (fila) — 202 Accepted

Cada emissão direta (201) mantém a sua conexão aberta por alguns segundos enquanto a SEFIN responde. Para lotes grandes (centenas de notas de uma vez), peça o modo assíncrono: a nota é validada e gravada, a resposta é imediata (202) e a emissão acontece em segundo plano, uma nota por empresa por vez, em ordem de chegada. Isso protege o seu lote e o serviço: nenhuma empresa consegue travar as outras.

curl -X POST https://e-nfse.com.br/api/v1/nfse \
  -H "Authorization: Bearer $CHAVE" \
  -H "Content-Type: application/json" \
  -H "Prefer: respond-async" \
  -d '{ "external_id": "pedido-12345", ... }'      # ou, no corpo: "assincrono": true
HTTP/1.1 202 Accepted
Location: https://e-nfse.com.br/api/v1/nfse/42
Retry-After: 5
Preference-Applied: respond-async

{
  "id": 42,
  "external_id": "pedido-12345",
  "status": "na_fila",
  "valor": 150.0,
  "retencoes": { ... },
  "numero_dps": "31",
  "serie_dps": "1",
  "origem": "e-nfse",
  "criada_em": "2026-09-30T14:20:11-03:00",
  "nfse": null,
  "links": null,
  "posicao_na_fila": 7,
  "mensagem": "NFS-e recebida e na fila de emissão. Consulte o andamento em GET /api/v1/nfse/42."
}

Acompanhe com GET /nfse/{id} (ou GET /nfse?external_id=...) a cada poucos segundos: o status vai de na_fila para processando e termina em emitida (com nfse e links) ou erro (com erros; reenvie o mesmo external_id depois de corrigir). GET /nfse?status=na_fila lista o que ainda aguarda. Repetir o POST com o mesmo external_id de uma nota que ainda está na fila devolve 202 com "ja_na_fila": true, sem duplicar (com ou sem o modo assíncrono).

Enviar a nota ao cliente — POST /nfse/{id}/enviar

Envia a NFS-e já emitida ao tomador por e-mail e/ou WhatsApp, usando o e-mail e a Z-API da própria empresa, configurados no painel em Configurações → Envio da NFS-e ao cliente (e-mail pelo Google com senha de app, ou outro SMTP). O cliente recebe os links do DANFSe e do XML (e o XML anexado, no e-mail).

O envio também pode ser automático: com a opção ligada no painel, toda nota de produção emitida (pelo painel ou pela API, inclusive no modo assíncrono) é enviada ao tomador em até 1 minuto, usando o e-mail e o telefone do cadastro do tomador (atualizados com os dados informados na emissão; para o WhatsApp, um celular já cadastrado tem preferência sobre um telefone fixo). Notas de homologação não são enviadas automaticamente.

curl -X POST https://e-nfse.com.br/api/v1/nfse/42/enviar \
  -H "Authorization: Bearer $CHAVE" \
  -H "Content-Type: application/json" \
  -d '{ "canais": ["email", "whatsapp"], "email": "outro@cliente.com.br", "telefone": "(92) 99999-2222" }'

Todos os campos são opcionais e o corpo pode ser omitido: sem canais tenta e-mail e WhatsApp; email e telefone valem só para este envio (sem eles, usa os do cadastro do tomador). Se enviar corpo, ele precisa ser application/json (senão 415) com até 4 KB.

{
  "enviados": ["email"],
  "resultados": {
    "email":    { "ok": true,  "destino": "outro@cliente.com.br", "mensagem": "E-mail enviado para outro@cliente.com.br." },
    "whatsapp": { "ok": false, "destino": "5592999992222", "mensagem": "WhatsApp não configurado: informe a Z-API da empresa em Configurações." }
  }
}

Resposta 200 se pelo menos um canal enviou; 422 com "erro": "envio_falhou" e resultados (o motivo de cada canal) se nenhum enviou; 409 (nao_emitida) se a nota ainda não foi autorizada; 404 se a nota não existe. Há um limite de envios por dia por empresa e um intervalo de 1 minuto para repetir o mesmo destino.

Códigos de resposta

HTTPerroQuando
200 / 201—Sucesso (201 = emitida agora; 200 = consulta ou nota já emitida).
202—Nota recebida e na fila de emissão (modo assíncrono). Consulte o andamento em GET /nfse/{id} (cabeçalho Location).
400validacao, json_invalido, corpo_vazioDados inválidos. Em detalhes vem a lista [{campo, mensagem}].
401nao_autenticadoChave ausente, inválida ou revogada.
402assinatura_em_teste, assinatura_pendente, assinatura_bloqueada, assinatura_cancelada, limite_notasSó no POST /nfse em produção: a assinatura não permite emitir agora (período de teste, aguardando pagamento, bloqueada por falta de pagamento, cancelada) ou o limite mensal de notas do plano foi atingido. A nota não é enviada à SEFIN; no modo síncrono ela fica salva como rascunho com o external_id (repita a chamada depois de regularizar). A empresa regulariza em "Minha assinatura" no painel; consulte GET /me (assinatura.pode_emitir_producao). Em homologação nunca ocorre.
403https_obrigatorio, plano_sem_apiChamada sem HTTPS / o plano da empresa não inclui o acesso à API.
422 (relatório)mes_invalido, ambiente_invalidoParâmetros de /relatorios/mensal inválidos.
404nao_encontrada, rota_inexistenteNota inexistente (ou de outra empresa) / caminho errado.
405metodo_nao_permitidoMétodo HTTP não aceito no caminho (cabeçalho Allow diz quais são).
409em_processamento, nao_emitidaJá existe envio em andamento para o external_id (consulte em instantes) / XML ou envio ao cliente pedido para nota não autorizada.
413 / 415corpo_grande, tipo_invalidoCorpo acima de 256 KB / sem application/json.
422 (envio)envio_falhou, envio_invalidoEm POST /nfse/{id}/enviar: nenhum canal conseguiu enviar (veja resultados) / o pedido é inválido.
422empresa_incompleta, sem_certificado, certificado_vencido, codigo_municipal_invalido, dados_invalidos ou status erroA empresa não está pronta para emitir (empresa_incompleta: a mensagem lista o que falta no cadastro; certificado ausente ou vencido), o código municipal não existe no município (veja detalhes), as retenções somadas passam do valor do serviço menos o desconto ou a nota foi cancelada (dados_invalidos), ou a SEFIN rejeitou a nota (corpo = nota com status erro e erros).
429limite_excedido, muitas_emissoes_simultaneas, servidor_ocupado, fila_cheiaMais de 120 requisições/minuto; emissões simultâneas demais (empresa ou servidor) no modo síncrono; ou fila de emissão da empresa cheia. Aguarde o tempo de Retry-After (e, para lotes, use o modo assíncrono).
502—Falha de comunicação ou erro interno da SEFIN (corpo = nota com status erro); a nota ficou salva, reenvie com o mesmo external_id.
500erro_internoErro inesperado no servidor. A nota pode ter ficado salva: repita com o mesmo external_id.

Corpo de erro padrão: {"erro": "codigo", "mensagem": "texto", "detalhes": [...]} (detalhes só aparece quando há itens: [{campo, mensagem}] na validação, com codigos_validos no código municipal). Falhas de envio à SEFIN devolvem a própria nota, com status erro e erros.

Exemplos

PHP

$ch = curl_init('https://e-nfse.com.br/api/v1/nfse');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NFSE_CHAVE'), 'Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'external_id' => 'pedido-' . $pedidoId,
        'tomador' => ['cpf_cnpj' => $cpf, 'nome' => $nome],
        'servico' => ['descricao' => 'Mensalidade', 'valor' => 99.90, 'codigo_tributacao_nacional' => '010701'],
    ]),
    CURLOPT_TIMEOUT => 60,
]);
$resposta = json_decode(curl_exec($ch), true);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http === 201 || $http === 200) { /* $resposta['links']['danfse'] */ }

Node.js

const r = await fetch('https://e-nfse.com.br/api/v1/nfse', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${process.env.NFSE_CHAVE}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    external_id: `pedido-${pedido.id}`,
    tomador: { cpf_cnpj: pedido.cpf, nome: pedido.nome },
    servico: { descricao: 'Mensalidade', valor: 99.9, codigo_tributacao_nacional: '010701' },
  }),
});
const nota = await r.json(); // r.status === 201 => nota.links.danfse

Boas práticas