NFS-e
Issue service invoices (NFS-e) on behalf of a registered issuer: onboarding flow, municipal channel differences, inbound manifestation, webhook payloads, error model, state machine and an integration checklist.
Capabilities and onboarding flow
| Step | Endpoint | Notes |
|---|---|---|
| 1 Register issuer | POST /openapi/v2/empresas | Same as NF-e, with a new optional emissaoNFSe section; the NF-e block may be omitted if you only issue NFS-e, and IE is not required then |
| 2 Bind certificate | POST /openapi/v1/empresas/{empresaId}/certificadoDigital | Same as NF-e |
| 3 Register callback | POST /openapi/v1/webhooks | Same as NF-e; NFS-e results reuse the same callback URL |
| 4 Issue | POST /openapi/v2/empresas/{empresaId}/nfse | Returns HTTP 200 with no body on acceptance; filing is asynchronous |
| 5 Query | GET /openapi/v2/empresas/{empresaId}/nfse/{nfseId} | Status, events, XML / DANFSe download links, request echo |
| 5-A Reconcile | GET …/nfse/emitidas · GET …/nfse/emitidas/{chaveAcesso} | Issued list (time-window paging) and lookup by access key |
| 6 Cancel | POST /openapi/v2/empresas/{empresaId}/nfse/{nfseId}/cancelar | Asynchronous: 200 with an acceptance receipt, outcome via webhook |
| 7 Query received invoices | GET …/nfse/recebidas · GET …/nfse/recebidas/{chaveAcesso} | List of received invoices (paged by period) and lookup of one by key; this is where the chaveAcesso used to manifest comes from |
| 8 Manifest on a received invoice | POST …/recebidas/{chaveAcesso}/manifestar | Register a confirmation / rejection of a received NFS-e; the body carries tipo |
Authentication is identical to NF-e: headers token / timestamp / sign, see Authentication.
Three structural differences from NF-e
1. The access key is 50 digits and is generated by the issuing authority. An NF-e's 44-digit key is computed by the taxpayer; an NFS-e key only exists once the authority has accepted the document. A failed document therefore has no chaveAcesso and can only be located by identificacaoDps, the 45-character DPS identifier we generate.
2. Service invoices are governed by the municipality. The platform routes by the issuer's municipality; the resulting channel appears as padraoMunicipal in the query response. The national standard (Nacional) and the São Paulo municipal system (Paulistana) are supported today; an unsupported municipality is rejected at acceptance (10018010) rather than accepted and failed asynchronously.
3. Cancellation has a window, and outside it the answer is not "you cannot cancel" but "it goes to municipal review". See Cancel NFS-e.
Prerequisites
- The issuer must be approved with a usable certificate, otherwise acceptance is rejected (
10018004). - The issuer must have an NFS-e profile, created through the
emissaoNFSesection of Register company or maintained by operations in the console. Simples status, Simples assessment, special regime and DPS series always come from the profile; the issuance request neither sends nor overrides them. Without a profile, acceptance rejects (10018005). Service code, CNAE, NBS and the other service-level fields are not in the profile: they go with every invoice. ambienteEmissaomust match the issuer's current environment. After registration an issuer sits in the test environment (Homologacao); switching to production is an operations action and is not exposed through the API.- Tax values follow the request: what it carries prevails, and only what is missing is looked up by the platform in the tax reference data, such as the LC 116 list, the federal withholding classification and rates and the IBPT approximate burden. When the data needed for federal withholding, the own PIS/COFINS rates or the ISS place of taxation has not been imported yet, acceptance rejects with
10018065, listing the request fields that can replace it. - When the recipient is a legal entity (CNPJ), federal withholding applies: send the amounts in
servico.retencoes, where zero declares no withholding, or omit them together withpisCofins.tipoRetencaoand the platform determines them from the official withholding classification of the service pluscliente.orgaoPublicoandcliente.optanteSimplesNacional; sendingpisCofins.tipoRetencaowithout the amounts is rejected (10018001). Missing information is rejected explicitly (10018023) with every missing item listed at once, rather than treated as zero.
Extra constraints on the São Paulo channel
A São Paulo issuer (padraoMunicipal = Paulistana) needs its municipal registration CCM (inscricaoMunicipal): when it is in the issuer profile it is used as is; when it is missing the platform queries the municipal system by CNPJ at acceptance and stores the result in the profile; acceptance is rejected (10018005) only when nothing is found.
The channel does not support discount fields: fold discounts into the service amount or the deduction. It does not support substitute issuance either (the municipal RPS layout has no such group): cancel the original and issue a new one; sending it is rejected at acceptance (10018001).
It also gets two automatic capabilities that need no new endpoint: São Paulo invoices it receives are pulled directly from the municipal system into the inbound list (origem=PrefeituraSP, merged automatically once the ADN transcription arrives); an outbound invoice cancelled on the municipal portal is synchronized to Cancelada and nfse.canceled is sent as usual.
State machine
┌──────────────► Negada (terminal)│Pendente ────────┼──────────────► Autorizada ──┬──► Cancelada (terminal)│ │└──────────────► Falha ├──► EmProcessoDeCancelamento ──┬──► Cancelada(failed before numbering, │ └──► Autorizada (review refused)no document exists) └──► Substituida (replaced)
| Status | Meaning | Terminal |
|---|---|---|
Pendente | Accepted, no outcome yet | no |
Autorizada | Issued successfully | no (can be cancelled or replaced) |
Negada | Rejected by the authority | yes |
Falha | Platform-side failure (bad parameters / issuer not eligible); no document was created | yes |
Cancelada | Cancelled | yes |
EmProcessoDeCancelamento | Cancellation review accepted, awaiting the municipality | no |
Substituida | Replaced by another document | yes |
Manifestation status of a received invoice
A received invoice's status uses Autorizada / Cancelada / Substituida from the table above; this issuer's manifestation is kept separately in the manifestacao block, and each party manifests at most once per invoice.
(not manifested, list filter Pendente) ──┬──► Confirmada (confirmed by this issuer, terminal)├──► Rejeitada (rejected by this issuer) ──► RejeicaoAnulada (rejection annulled by the municipality, terminal)└──► ConfirmacaoTacita (tacit confirmation by the municipality, terminal)
| Status | Meaning | Can still manifest |
|---|---|---|
null (filter value Pendente) | Not manifested | yes (while the invoice is Autorizada) |
Confirmada | Confirmed by this issuer | no |
Rejeitada | Rejected by this issuer | no |
ConfirmacaoTacita | Tacitly confirmed by the municipality after the deadline | no |
RejeicaoAnulada | The municipality annulled this issuer's rejection | no |
Webhook callbacks
Delivered to the URL you register as a bare JSON payload.
| Event code | Trigger | nfseStatus |
|---|---|---|
nfse.authorized | Issued successfully | Autorizada |
nfse.rejected | Rejected by the authority or failed on our side | Negada |
nfse.canceled | Cancellation completed (including an approved review) | Cancelada |
nfse.cancel_requested | Cancellation review accepted | EmProcessoDeCancelamento |
nfse.cancel_denied | Cancellation did not happen and the document stays authorized: synchronous refusal by the authority, dispatch exhausted, or review request refused by the municipality | Autorizada |
nfse.substituted | This document was replaced by a substitute; the payload carries an extra nfseChaveSubstituta | Substituida |
nfse.inbound.received | Inbound NFS-e received (this issuer is the recipient / intermediary, explicit subscription required) | — |
nfse.inbound.canceled | Inbound NFS-e cancelled (explicit subscription required) | — |
nfse.inbound.substituted | Inbound NFS-e substituted (explicit subscription required) | — |
Note: Subscription semantics. An app that has never registered any subscription row receives all six outbound events. As soon as any subscription row exists for the app (for example an inbound event), the app switches to allow-list mode and only explicitly enabled events are delivered. When subscribing to inbound events, enable the outbound events you need as well, otherwise outbound notifications stay parked waiting for a target.
{"tipo": "NFS-e","empresaId": "1001","nfseId": "PED-20260918-001","nfseStatus": "Autorizada","nfseMotivoStatus": null,"nfseLinkDanfse": "https://api.v2.tffiscal.com/openapi/files/danfse/…","nfseLinkXml": "https://api.v2.tffiscal.com/openapi/files/xml/9001?token=…","nfseNumero": "1287","nfseSerie": "1","nfseChaveAcesso": "3550308…","nfseIdentificacaoDps": "DPS3550308…","nfseDataEmissao": "2026-09-18T13:00:00Z","nfseDataAutorizacao": "2026-09-18T13:00:03Z","nfseNumeroProtocolo": null,"nfseAtualizadoEm": "2026-09-18T13:00:03Z"}
nfseAtualizadoEm is the moment of this state change. Delivery order is not guaranteed: retries and multiple instances can make an older event arrive after a newer one. Compare it with the timestamp you already hold and drop the older one instead of overwriting state in arrival order.
On a failure callback nfseChaveAcesso may be empty (the authority never generated a key); reconcile by nfseIdentificacaoDps then. A callback for a failure before numbering carries neither, so only nfseId can reconcile it.
Error model
Public endpoints return a bare array:
[{"codigo": "10018012", "mensagem": "Codigo de servico fora da lista oficial LC 116: 999999"}]
| Code | Meaning | What to do |
|---|---|---|
10018001 | Parameter validation refused at acceptance (the message states the reason) | Fix as the message says |
NFSe0001 | Document not found: nfseId does not belong to this issuer, or the inbound key is not among this issuer's received invoices | Check nfseId / chaveAcesso against empresaId |
10018004 | Issuer not eligible (disabled / certificate unusable) | Check issuer status and certificate |
10018005 | Issuer has no NFS-e profile | Create it first with Register company carrying the emissaoNFSe section |
10018010 | The municipality's NFS-e standard is not supported yet | That municipality cannot issue yet, contact the platform |
10018012 | Service code is not in the LC 116 list | Check codigoTributacaoNacional |
10018013 | Recipient document is invalid | Check the CPF / CNPJ / NIF |
10018014 | Service export is missing the destination country | Add servico.paisPrestacao |
10018015 | The replaced document cannot be replaced | Check the status of the replaced document |
10018020 | The ISS rate cannot be determined | Send servico.iss.aliquota or the right servico.codigoTributacaoMunicipal |
10018022 | A Simples issuer must not send the ISS rate directly | Remove aliquota |
10018023 | Federal withholding cannot be determined yet; the message lists every missing item | Provide the listed fields or send the withheld amounts in servico.retencoes |
10018030 | The same request is already being processed | Retry later |
10018031 | That order already has an invoice, or the same id carries different content | Use another order number or check the content |
10018040 | A cancellation review is already in progress | Wait for the verdict |
10018041 | The document has already been replaced | No further operation is allowed |
10018042 | The current status does not allow this event | Check the document status |
10018063 | The DPS message does not comply with the official XSD (details carry line, column, error key and element name) | Fix as detailed: usually text outside Latin-1, an incomplete address, or a value outside the official code table |
10018064 | The NFS-e event message does not comply with the official XSD | Check motivo length (15-255), the codigoMotivo value and the character set |
10018065 | The NFS-e tax reference data this invoice needs has not been imported, rejected at acceptance; the message lists each item with an arrow to the request field that replaces it | Send the fields after the arrow in the request, or ask the platform to confirm the data is imported; for iss-local-incidencia, sending servico.iss.municipioIncidencia is enough |
Authentication and authorization errors (10009xxx) are in Authentication; the full table is in Error codes.
Integration checklist
- Register the issuer (with
emissaoNFSe) → bind the certificate → register the webhook. - Issue a minimal invoice in
Homologacao(required fields only), confirmnfse.authorizedarrives and that the XML and DANFSe download. - Submit the same
idagain and confirm no second invoice is issued. - Send a service code that does not exist and confirm a synchronous
10018012refusal rather than an asynchronousNegada. - Cancel an authorized invoice and confirm it reaches
CanceladaorEmProcessoDeCancelamentodepending on the window. - Query a failed document and confirm it reconciles by
identificacaoDpsrather thanchaveAcesso. - Manifest on a received invoice with
tipo=Confirmacao, then check the detail showsmanifestacao.status=Confirmada; sendtipo=Rejeicaoon the same invoice and confirm10018042(each party manifests once).
