Como integrar a emissão de NFS-e da v3 ponta a ponta: pré-requisitos, autenticação, idempotência, tratamento do callback, catálogo de erros e armadilhas de produção.
Este guia cobre o que envolve a emissão de NFS-e além do contrato do endpoint. O contrato em si, com campos, tipos, enums, exemplos de requisição, respostas e payload do callback, está na referência do endpoint.
Apoio técnico, não parecer fiscalEsta página descreve o comportamento do endpoint. Exigências da SEFIN Nacional e das prefeituras (códigos de tributação, alíquotas, regimes) devem ser definidas com a contabilidade do prestador.
Como funciona
sequenceDiagram
autonumber
participant I as Integrador
participant S as Safe2Pay
participant P as SEFIN Nacional / prefeitura
I->>S: POST /v3/nfse (X-API-KEY + JSON)
S->>S: Autentica (403), converte o JSON (400), verifica a conta (412), valida o payload (422)
S-->>I: 202 Accepted (id, status, idempotencyKey)
S->>S: Verifica idempotência (chave + prestador)
S->>P: Transmite a DPS
P-->>S: Autorizada (número, XML) ou rejeitada (código Exxxx)
S->>I: POST callbackUrl (Identificador, Status, ...)
I-->>S: 2xx
- O integrador envia
POST /v3/nfsecom o headerX-API-KEYe o JSON da nota. - A Safe2Pay autentica a chave (falha é
403), converte o JSON (falha é400), verifica se a conta pode emitir (falha é412) e valida o payload acumulando todos os erros (falha é422). - O pedido válido entra na fila de emissão. Se a emissão estiver indisponível nesse momento, a API devolve
502. - A API responde
202 Acceptedcomdata.id,data.statuse o eco deidempotencyKey. A nota ainda não existe. - A Safe2Pay confere a idempotência (mesma chave e mesmo prestador com nota em curso ou autorizada não gera segunda nota), atribui o número do RPS e transmite a DPS à SEFIN Nacional. A autorização leva cerca de 1 segundo em condições normais.
- A Safe2Pay envia um
POSTaocallbackUrlcom o desfecho:Status2 (autorizada) ouStatus9 (rejeitada). O integrador responde 2xx.
Pré-requisitos
Tudo abaixo é feito no painel Safe2Pay antes da primeira chamada. A API verifica cada item na ordem listada e devolve 412 no primeiro que faltar.
| Requisito | O que é | Erro se faltar (412) |
|---|---|---|
| Serviço de NFS-e habilitado | Contratação comercial do módulo de emissão na conta | nfse.serviceNotEnabled |
| Configuração fiscal | Dados do prestador para emissão (regime, município, URL de callback padrão) | nfse.fiscalSettingsNotFound |
| Certificado digital | Certificado digital A1 (arquivo .pfx) do prestador, enviado pelo painel na configuração de NFS-e. A DPS é assinada com o certificado cadastrado. | nfse.certificateNotFound |
| Inscrição municipal | Inscrição do prestador na prefeitura, gravada na configuração fiscal | nfse.municipalRegistrationNotFound |
Certificado com data de vencimento já passada não bloqueia a chamada (gera apenas aviso interno); a rejeição, se houver, virá da prefeitura pelo callback.
Você também precisa da chave de integração de produção da conta que vai emitir. Não existe sandbox para este endpoint: NFS-e é documento fiscal real e não há prefeitura de homologação disponível ao lojista.
Testes acontecem em produçãoPlaneje os primeiros testes com valores baixos e saiba de antemão por onde vai cancelar a nota emitida. A chave de sandbox recebe
403neste endpoint.O cancelamento de uma nota, inclusive de teste, é feito hoje pelo painel Safe2Pay. Ainda não há endpoint público de cancelamento.
Autenticação
Envie a chave de produção no header X-API-KEY. Não há outro esquema (sem Bearer, sem Basic, sem assinatura). O prestador da nota é o dono dessa chave; não existe campo no payload para emitir em nome de terceiro. Um marketplace não emite pela subconta: a subconta emite com a chave dela.
Toda falha de credencial responde 403 no envelope da API. Não usamos 401 e não enviamos WWW-Authenticate. A chave de sandbox é reconhecida na autenticação, mas recusada em seguida com a mensagem específica abaixo.
| Situação | name | message |
|---|---|---|
Header X-API-KEY ausente ou com valor vazio | Sem credencial | Não foram informados dados validos de autenticação. |
| Chave não pertence a nenhuma conta | Erro ao tentar autenticar. | O merchant não foi encontrado. |
| Chave de sandbox da conta | authentication | NFS-e é documento fiscal real e não tem ambiente de sandbox. Use a chave de produção. |
| Chave que resolveu a conta, mas não coincide com a de produção nem com a de sandbox atuais (caso residual) | authentication | A chave informada não pertence ao ambiente de produção desta conta. |
| Conta sem chave de produção cadastrada | authentication | Não foi possível autenticar com a chave informada. |
A credencial é avaliada antes de qualquer leitura do corpo: uma requisição com chave inválida e JSON malformado recebe 403, não 400. Um 403 nunca enfileira nada: corrija a chave e repita com a mesma idempotencyKey.
Guarde a chave em cofreA chave não deve ir em query string, em log do integrador nem em repositório. Use Azure Key Vault ou equivalente e injete por variável de ambiente.
Idempotência
idempotencyKey é obrigatória, tem até 64 caracteres e é escolhida por você. Seu escopo é o prestador (a conta da chave). Ela atua em dois momentos.
Na solicitação não há verificação. Cada POST aceito gera um data.id novo e enfileira o pedido; a mesma chave garante apenas que pedidos com ela sejam processados em ordem, um por vez. Reenviar a mesma chave devolve 202 de novo, com id diferente. O 202 não diz "já recebido".
No processamento, antes de transmitir, a Safe2Pay procura uma nota do mesmo prestador com a mesma chave em situação 1 (aguardando), 2 (autorizada) ou 3 (RPS encaminhado). Se achar, o segundo pedido é encerrado sem transmitir nada e você recebe um callback Status 9 com o Identificador do segundo pedido e o erro de chave duplicada. A primeira nota segue normalmente e recebe o próprio callback pelo Identificador original. Resultado: a mesma idempotencyKey nunca gera segunda nota, o que também protege contra o E0014 da SEFIN (DPS duplicada).
O conteúdo do payload não é comparado. Payload diferente com a mesma chave é tratado como duplicata enquanto a primeira nota estiver em 1, 2 ou 3: o segundo payload é descartado. Não devolvemos 409 para esse caso.
Se a nota anterior com aquela chave terminou rejeitada (9) ou cancelada (4), a chave fica livre e um novo POST com ela gera nota nova. Esse comportamento é intencional: a chave volta a servir para um pedido corrigido. A idempotência protege notas válidas e em curso; não é histórico permanente da chave.
| Situação | O que fazer com a chave |
|---|---|
400, 403, 412, 422 | Nada foi enfileirado. Corrija e repita com a mesma chave. |
502 ou timeout de rede no POST | O pedido pode ter sido aceito do outro lado. Repita com a mesma chave: no pior caso você recebe um callback de duplicidade para o id novo e o desfecho real para o id original. |
500 | Trate como transitório e repita com a mesma chave; a idempotência protege. |
Callback Status 9 por erro fiscal, e você corrigiu o payload | A mesma chave serve (ficou livre), mas uma chave nova por tentativa distinta evita ambiguidade na sua correlação. |
| Nota nova (outro serviço, outro período) | Chave nova. |
Nunca troque a chave para "tentar de novo"Chave nova para o mesmo pedido significa duas notas fiscais reais na prefeitura. A
idempotencyKeyé a única proteção contra duplicidade.
Callback
O payload, os campos e os exemplos do callback estão na seção Callbacks da referência do endpoint. Aqui fica o que não cabe no contrato: quando ele é disparado, como responder e o que acontece quando a entrega falha.
Quando é enviado
| Origem | Status |
|---|---|
| Nota autorizada pela prefeitura | 2 |
| Prefeitura recusou a nota | 9 |
idempotencyKey repetida com nota em curso ou autorizada | 9, com o Identificador do pedido novo |
| Conta sem configuração fiscal ou sem serviço no momento do processamento | 9 (improvável, porque a API já devolve 412) |
| Nota pendente resolvida por consulta à prefeitura | 2, 4 ou 9, conforme o estado final encontrado |
| Cancelamento feito pelo painel Safe2Pay (operação distinta deste endpoint) | 4, ou 9 se falhar. Enviado para a callbackUrl gravada no pedido original. |
Status 1 e 3 podem ocorrer e são informativos: não encerram o pedido. O desfecho é sempre 2, 4 ou 9. A autorização na prefeitura leva cerca de 1 segundo e o callback chega logo depois, mas dimensione para atrasos.
Como responder
Responda qualquer status 2xx em menos de 30 segundos. O corpo da resposta é ignorado. Faça o processamento pesado fora do ciclo da requisição.
Processe de forma idempotente: o mesmo callback pode chegar mais de uma vez. Use Identificador + Status como chave de deduplicação. Aceite callbacks com Identificador desconhecido, porque, após um 502 ou timeout no POST, o desfecho real pode chegar no id do primeiro 202 que você não recebeu.
Não há header de autenticação, assinatura HMAC ou token no callback; os headers padrão são limpos antes do envio. A API aceita http:// em callbackUrl, mas use HTTPS: o callback carrega dados da nota.
Retentativa
Se seu endpoint responder não-2xx, a entrega é reagendada com backoff de 15 s, 30 s, 1 min, 2 min, 4 min, 8 min e 8 min: 8 tentativas no total, cerca de 23 minutos e 45 segundos de tolerância. Esgotadas as tentativas, a entrega é encerrada; não há reenvio pela API pública, e o suporte da Safe2Pay precisa ser acionado com traceId e data.id.
Quando não há resposta HTTP (timeout de 30 s, falha de DNS, conexão recusada), a entrega não passa por esse backoff: ela volta pela reentrega interna, cujo número de tentativas e intervalo não são definidos por esta página.
Sem consulta por id nesta versãoAinda não existe
GET /v3/nfse/{id}nem consulta poridempotencyKey. O callback é o único canal do desfecho; se as retentativas se esgotarem, vale o que está acima.
Erros
Os envelopes e exemplos de cada status estão em Responses na referência do endpoint. A ordem de avaliação é: autenticação (403), conversão do JSON (400), estado da conta (412), validação do payload com todos os erros de uma vez (422), documento e município do tomador (422, um erro isolado) e validação fiscal (422 nfse.issuer ou 502).
| Status | O que fazer |
|---|---|
400 | Corrija o JSON: tipo de campo, formato de data ou booleano fora de true/false. Nenhuma regra de negócio foi avaliada. Repita com a mesma chave. |
403 | Não retente automaticamente. Use a chave de produção da conta emissora. |
412 | Regularize no painel e repita com a mesma chave. Reenviar o mesmo JSON dá o mesmo 412. |
422 | Corrija todos os campos listados de uma vez e repita com a mesma chave. Preencha o endereço completo do tomador para evitar rejeição posterior na prefeitura. |
500 | Aguarde e repita com a mesma chave. Guarde o traceId para chamado. |
502 | Repita em alguns minutos com a mesma chave. Não altere o payload nem a chave. |
Erros que só a prefeitura detecta (E0240, E0247, códigos de tributação incorretos) não são 422: passam no 202 e voltam como callback Status 9. As rejeições da SEFIN registradas até aqui são E0240 (endereço do tomador) e E0247 (e-mail do tomador).
Catálogo de name e message
name e messageValidação do payload (422, acumulados)
name | message |
|---|---|
idempotencyKey | O atributo idempotencyKey é obrigatório. |
idempotencyKey | O atributo idempotencyKey deve ter no máximo 64 caracteres. |
callbackUrl | O atributo callbackUrl é obrigatório: é por ele que o resultado da nota é entregue. |
callbackUrl | O atributo callbackUrl deve ser uma URL absoluta válida. |
serviceDate | O atributo serviceDate é obrigatório e deve ser informado no formato YYYY-MM-DD. |
serviceDate | O atributo serviceDate não pode estar no futuro. |
rpsIdentifier.series | O atributo rpsIdentifier.series é obrigatório. (também quando o objeto rpsIdentifier inteiro é omitido) |
operationType | O atributo operationType deve estar entre 1 e 6. |
specialTaxRegime | O atributo specialTaxRegime deve estar entre 0 e 6. |
service | O atributo service é obrigatório. (interrompe a validação dos filhos; os erros anteriores permanecem) |
service.values | O atributo service.values é obrigatório. |
service.values.amount | O atributo service.values.amount deve ser maior que zero. |
service.withholdingAgent | O atributo service.withholdingAgent deve estar entre 1 e 2. (só quando informado e diferente de 0) |
service.issEnforceability | O atributo service.issEnforceability deve estar entre 1 e 7. |
service.description | O atributo service.description deve ter no máximo 2000 caracteres. |
service.serviceRecipient | O atributo service.serviceRecipient é obrigatório. (interrompe a validação dos filhos) |
service.serviceRecipient.name | O atributo service.serviceRecipient.name é obrigatório. |
service.serviceRecipient.identity | O atributo service.serviceRecipient.identity é obrigatório. |
service.serviceRecipient.email | O atributo service.serviceRecipient.email é obrigatório. |
service.serviceRecipient.email | O atributo service.serviceRecipient.email não é um e-mail válido. |
service.serviceRecipient.address | O atributo service.serviceRecipient.address é obrigatório. |
service.serviceRecipient.address.number | O atributo service.serviceRecipient.address.number é obrigatório. |
Tomador (422, erro isolado, só após a lista acima passar)
name | message |
|---|---|
service.serviceRecipient.identity | O documento do tomador precisa ser um CPF com 11 dígitos ou um CNPJ com 14. |
service.serviceRecipient.address.municipalityCode | Não foi possível determinar o município do tomador. Informe municipalityCode com o código IBGE de 7 dígitos. |
Estado da conta (412)
name | message |
|---|---|
nfse.serviceNotEnabled | O serviço de emissão de NFS-e não está habilitado para esta conta. |
nfse.fiscalSettingsNotFound | Esta conta não tem configuração fiscal de NFS-e cadastrada. |
nfse.certificateNotFound | Esta conta não tem certificado digital cadastrado para emitir NFS-e. |
nfse.municipalRegistrationNotFound | Esta conta não tem inscrição municipal cadastrada na configuração fiscal. |
Validação fiscal (422, name = nfse.issuer)
Recusas detectadas na preparação da DPS, depois que o payload passou na validação de campos. Cada mensagem vem como um item de errors.
message | Como provocar |
|---|---|
O Documento deve ter 11 ou 14 caracteres (CPF ou CNPJ). | identity com máscara (CNPJ com 18 caracteres) |
O Nome/Razão Social deve ter no máximo 150 caracteres. | name longo |
O e-mail informado não é válido. | email com @ no início ou no fim, ou com mais de um @ |
O Número deve ter no máximo 10 caracteres. | address.number longo |
O Município é obrigatório. | address.cityName ausente |
O Município deve ter no máximo 100 caracteres. | address.cityName longo |
O Código do País deve ter entre 2 e 4 dígitos. | address.countryCode fora de 2 a 4 caracteres |
A série do RPS deve ter no máximo 5 caracteres. | rpsIdentifier.series longa |
O serviço de emissão recusou o pedido sem detalhar o motivo. | Recusa sem lista de erros nem mensagem |
Outros name
| Status | name | message |
|---|---|---|
400 | caminho completo do campo em camelCase (ex.: serviceDate, service.values.amount, service.isIssWithheld) | Mensagem do conversor. Para booleano: O atributo <caminho> deve ser true ou false; foi informado um valor do tipo <tipo JSON>. Para os demais tipos, a mensagem do conversor de JSON; se ele não informar nenhuma, O valor informado para <caminho> não é válido. |
400 | body | O corpo da requisição deve ser um JSON válido. Vale para JSON com erro de sintaxe, corpo vazio, em branco ou null. |
403 | ver a seção Autenticação | ver a seção Autenticação |
422 | nfse | Mensagem variável de exceção de negócio não prevista durante a emissão. |
500 | internal | Ocorreu um erro inesperado ao processar o pedido. Informe o traceId ao suporte da Safe2Pay. |
502 | nfse.issuerUnavailable | O serviço de emissão de NFS-e não respondeu. Tente novamente em alguns minutos com a mesma idempotencyKey. |
Boas práticas e armadilhas
Endereço completo do tomador. Preencha zipCode (8 dígitos), street, district, cityName, stateCode e municipalityCode coerentes entre si, embora a API exija só number. O E0240 é a rejeição mais frequente em produção e chega pelo callback, depois do 202.
E-mail válido. A API confere apenas a presença de um @ com texto antes e depois; a SEFIN rejeita com E0247. Valide o formato completo antes de enviar.
Código IBGE sempre vence. Com address.municipalityCode de 7 dígitos, cityName e stateCode não são usados para resolver. Código com tamanho diferente de 7 é ignorado em silêncio. Sem código válido, a busca por cityName + stateCode precisa achar exatamente um município; não há aproximação. Mesmo com o código informado, cityName continua obrigatório na validação fiscal.
Documento só com dígitos. identity segue como enviado: 12345678909 e 11222333000181, nunca com pontuação. Um CPF com máscara pode atravessar a validação e chegar pontuado à prefeitura.
Códigos de tributação com a contabilidade. nationalTaxCode (6 dígitos), serviceListItem, municipalTaxCode e nbsCode (9 dígitos) não são validados semanticamente. Código errado atravessa tudo e vira rejeição ou nota errada na prefeitura.
Booleanos são true/false. Nunca 0, 1 ou 2. Enviar número gera 400, e é bom que gere: num documento fiscal, um valor ambíguo poderia declarar o oposto do pretendido.
Decimais como número JSON, com ponto e duas casas. 1000.00, não 1.000,00. O conversor tolera string numérica, mas o contrato é número JSON; não dependa da tolerância.
Aritmética dos valores é sua. A API só valida amount > 0. amountTaxBase é recalculado como amount - amountDeductions; amountPis e amountCofins zerados são calculados a partir das alíquotas; amountNet vai como informado. A prefeitura pode recusar inconsistências.
serviceDate não pode estar no futuro, em horário de Brasília. Faturamento gerado à meia-noite UTC com a data do dia seguinte é recusado com 422.
Retry só em 502, 500 e timeout, sempre com a mesma idempotencyKey. Backoff de minutos, não segundos.
Leia o status HTTP antes da mensagem. 400 corrige o tipo; 422 corrige o payload (a lista é completa); 412 corrige no painel; 502 repete depois; 403 corrige a chave.
Receptor de callback idempotente e rápido. Deduplique por Identificador + Status, responda 2xx em menos de 30 s, aceite Identificador desconhecido após 502 ou timeout no POST.
Não ecoe nem logue dados pessoais. O payload carrega nome, CPF/CNPJ, e-mail e endereço do tomador (LGPD). A resposta 202 propositalmente não os devolve; seu log também não deve.
O prestador é o dono da chave. Marketplace não emite pela subconta; a subconta emite com a chave dela.
Limites e tamanhos
| Item | Limite | Onde é aplicado |
|---|---|---|
idempotencyKey | 64 caracteres | Validação do payload (422) |
service.description | 2000 caracteres | Validação do payload (422) |
rpsIdentifier.series | 5 caracteres | Validação fiscal (422 nfse.issuer) |
service.serviceRecipient.name | 150 caracteres | Validação fiscal (422 nfse.issuer) |
service.serviceRecipient.address.number | 10 caracteres | Validação fiscal (422 nfse.issuer) |
service.serviceRecipient.address.cityName | 100 caracteres | Validação fiscal (422 nfse.issuer) |
service.serviceRecipient.address.countryCode | 2 a 4 caracteres | Validação fiscal (422 nfse.issuer) |
| Rate limit | Nenhum | Decisão de projeto: faturamentos reais emitem centenas de notas em poucos minutos e um teto por chave devolveria 429 no meio do lote. O que protege contra repetição é a idempotencyKey. |
| Timeout interno de emissão | 30 s | Estourado, a API responde 502 |
| Timeout do callback para o seu endpoint | 30 s | Não-2xx reagenda até 8 tentativas com backoff; sem resposta HTTP, vale a reentrega interna |
| Tamanho máximo do corpo | Não configurado pela aplicação | Vale o padrão do servidor web que hospeda a API e o que a borda (WAF, App Gateway) impuser |
Glossário
| Termo | Significado |
|---|---|
| NFS-e | Nota Fiscal de Serviços eletrônica: documento fiscal que comprova a prestação de um serviço e o ISS devido, emitido no padrão nacional e autorizado pela prefeitura via SEFIN Nacional. |
| RPS | Recibo Provisório de Serviços: identificação do pedido de emissão que antecede a nota. Você informa a série; o número é atribuído pela Safe2Pay. |
| DPS | Declaração de Prestação de Serviços: o documento eletrônico enviado à SEFIN Nacional que, autorizado, dá origem à NFS-e. DPS idêntica reenviada gera E0014. |
| SEFIN Nacional | Sistema Nacional da NFS-e (Receita Federal e municípios conveniados) que recebe a DPS, valida e autoriza ou rejeita, devolvendo códigos como E0240 e E0247. |
| ISS | Imposto Sobre Serviços de Qualquer Natureza, tributo municipal sobre o valor do serviço (rateIss, amountIss), recolhido pelo prestador ou retido pelo tomador. |
| PIS | Programa de Integração Social, contribuição federal sobre a receita (ratePis, amountPis), passível de retenção na fonte por tomador pessoa jurídica. |
| COFINS | Contribuição para o Financiamento da Seguridade Social, contribuição federal sobre a receita (rateCofins, amountCofins), passível de retenção na fonte por tomador pessoa jurídica. |
| Prestador | Quem executa o serviço e emite a nota. Nesta API, sempre o titular da X-API-KEY. |
| Tomador | Quem contratou e recebe o serviço (serviceRecipient), identificado por nome, CPF ou CNPJ, e-mail e endereço. Pode ser o responsável por reter o ISS. |
| Código IBGE | Código de 7 dígitos que identifica cada município brasileiro (ex.: 4314902 = Porto Alegre). |
| NBS | Nomenclatura Brasileira de Serviços, código de 9 dígitos (nbsCode) complementar ao item da LC 116. |
| LC 116 | Lei Complementar 116/2003, que define a lista nacional de serviços tributáveis pelo ISS. serviceListItem (ex.: 04.12) e nationalTaxCode (ex.: 041201) derivam dela. |
| Competência | Data em que o serviço foi prestado e o ISS se tornou devido (serviceDate). Não pode estar no futuro. |