Guia de integração

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 fiscal

Esta 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
  1. O integrador envia POST /v3/nfse com o header X-API-KEY e o JSON da nota.
  2. 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).
  3. O pedido válido entra na fila de emissão. Se a emissão estiver indisponível nesse momento, a API devolve 502.
  4. A API responde 202 Accepted com data.id, data.status e o eco de idempotencyKey. A nota ainda não existe.
  5. 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.
  6. A Safe2Pay envia um POST ao callbackUrl com o desfecho: Status 2 (autorizada) ou Status 9 (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.

RequisitoO que éErro se faltar (412)
Serviço de NFS-e habilitadoContratação comercial do módulo de emissão na contanfse.serviceNotEnabled
Configuração fiscalDados do prestador para emissão (regime, município, URL de callback padrão)nfse.fiscalSettingsNotFound
Certificado digitalCertificado 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 municipalInscrição do prestador na prefeitura, gravada na configuração fiscalnfse.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ção

Planeje os primeiros testes com valores baixos e saiba de antemão por onde vai cancelar a nota emitida. A chave de sandbox recebe 403 neste 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çãonamemessage
Header X-API-KEY ausente ou com valor vazioSem credencialNão foram informados dados validos de autenticação.
Chave não pertence a nenhuma contaErro ao tentar autenticar.O merchant não foi encontrado.
Chave de sandbox da contaauthenticationNFS-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)authenticationA chave informada não pertence ao ambiente de produção desta conta.
Conta sem chave de produção cadastradaauthenticationNã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 cofre

A 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çãoO que fazer com a chave
400, 403, 412, 422Nada foi enfileirado. Corrija e repita com a mesma chave.
502 ou timeout de rede no POSTO 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.
500Trate como transitório e repita com a mesma chave; a idempotência protege.
Callback Status 9 por erro fiscal, e você corrigiu o payloadA 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

OrigemStatus
Nota autorizada pela prefeitura2
Prefeitura recusou a nota9
idempotencyKey repetida com nota em curso ou autorizada9, com o Identificador do pedido novo
Conta sem configuração fiscal ou sem serviço no momento do processamento9 (improvável, porque a API já devolve 412)
Nota pendente resolvida por consulta à prefeitura2, 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ão

Ainda não existe GET /v3/nfse/{id} nem consulta por idempotencyKey. 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).

StatusO que fazer
400Corrija 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.
403Não retente automaticamente. Use a chave de produção da conta emissora.
412Regularize no painel e repita com a mesma chave. Reenviar o mesmo JSON dá o mesmo 412.
422Corrija 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.
500Aguarde e repita com a mesma chave. Guarde o traceId para chamado.
502Repita 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

Validação do payload (422, acumulados)
namemessage
idempotencyKeyO atributo idempotencyKey é obrigatório.
idempotencyKeyO atributo idempotencyKey deve ter no máximo 64 caracteres.
callbackUrlO atributo callbackUrl é obrigatório: é por ele que o resultado da nota é entregue.
callbackUrlO atributo callbackUrl deve ser uma URL absoluta válida.
serviceDateO atributo serviceDate é obrigatório e deve ser informado no formato YYYY-MM-DD.
serviceDateO atributo serviceDate não pode estar no futuro.
rpsIdentifier.seriesO atributo rpsIdentifier.series é obrigatório. (também quando o objeto rpsIdentifier inteiro é omitido)
operationTypeO atributo operationType deve estar entre 1 e 6.
specialTaxRegimeO atributo specialTaxRegime deve estar entre 0 e 6.
serviceO atributo service é obrigatório. (interrompe a validação dos filhos; os erros anteriores permanecem)
service.valuesO atributo service.values é obrigatório.
service.values.amountO atributo service.values.amount deve ser maior que zero.
service.withholdingAgentO atributo service.withholdingAgent deve estar entre 1 e 2. (só quando informado e diferente de 0)
service.issEnforceabilityO atributo service.issEnforceability deve estar entre 1 e 7.
service.descriptionO atributo service.description deve ter no máximo 2000 caracteres.
service.serviceRecipientO atributo service.serviceRecipient é obrigatório. (interrompe a validação dos filhos)
service.serviceRecipient.nameO atributo service.serviceRecipient.name é obrigatório.
service.serviceRecipient.identityO atributo service.serviceRecipient.identity é obrigatório.
service.serviceRecipient.emailO atributo service.serviceRecipient.email é obrigatório.
service.serviceRecipient.emailO atributo service.serviceRecipient.email não é um e-mail válido.
service.serviceRecipient.addressO atributo service.serviceRecipient.address é obrigatório.
service.serviceRecipient.address.numberO atributo service.serviceRecipient.address.number é obrigatório.
Tomador (422, erro isolado, só após a lista acima passar)
namemessage
service.serviceRecipient.identityO documento do tomador precisa ser um CPF com 11 dígitos ou um CNPJ com 14.
service.serviceRecipient.address.municipalityCodeNã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)
namemessage
nfse.serviceNotEnabledO serviço de emissão de NFS-e não está habilitado para esta conta.
nfse.fiscalSettingsNotFoundEsta conta não tem configuração fiscal de NFS-e cadastrada.
nfse.certificateNotFoundEsta conta não tem certificado digital cadastrado para emitir NFS-e.
nfse.municipalRegistrationNotFoundEsta 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.

messageComo 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
Statusnamemessage
400caminho 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.
400bodyO corpo da requisição deve ser um JSON válido. Vale para JSON com erro de sintaxe, corpo vazio, em branco ou null.
403ver a seção Autenticaçãover a seção Autenticação
422nfseMensagem variável de exceção de negócio não prevista durante a emissão.
500internalOcorreu um erro inesperado ao processar o pedido. Informe o traceId ao suporte da Safe2Pay.
502nfse.issuerUnavailableO 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

ItemLimiteOnde é aplicado
idempotencyKey64 caracteresValidação do payload (422)
service.description2000 caracteresValidação do payload (422)
rpsIdentifier.series5 caracteresValidação fiscal (422 nfse.issuer)
service.serviceRecipient.name150 caracteresValidação fiscal (422 nfse.issuer)
service.serviceRecipient.address.number10 caracteresValidação fiscal (422 nfse.issuer)
service.serviceRecipient.address.cityName100 caracteresValidação fiscal (422 nfse.issuer)
service.serviceRecipient.address.countryCode2 a 4 caracteresValidação fiscal (422 nfse.issuer)
Rate limitNenhumDecisã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ão30 sEstourado, a API responde 502
Timeout do callback para o seu endpoint30 sNão-2xx reagenda até 8 tentativas com backoff; sem resposta HTTP, vale a reentrega interna
Tamanho máximo do corpoNão configurado pela aplicaçãoVale o padrão do servidor web que hospeda a API e o que a borda (WAF, App Gateway) impuser

Glossário

TermoSignificado
NFS-eNota 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.
RPSRecibo 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.
DPSDeclaraçã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 NacionalSistema 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.
ISSImposto Sobre Serviços de Qualquer Natureza, tributo municipal sobre o valor do serviço (rateIss, amountIss), recolhido pelo prestador ou retido pelo tomador.
PISPrograma de Integração Social, contribuição federal sobre a receita (ratePis, amountPis), passível de retenção na fonte por tomador pessoa jurídica.
COFINSContribuiçã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.
PrestadorQuem executa o serviço e emite a nota. Nesta API, sempre o titular da X-API-KEY.
TomadorQuem 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 IBGECódigo de 7 dígitos que identifica cada município brasileiro (ex.: 4314902 = Porto Alegre).
NBSNomenclatura Brasileira de Serviços, código de 9 dígitos (nbsCode) complementar ao item da LC 116.
LC 116Lei 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ênciaData em que o serviço foi prestado e o ISS se tornou devido (serviceDate). Não pode estar no futuro.