Special characters in NFS-e text fields
🇧🇷 Esta página também está disponível em português: Caracteres especiais nos campos de texto da NFS-e.
NFS-e free-text fields — service description, additional information, customer legal name and address — accept different character sets depending on the municipality that issues the invoice. The same text may be accepted in one city and rejected in another.
This page describes how the platform handles each case, so you know what to send and what to do when an invoice is rejected.
In one sentence
If all your text uses only letters, digits, common punctuation and Portuguese accents (the Latin-1 set, U+0000 to U+00FF), it is accepted by every provider. Problems come from characters that look harmless but fall outside that set: en dash –, curly quotes " ", ellipsis …, bullet •, emoji and invisible characters.
Accents are safe. Curly quotes, dashes and emoji are not.
The three validation layers
Text submitted for issuance goes through three independent checks, in this order:
| Order | Layer | What it does | How it fails |
|---|---|---|---|
| 1 | API | Rejects what cannot be represented in an electronic fiscal document | HTTP 400 at request time |
| 2 | Platform | Normalizes text to the format the municipal provider requires | — (silent adjustment, when unambiguous) |
| 3 | Municipal provider | Validates against the city hall schema | Invoice rejected, message delivered by webhook |
Layer 1 — API validation
Happens at request time, before any processing.
The body must be UTF-8
JSON must be sent encoded in UTF-8. A body in any other encoding is rejected with 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"
}
The response follows the application/problem+json shape, the same as the API's other validation errors. Keep the traceId: it is how support locates the request.
Text originating in legacy systems that use Windows-1252 and is sent without conversion. Accented characters become invalid bytes along the way.
Characters impossible in XML are rejected
An NFS-e is transmitted to the city hall as XML. Some characters simply do not exist in XML 1.0 — the main one is the null character U+0000, but the rule covers every control character the standard forbids.
When the body contains one of them, the response is HTTP 400 naming the property and the 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"
}
Up to 10 distinct occurrences are reported at once, so you can fix everything in a single pass instead of discovering one field per attempt.
$Each key in errors is the property whose value held the character. When the character does not belong to any property value — it sits in a JSON key or in a bare array element — the key comes back as $, the API's convention for a body error with no property path. Very long property names are truncated in the response.
U+XXXXSeveral of these characters are invisible. If the message printed the character itself, you would read "contains the character " and see no character at all. The code point is what lets you locate it in your payload.
API and provider messages are issued in Portuguese, since they are also surfaced in the Brazilian tax authority's channels. The examples on this page reproduce them verbatim so you can match them exactly in your integration.
What gets adjusted instead of rejected
Characters that can be represented in XML but need handling — such as &, < and > — are escaped automatically. Nothing is removed from your text at this stage.
Layer 2 — Platform normalization
When the municipal provider requires a specific format and the conversion is unambiguous, the platform adjusts the text on its own. The guiding rule is never to change the meaning of what you sent.
Transliteration to Latin-1 (national standard)
In municipalities on the NFS-e national standard, text fields follow a schema type that accepts only U+0020 to U+00FF and allows no line breaks or tabs. Before submission, the platform applies:
| Input | Becomes | Note |
|---|---|---|
Line break, tab, CR | space | Consecutive spaces collapse into one |
– — − • (dashes, bullet) | - | Plain hyphen |
' ' ′ (curly single quotes) | ' | Straight apostrophe |
" " ″ (curly double quotes) | " | Straight quotes |
… (ellipsis) | ... | Three dots |
| Emoji, ideographs, symbols with no equivalent | space | No conversion is possible |
Accents, ç, ª, º | unchanged | Latin-1 passes through intact |
Text is trimmed at both ends. Nothing is truncated at this stage — length limits are validated separately, per field.
Under the national standard, the service description field uses a more permissive schema type and does accept line breaks. The normalization above does not apply to it. Additional information, by contrast, follows the restrictive rule.
If a field contains only characters with no equivalent (for example, emoji only), the normalization result is empty — and the field is omitted from the invoice, because the schema requires at least one character. The text will not appear in the issued document.
Accent removal
Some municipal providers do not accept accented characters at all. For those, the platform strips diacritics automatically before submission: São Paulo becomes Sao Paulo, informações becomes informacoes. The base letter is preserved; nothing is discarded.
Fixed-width fields
A few municipalities receive the invoice as positional text rather than XML. In those cases, on top of accent removal, each field is padded or cut to the exact width the layout requires — longer text is truncated at the field boundary.
Layer 3 — Municipal provider validation
At this point the invoice has already left the platform. The rejection comes from the city hall and reaches you through the webhook and through invoice lookup.
Providers that reject anything outside Latin-1
Some providers reject the whole invoice when any text field contains a character above U+00FF, rather than accepting a conversion. The most relevant case is the city of São Paulo (NFS-e Paulistana).
In that scenario the platform does not transliterate the text: the rejection happens before submission to the city hall, with a message naming the field, the character and the 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.
Up to 10 distinct occurrences are listed per rejection.
This pre-submission check depends on the municipality and provider configuration. Where it is not in effect, the invoice is submitted and the city hall itself rejects it, with a generic schema error — typically [1001] XML não compatível com Schema — that names neither the field nor the character. The fix is the same: look for characters outside Latin-1 in the text fields, starting with the invisible ones.
A rejection is something you resolve in seconds by resubmitting adjusted text. Text that has been altered inside an invoice already authorized by the city hall cannot be undone — the fiscal document would permanently carry content different from what you sent. That is why, when the conversion is not unambiguous, the platform prefers to return the error.
The rule applies to all text fields, not just the description. The ones most affected in practice:
- Service description
- Customer legal name
- Street, number, complement and district of the address
- Customer and intermediary e-mail
Providers that require plain ASCII
A small group of providers rejects any non-ASCII character, accents included. For those, the platform automatically converts each accented character into a numeric character reference — a representation the provider converts back into the original character when processing the invoice. The accent appears correctly in the final document; the conversion is transparent to you.
Recommended replacements
If your system generates text from editors (Word, Google Docs, browsers) or from content pasted by users, these are the characters that show up most often and that you should replace at the source:
| Character | Name | Code point | Replace with |
|---|---|---|---|
– | En dash | U+2013 | - |
— | Em dash | U+2014 | - |
' ' | Curly single quotes | U+2018 U+2019 | ' |
" " | Curly double quotes | U+201C U+201D | " |
… | Ellipsis | U+2026 | ... |
• | Bullet | U+2022 | - |
→ | Arrow | U+2192 | -> |
™ ® | Trademark marks | U+2122 U+00AE | (TM) / ® is Latin-1 and passes |
| (invisible) | Word joiner | U+2060 | remove |
| (invisible) | Zero-width space | U+200B | remove |
| (invisible) | Directional marks | U+200E U+200F | remove |
| 😀 🙏 | Emoji | above U+FFFF | remove |
U+2060, U+200B and similar enter your text through copy and paste and never show on screen. A description that looks identical to one that worked can be rejected because of them. That is why error messages always carry the code point.
Diagnosing a rejection
| Symptom | Layer | What to do |
|---|---|---|
HTTP 400 saying "não esta em UTF-8" | API | Convert the request body to UTF-8 |
HTTP 400 citing U+XXXX and inválido em XML 1.0 | API | Remove the reported character from the reported field |
Invoice rejected with [E1002] | Provider | Replace the listed characters with Latin-1 equivalents and resubmit |
Invoice rejected with a schema error and no field named (e.g. [1001] XML não compatível com Schema) | Provider | Check for line breaks and non-Latin-1 characters in text fields |
| Text came out without accents in the document | Platform | Expected behavior for that municipality's provider |
| A field did not appear in the document | Platform | Its content was made only of characters with no equivalent and was omitted |
Use the code point from the message. In JavaScript: text.codePointAt(i).toString(16). In C#: char.ConvertToUtf32(text, i).ToString("X4"). In Python: hex(ord(c)).
Integration best practices
- Normalize at the source. Apply the replacement table before sending, not after a rejection. The Latin-1 set works with every provider.
- Sanitize user-pasted text. That is where curly quotes, dashes and invisible characters come from.
- Do not rely on line breaks. Only the service description accepts them, and only in some municipalities. Use separators such as
-or|to structure text. - Read the failure webhook. Messages name the field, the character and the code point — enough to automate the correction.
- Validate length separately. Character normalization does not truncate; each field's limit is enforced on its own.