Caracteres especiais nos campos de texto da NFS-e
🇺🇸 This page is also available in English: Special characters in NFS-e text fields.
Campos de texto livre da NFS-e — descrição do serviço, informações complementares, razão social e endereço do tomador — aceitam conjuntos de caracteres diferentes conforme o município de emissão. Um mesmo texto pode ser aceito num município e recusado em outro.
Esta página descreve como a plataforma trata cada caso, para você saber o que enviar e o que fazer quando uma nota é recusada.
Em uma frase
Se todos os seus textos usarem apenas letras, números, pontuação comum e acentos do português (o conjunto Latin-1, de U+0000 a U+00FF), eles são aceitos em todos os provedores. Os problemas vêm de caracteres que parecem inofensivos mas estão fora desse conjunto: travessão –, aspas curvas " ", reticências …, bullet •, emoji e caracteres invisíveis.
Acento é seguro. Aspas curvas, travessão e emoji não são.
As três camadas de validação
Um texto enviado na emissão passa por três verificações independentes, nesta ordem:
| Ordem | Camada | O que faz | Quando falha |
|---|---|---|---|
| 1 | API | Recusa o que é impossível representar em documento fiscal eletrônico | HTTP 400 na hora da requisição |
| 2 | Plataforma | Normaliza o texto para o formato que o provedor do município exige | — (ajuste silencioso, quando é seguro) |
| 3 | Provedor municipal | Valida contra o schema da prefeitura | Nota recusada, com mensagem no webhook |
Camada 1 — Validação na API
Acontece no momento da requisição, antes de qualquer processamento.
Corpo precisa estar em UTF-8
O JSON deve ser enviado codificado em UTF-8. Corpo em outra codificação é recusado com HTTP 400:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"$": ["O corpo da requisicao nao esta em UTF-8. Reenvie o JSON codificado em UTF-8."]
},
"traceId": "00-1f3a…-b7c2…-01"
}
A resposta segue o formato application/problem+json, o mesmo dos demais erros de validação da API. Guarde o traceId: é por ele que o suporte localiza a requisição.
Texto originado em sistemas legados que usam Windows-1252 e é enviado sem conversão. Acentos viram bytes inválidos no meio do caminho.
Caracteres impossíveis em XML são recusados
A NFS-e é transmitida à prefeitura como XML. Alguns caracteres não existem em XML 1.0 — o principal é o caractere nulo U+0000, mas a regra vale para todos os caracteres de controle proibidos pelo padrão.
Quando o corpo contém um deles, a resposta é HTTP 400 nomeando a propriedade e o code point:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"description": [
"Contém o caractere U+0000, inválido em XML 1.0 e não permitido em documento fiscal eletrônico. Remova o caractere e reenvie a requisição."
]
},
"traceId": "00-1f3a…-b7c2…-01"
}
Até 10 ocorrências distintas são reportadas de uma vez, para você corrigir tudo numa passada em vez de descobrir um campo por tentativa.
$A chave do objeto errors é o nome da propriedade em que o caractere apareceu. Quando ele não pertence ao valor de nenhuma propriedade — está numa chave do JSON ou num elemento solto de array —, a chave vem como $, que é a convenção da API para erro de corpo sem caminho de propriedade. Nomes de propriedade muito longos são truncados na resposta.
U+XXXXVários desses caracteres são invisíveis. Se a mensagem imprimisse o caractere em si, você leria "contém o caractere " sem ver caractere nenhum. O code point é o que permite localizá-lo no seu payload.
O que é ajustado em vez de recusado
Caracteres que podem ser representados em XML, mas exigem tratamento — como &, < e > — são escapados automaticamente. Nada é removido do seu texto nessa etapa.
Camada 2 — Normalização pela plataforma
Quando o provedor do município exige um formato específico e a conversão é inequívoca, a plataforma ajusta o texto sozinha. O critério é não alterar o sentido do que você enviou.
Transliteração para Latin-1 (padrão nacional)
Nos municípios que usam o padrão nacional da NFS-e, os campos de texto seguem um tipo de schema que aceita apenas U+0020 a U+00FF e não admite quebra de linha nem tabulação. Antes do envio, a plataforma aplica:
| Entrada | Vira | Observação |
|---|---|---|
Quebra de linha, tabulação, CR | espaço | Espaços consecutivos colapsam em um |
– — − • (travessão, bullet) | - | Hífen comum |
' ' ′ (aspas simples curvas) | ' | Apóstrofo reto |
" " ″ (aspas duplas curvas) | " | Aspas retas |
… (reticências) | ... | Três pontos |
| Emoji, ideogramas, símbolos sem equivalente | espaço | Não há conversão possível |
Acentos, ç, ª, º | inalterado | Latin-1 passa intacto |
O texto é aparado no início e no fim. Nada é truncado nesta etapa — o limite de tamanho é validado separadamente, por campo.
No padrão nacional, o campo de discriminação do serviço usa um tipo de schema mais permissivo e aceita quebras de linha. A normalização acima não se aplica a ele. Já as informações complementares seguem a regra restrita.
Se um campo contiver apenas caracteres sem equivalente (por exemplo, só emoji), o resultado da normalização é vazio — e o campo é omitido da nota, porque o schema exige ao menos um caractere. O texto não aparece no documento emitido.
Remoção de acentos
Uma parte dos provedores municipais não aceita acentuação. Para eles, a plataforma remove os sinais diacríticos automaticamente antes do envio: São Paulo vira Sao Paulo, informações vira informacoes. A letra base é preservada; nada é descartado.
Campos de largura fixa
Alguns municípios recebem a nota em formato de texto posicional, não XML. Nesses casos, além da remoção de acentos, cada campo é ajustado ao tamanho exato exigido pelo layout — texto mais longo é cortado no limite do campo.
Camada 3 — Validação do provedor municipal
Aqui a nota já saiu da plataforma. A recusa vem da prefeitura e chega até você pelo webhook e pela consulta da nota.
Provedores que recusam fora de Latin-1
Alguns provedores recusam a nota inteira quando qualquer campo de texto contém caractere acima de U+00FF, em vez de aceitar uma conversão. O caso mais relevante é o da Prefeitura de São Paulo (NFS-e Paulistana).
Nesse cenário a plataforma não translitera o texto: a recusa acontece antes do envio à prefeitura, com uma mensagem que nomeia campo, caractere e code point:
[E1002] A Prefeitura de São Paulo não aceita o caractere '–' (U+2013) no campo
Discriminação dos serviços (Discriminacao). Os campos de texto admitem apenas
caracteres do conjunto Latin-1 (ISO-8859-1). Substitua por um equivalente:
travessão por hífen '-', reticências por '...', aspas curvas por aspas retas,
caractere invisível pela remoção. Depois, reenvie a nota.
Até 10 ocorrências distintas são listadas por recusa.
Essa verificação prévia depende da configuração do município e do provedor. Onde ela não está em vigor, a nota é enviada e quem recusa é a própria prefeitura, com um erro de schema genérico — tipicamente [1001] XML não compatível com Schema — que não aponta o campo nem o caractere. O tratamento é o mesmo: procure caracteres fora do Latin-1 nos campos de texto, começando pelos invisíveis.
Uma recusa você resolve em segundos reenviando o texto ajustado. Um texto alterado dentro de uma nota já autorizada pela prefeitura não tem volta — o documento fiscal fica com conteúdo diferente do que você enviou. Por isso, quando a conversão não é inequívoca, a plataforma prefere devolver o erro.
A regra vale para todos os campos de texto, não só a discriminação. Os mais afetados na prática:
- Discriminação dos serviços
- Razão social do tomador
- Logradouro, número, complemento e bairro do endereço
- E-mail do tomador e do intermediário
Provedores que exigem apenas ASCII
Um pequeno grupo de provedores rejeita qualquer caractere não-ASCII, inclusive acentos. Para eles, a plataforma converte automaticamente cada caractere acentuado em uma referência numérica — uma representação que o provedor reconverte no caractere original ao processar a nota. O acento aparece corretamente no documento final; a conversão é transparente para você.
Tabela de substituições recomendadas
Se o seu sistema gera texto a partir de editores (Word, Google Docs, navegadores) ou de conteúdo colado por usuários, estes são os caracteres que aparecem com mais frequência e que você deve substituir na origem:
| Caractere | Nome | Code point | Substitua por |
|---|---|---|---|
– | Travessão curto (en dash) | U+2013 | - |
— | Travessão longo (em dash) | U+2014 | - |
' ' | Aspas simples curvas | U+2018 U+2019 | ' |
" " | Aspas duplas curvas | U+201C U+201D | " |
… | Reticências | U+2026 | ... |
• | Marcador de lista | U+2022 | - |
→ | Seta | U+2192 | -> |
™ ® | Marca registrada | U+2122 U+00AE | (TM) / (R) — ® é Latin-1 e passa |
| (invisível) | Word joiner | U+2060 | remover |
| (invisível) | Espaço de largura zero | U+200B | remover |
| (invisível) | Marca de ordenação | U+200E U+200F | remover |
| 😀 🙏 | Emoji | acima de U+FFFF | remover |
U+2060, U+200B e similares entram no texto por cópia e colagem e não aparecem na tela. Uma descrição visualmente idêntica a outra que funcionou pode ser recusada por causa deles. Por isso as mensagens de erro sempre trazem o code point.
Como diagnosticar uma rejeição
| Sintoma | Camada | O que fazer |
|---|---|---|
HTTP 400 com "não esta em UTF-8" | API | Converta o corpo da requisição para UTF-8 |
HTTP 400 citando U+XXXX e inválido em XML 1.0 | API | Remova o caractere indicado do campo indicado |
Nota recusada com [E1002] | Provedor | Substitua os caracteres listados pelos equivalentes Latin-1 e reenvie |
Nota recusada por erro de schema sem campo identificado (ex.: [1001] XML não compatível com Schema) | Provedor | Verifique quebras de linha e caracteres fora de Latin-1 nos campos de texto |
| Texto saiu sem acento no documento | Plataforma | Comportamento esperado para o provedor do município |
| Campo não apareceu no documento | Plataforma | O conteúdo era só de caracteres sem equivalente e foi omitido |
Use o code point da mensagem. Em JavaScript: texto.codePointAt(i).toString(16). Em C#: char.ConvertToUtf32(texto, i).ToString("X4"). Em Python: hex(ord(c)).
Boas práticas para a integração
- Normalize na origem. Aplique a tabela de substituições antes de enviar, não depois de uma recusa. O conjunto Latin-1 funciona em todos os provedores.
- Trate o texto colado por usuários. É de onde vêm aspas curvas, travessões e invisíveis.
- Não dependa de quebras de linha. Só a discriminação do serviço as aceita, e apenas em parte dos municípios. Use separadores como
-ou|para estruturar o texto. - Leia o webhook de falha. As mensagens nomeiam campo, caractere e code point — são suficientes para automatizar a correção.
- Valide o tamanho separadamente. A normalização de caracteres não trunca; o limite de cada campo é validado por conta própria.