Verification API
The platform performs two-tier verification of third-party NF-e documents (model 55):
| Tier | What it does | When |
|---|---|---|
| Tier 1 (synchronous) | Local validation of the XML: syntax, NF-e 4.00 XSD layout, digital signature and certificate ownership, 44-digit access key (chave) structure and consistency with document fields, authorization protocol integrity | Returned immediately in the API response |
| Tier 2 (asynchronous) | Real-time authenticity check against SEFAZ (the state tax authority): authorization status, cancellation events, protocol number and digest compared with official records | Runs after Tier 1 passes; the verdict is delivered via webhook |
Two endpoints:
| Endpoint | Purpose |
|---|---|
POST /openapi/v3/consultas/nf-e/xml | Submit the raw NF-e XML; returns the Tier-1 verdict and the parsed invoice, starts Tier 2 |
GET /openapi/v3/consultas/nf-e/{chave} | Pure lookup by access key; returns invoice data and carries no verification verdict |
Both require the three signature headers — see
Authentication & Signing. Responses are bare (no platform
envelope); request-level errors are a bare {code, message} object — see
General Conventions.
Verification lifecycle
submit XML ──► Tier 1
├─ blocking error ──────────────► REJECTED (terminal)
└─ pass ──► PENDING_SEFAZ ──► VALIDATING (Tier 2)
├──► VALIDATED (terminal)
├──► REJECTED (terminal, with reason)
└──► VALIDATION_ERROR (retryable, with reason)| validationStatus | Meaning |
|---|---|
VALIDATED | SEFAZ confirms the invoice is authentic and effective (authorized, no cancellation, protocol matches) |
REJECTED | Verification failed: a Tier-1 blocking error, or SEFAZ reports the invoice is cancelled / denied / voided / not found / protocol mismatch. reason is provided |
VALIDATION_ERROR | Verification could not complete (SEFAZ throttling or query failure after retries). Not a judgment on the invoice itself — resubmit later |
Onboarding
- Application credential — an
app_secret, displayed only once. - API subscription — the platform enables your application for both endpoints above.
- Webhook endpoint — configure your application-level webhook URL and subscribe to
the event
invoice.verify.completed, the only push channel for Tier-2 verdicts. When the URL is saved, the platform immediately sends a test delivery withevent_type=webhook.verify; your endpoint must return 2xx for the save to succeed (returning 200 without processing is fine for the test event). See Webhooks.
XML verification
Submits the raw NF-e XML. Returns the Tier-1 verdict and the parsed invoice data synchronously; on Tier-1 pass, Tier-2 SEFAZ verification starts automatically (verdict via webhook).
Request
POST /openapi/v3/consultas/nf-e/xml
Content-Type: application/xml (or text/xml)
token / timestamp / sign see Authentication & Signing
forceRevalidate: true (optional header, NOT signed, default false)
<request body: raw NF-e XML>Body requirements:
- Preferred: the full nfeProc authorized document (including the
protNFeprotocol node). A bareNFewithout the protocol node is also accepted — it produces a warning but is processed normally. - Hard limits: max 1 MB, UTF-8 only, DTD forbidden (any
<!DOCTYPEis rejected outright).
Signing: the body participates in the signature after stripping every CR and LF,
while the request body itself is sent unchanged:
sign = md5Hex(app_secret + "/openapi/v3/consultas/nf-e/xml" + xmlWithoutCrLf + timestamp).
Complete worked example:
POST /openapi/v3/consultas/nf-e/xml HTTP/1.1
Host: api.v2.tffiscal.com
Content-Type: application/xml
token: sk_live_9f8e7d6c5b4a
timestamp: 1784906308
sign: 7c1b2a3d4e5f60718293a4b5c6d7e8f9
Language: en
<?xml version="1.0" encoding="UTF-8"?><nfeProc xmlns="http://www.portalfiscal.inf.br/nfe" versao="4.00"><NFe>...</NFe><protNFe versao="4.00">...</protNFe></nfeProc>Equivalent curl:
APP_SECRET="sk_live_9f8e7d6c5b4a"
PATH_SIGNED="/openapi/v3/consultas/nf-e/xml"
BODY=$(tr -d '\r\n' < invoice.xml)
TS=$(date +%s)
SIGN=$(printf '%s' "${APP_SECRET}${PATH_SIGNED}${BODY}${TS}" | md5sum | cut -d' ' -f1)
curl -X POST "https://api.v2.tffiscal.com${PATH_SIGNED}" \
-H "Content-Type: application/xml" \
-H "token: ${APP_SECRET}" \
-H "timestamp: ${TS}" \
-H "sign: ${SIGN}" \
-H "Language: en" \
--data-binary @invoice.xmlSuccess response (HTTP 200)
HTTP 200 means the request was accepted — not that the invoice passed. A well-formed XML that fails validation still returns 200; the verdict is in the
validationblock. Never branch on the HTTP status to decide validity.
{
"tipo": "NF-e",
"modelo": "55",
"status": "Autorizada",
"statusDescription": "Invoice authorized",
"ambienteEmissao": "Producao",
"numero": "1006122",
"serie": "2",
"dataEmissao": "2024-11-22T11:20:05-03:00",
"chaveAcesso": "35260764962869000108550990001366171195929648",
"emitente": {
"nome": "EMPRESA EXEMPLO LTDA",
"cpfCnpj": "64962869000108",
"inscricaoEstadual": "123456789012",
"telefone": "1133334444",
"endereco": {
"uf": "SP", "cidade": "SAO PAULO", "logradouro": "RUA EXEMPLO",
"numero": "100", "complemento": null, "bairro": "CENTRO", "cep": "01001000"
}
},
"destinatario": { "...": "same structure as emitente, plus email" },
"itens": [
{
"cfop": "5102",
"codigo": "SKU-001",
"descricao": "PRODUTO EXEMPLO",
"quantidade": 2.0,
"valorUnitario": 50.00,
"descontos": 0.00,
"valorTotal": 100.00
}
],
"dataAutorizacao": "2024-11-22T11:20:31-03:00",
"protocolo": { "numero": "135240001234567", "digestValue": "oAEE...HwY=" },
"valorTotal": 100.00,
"validation": {
"fileAccessible": true,
"xmlWellFormed": true,
"schemaValid": true,
"signatureValid": true,
"accessKeyValid": true,
"protocolPresent": true,
"protocolValid": true,
"xmlVersion": "4.00",
"validationStatus": "PENDING_SEFAZ",
"validationStatusDescription": "Tier-1 validation passed, awaiting tax authority verification",
"errors": [],
"warnings": []
}
}Field reference (top level):
| Field | Type | Presence | Description |
|---|---|---|---|
tipo | string | always | Document type: NF-e / NFC-e |
modelo | string | always | Document model: 55 / 65 |
status | string | always | SEFAZ fiscal status: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida |
statusDescription | string | always | Localized description of status |
ambienteEmissao | string | always | Issuing environment: Producao / Homologacao |
numero | string | always | Invoice number nNF, no leading zeros |
serie | string | always | Series number |
dataEmissao | string | always | Issue time dhEmi — ISO-8601 with UTC offset, verbatim from the document |
chaveAcesso | string | always | 44-digit access key |
emitente | Party | always | Issuer — see Party object |
destinatario | Party | always | Recipient — see Party object |
itens[] | array of Line item | always | Invoice line items — see Line item object |
dataAutorizacao | string | null | nullable | SEFAZ authorization time dhRecbto (ISO-8601); null when the submitted XML carries no protocol node |
protocolo | Protocol | null | nullable | Authorization protocol — see Protocol object; null when no protocol node |
valorTotal | number | always | Invoice net total vNF (after discounts), 2 decimal places |
validation | Validation | always (XML API only) | Tier-1 verdict block — see The validation block |
Party object (emitente / destinatario):
| Field | Type | Presence | Description |
|---|---|---|---|
nome | string | always | Legal name xNome |
email | string | null | nullable | E-mail address; populated for the recipient only, when present on the document |
cpfCnpj | string | always | CPF (11 digits) or CNPJ (14 digits), digits only, leading zeros preserved |
inscricaoMunicipal | string | null | nullable | Municipal registration IM |
inscricaoEstadual | string | null | nullable | State tax registration IE; typically null for consumer recipients |
telefone | string | null | nullable | Phone number fone, digits only |
endereco | Address | always | See Address object |
Address object (endereco):
| Field | Type | Presence | Description |
|---|---|---|---|
uf | string | always | State code, 2 letters (e.g. SP) |
cidade | string | always | City name xMun |
logradouro | string | always | Street xLgr |
numero | string | always | Street number nro |
complemento | string | null | nullable | Additional info xCpl (apartment, suite, …) |
bairro | string | always | District xBairro |
cep | string | always | Postal code CEP, 8 digits, no hyphen |
Line item object (itens[] element):
| Field | Type | Presence | Description |
|---|---|---|---|
cfop | string | always | CFOP operation code, 4 digits |
codigo | string | always | Seller product code cProd (SKU) |
descricao | string | always | Product description xProd |
quantidade | number | always | Quantity qCom, up to 4 decimal places |
valorUnitario | number | always | Unit price vUnCom, up to 10 decimal places |
descontos | number | always | Line discount vDesc; 0.00 when absent on the document |
valorTotal | number | always | Gross line amount vProd = quantidade × valorUnitario, before discount |
Protocol object (protocolo):
| Field | Type | Presence | Description |
|---|---|---|---|
numero | string | always | Authorization protocol number nProt, 15 digits |
digestValue | string | always | Authorization digest digVal, Base64 |
Amount semantics: line-level
itens[].valorTotalis the gross line amount (vProd, before discount); root-levelvalorTotalis the net invoice total (vNF, after discount). The difference equals the total discount.
The validation block
| Field | Type | Presence | Description |
|---|---|---|---|
fileAccessible | boolean | always | XML content received and readable (always true for direct body submission; compatibility field) |
xmlWellFormed | boolean | always | XML syntax is valid |
schemaValid | boolean | always | Conforms to the official NF-e 4.00 XSD layout |
signatureValid | boolean | always | Issuer digital signature valid: digest, signature value, certificate validity period and CNPJ match |
accessKeyValid | boolean | always | 44-digit key structure and check digit valid, and consistent with document fields |
protocolPresent | boolean | always | A protNFe authorization node was present |
protocolValid | boolean | null | nullable | Protocol structurally consistent with the invoice (local check; authenticity is decided by Tier 2). null when no protocol node |
xmlVersion | string | always | Layout version from infNFe@versao (e.g. 4.00) |
validationStatus | string | always | Synchronous response carries PENDING_SEFAZ (passed, Tier 2 queued) or REJECTED (blocking, terminal). Full enum incl. asynchronous states: VALIDATING / VALIDATED / VALIDATION_ERROR |
validationStatusDescription | string | always | Localized description of validationStatus |
errors[] | array of Issue | always (may be empty) | Blocking findings — any entry means REJECTED |
warnings[] | array of Issue | always (may be empty) | Non-blocking findings (e.g. missing protocol node) |
Issue object (errors[] / warnings[] element):
| Field | Type | Presence | Description |
|---|---|---|---|
code | string | always | Stable enum code — see Tier-1 validation errors. Branch on this, never on message |
field | string | null | nullable | Locates the offending element (e.g. Signature, chNFe) |
message | string | always | Human-readable explanation, localized |
Chave lookup
Returns invoice data for a 44-digit access key. Pure query endpoint: the response
carries no validation block; verification verdicts are delivered exclusively via
webhook.
Request
GET /openapi/v3/consultas/nf-e/{chave}
token / timestamp / sign (GET → body is the empty string; the chave is part of the signed path)Complete worked example. Given:
app_secret = sk_live_9f8e7d6c5b4a
chave = 35260764962869000108550990001366171195929648
timestamp = 1784906308Step 1 — build the signing string:
sign_input = "sk_live_9f8e7d6c5b4a"
+ "/openapi/v3/consultas/nf-e/35260764962869000108550990001366171195929648"
+ ""
+ "1784906308"
sign = md5Hex(sign_input) e.g. → 3f2a9c1d8b7e6f5a4c3d2e1f0a9b8c7dStep 2 — send the request:
GET /openapi/v3/consultas/nf-e/35260764962869000108550990001366171195929648 HTTP/1.1
Host: api.v2.tffiscal.com
token: sk_live_9f8e7d6c5b4a
timestamp: 1784906308
sign: 3f2a9c1d8b7e6f5a4c3d2e1f0a9b8c7d
Language: enEquivalent curl:
APP_SECRET="sk_live_9f8e7d6c5b4a"
CHAVE="35260764962869000108550990001366171195929648"
TS=$(date +%s)
SIGN=$(printf '%s' "${APP_SECRET}/openapi/v3/consultas/nf-e/${CHAVE}${TS}" | md5sum | cut -d' ' -f1)
curl -X GET \
"https://api.v2.tffiscal.com/openapi/v3/consultas/nf-e/${CHAVE}" \
-H "token: ${APP_SECRET}" \
-H "timestamp: ${TS}" \
-H "sign: ${SIGN}" \
-H "Language: en"Data sources and outcomes
The platform resolves the chave through the following tiers; the first hit is returned:
| Case | Result |
|---|---|
| Previously submitted for verification (platform retains the record) | Full invoice data |
| Invoice issued through this platform | Full invoice data parsed from the authorized XML |
| Unknown chave | Full authorized data fetched from the official data source, per platform policy |
| No source hit | HTTP 400 — { "code": 10015004, "message": "Invoice not found" } |
| Malformed chave / bad check digit | HTTP 400 — { "code": 10015104, ... }; no lookup is attempted |
Success response (HTTP 200)
{
"tipo": "NF-e",
"modelo": "55",
"status": "Autorizada",
"statusDescription": "Invoice authorized",
"ambienteEmissao": "Producao",
"numero": "136617",
"serie": "99",
"dataEmissao": "2026-07-23T11:20:05-03:00",
"chaveAcesso": "35260764962869000108550990001366171195929648",
"emitente": {
"nome": "EMPRESA EXEMPLO LTDA",
"email": null,
"cpfCnpj": "64962869000108",
"inscricaoMunicipal": null,
"inscricaoEstadual": "123456789012",
"telefone": "1133334444",
"endereco": {
"uf": "SP",
"cidade": "SAO PAULO",
"logradouro": "RUA EXEMPLO",
"numero": "100",
"complemento": null,
"bairro": "CENTRO",
"cep": "01001000"
}
},
"destinatario": {
"nome": "COMPRADOR EXEMPLO",
"email": "buyer@example.com",
"cpfCnpj": "39053344705",
"inscricaoMunicipal": null,
"inscricaoEstadual": null,
"telefone": null,
"endereco": {
"uf": "RJ",
"cidade": "RIO DE JANEIRO",
"logradouro": "AV RIO BRANCO",
"numero": "156",
"complemento": "APT 501",
"bairro": "CENTRO",
"cep": "20040030"
}
},
"itens": [
{
"cfop": "6102",
"codigo": "SKU-001",
"descricao": "PRODUTO EXEMPLO",
"quantidade": 2.0,
"valorUnitario": 50.00,
"descontos": 0.00,
"valorTotal": 100.00
}
],
"dataAutorizacao": "2026-07-23T11:20:31-03:00",
"protocolo": {
"numero": "135260001234567",
"digestValue": "oAEEuC3tGmb2W7ZJxWkNVWuBHwY="
},
"valorTotal": 100.00
}The top-level fields are the same as for XML verification, minus the validation block;
chaveAcesso echoes the path variable. The nested Party / Address / Line item / Protocol
objects are defined once under XML verification and are identical
here.
Error responses
Unknown chave (no source hit):
{ "code": 10015004, "message": "Invoice not found" }Malformed chave (wrong length / character / check digit — rejected locally, no lookup attempted):
{ "code": 10015104, "message": "Access key structure or check digit invalid" }Authentication failures (401 / 403 / 429) use the platform envelope — see Authentication & Signing.
Receiving the final verdict
When Tier-2 SEFAZ verification settles, the platform POSTs the invoice.verify.completed
event to your configured webhook URL. This is the only push channel for final
verdicts; the chave lookup can serve as a polling fallback. Headers, payload and
receiver requirements are documented in
Webhooks — Verification verdict event.
Verdict handling:
| validationStatus | Terminal? | Action |
|---|---|---|
VALIDATED | Yes | Safe to proceed (release goods, settle, etc.) |
REJECTED | Yes | Do not proceed; reason explains the SEFAZ verdict (cancelled / denied / voided / not found / protocol mismatch) |
VALIDATION_ERROR | No | Platform-side verification failure, not an invoice judgment; resubmit later with forceRevalidate: true |
Idempotency and re-verification
- XML verification is idempotent on the chave: resubmitting while a verification is in progress returns the current progress; terminal verdicts are reused for 24 hours (no duplicate verification cost).
- Forced re-verification: send header
forceRevalidate: trueon the XML endpoint (not part of the signature). The chave lookup has a fixed GET shape with no re-verification channel — to force a re-check, resubmit through the XML endpoint. - A Tier-1 blocking
REJECTEDhas no Tier-2 record; re-verification requires resubmitting the XML.
Error reference
Request-level errors — XML endpoint (HTTP 400, bare shape)
The request never enters validation.
| code | Meaning | Action |
|---|---|---|
| 10015000 | Empty request body | Send the XML in the body |
| 10015001 | Body exceeds 1 MB | An authentic single NF-e never exceeds this; check that you are not wrapping or double-encoding |
| 10015002 | DTD detected (<!DOCTYPE) | Strip DTDs; they are rejected as an XXE safeguard |
| 10015003 | Encoding is not UTF-8 | Convert to UTF-8 before submitting |
Request-level errors — chave lookup (HTTP 400, bare shape)
| code | Meaning | Action |
|---|---|---|
| 10015104 | Malformed chave (length / characters / check digit) | Validate locally first: 44 digits; the last digit is a mod-11 check digit |
| 10015004 | Invoice not found in any source | The chave is unknown to the platform and the official data source; verify the chave with its issuer |
Tier-1 validation errors
Returned with HTTP 200 inside validation.errors[]. These are not transport errors:
the request succeeded, the document failed. All are blocking (terminal REJECTED) except
PROTOCOL_MISSING, which is a warning.
| errors[].code | Numeric | Meaning |
|---|---|---|
XML_MALFORMED | 10015100 | XML syntax invalid |
XSD_INVALID | 10015101 | Does not conform to the NF-e 4.00 XSD layout |
SIGNATURE_INVALID | 10015102 | Digital signature verification failed (content tampered, or certificate expired at signing time) |
SIGNATURE_CERT_MISMATCH | 10015103 | Signing certificate CNPJ does not match the issuer |
ACCESS_KEY_INVALID | 10015104 | chave structure / check digit invalid |
ACCESS_KEY_MISMATCH | 10015105 | chave segments do not match the document fields |
PROTOCOL_MISMATCH | 10015106 | Protocol block inconsistent with the document |
PROTOCOL_MISSING | 10015107 | No protocol node (warning, non-blocking) |
XML_VERSION_UNSUPPORTED | 10015108 | Layout version is not 4.00 |
HTTP status quick reference
| HTTP | Scenario | Body shape |
|---|---|---|
| 200 | Request accepted — including failed validation (check the validation block) | Bare data |
| 400 | Request itself invalid (empty / oversized / DTD / encoding / malformed chave / not found) | Bare {code, message} |
| 401 | Authentication failed (token / sign / timestamp) | Platform envelope |
| 403 | Application disabled / not effective / integrator disabled / not subscribed | Platform envelope |
| 429 | Rate limit exceeded | Platform envelope |
| 5xx | Platform-side fault | Retry with backoff; if persistent, contact the platform with the failing timestamp and path |
Integration checklist
- Obtain the
app_secret; cross-check your signing implementation against the sign-helper output (once for POST with an XML body, once for GET with an empty body). POST /openapi/v3/consultas/nf-e/xmlwith a genuine authorized nfeProc → HTTP 200, all five validation checks true,validationStatus=PENDING_SEFAZ.- Receive the
invoice.verify.completedwebhook: signature verifies, deduplicated byevent_id, verdictVALIDATED. - Chave lookup: a previously submitted chave → 200 with invoice data; a nonexistent chave → 400 + 10015004; a chave with a wrong check digit → 400 + 10015104.
- Negative cases: submit a tampered XML (
SIGNATURE_INVALID); submit an XML withoutprotNFe(warning only, still accepted). - Idempotency: resubmit the same chave → reused verdict; submit with
forceRevalidate: true→ re-verification, new verdict via webhook. - Failure paths: call with a wrong
sign→ 401 (code 10009003); call an unsubscribed endpoint → 403 (code 10009005).
Troubleshooting
HTTP 200 but the invoice is fake? Tier 1 only judges whether the XML is internally
consistent. Authenticity is decided by Tier 2 against SEFAZ and delivered via webhook —
gate your business action (release, settlement) on the webhook VALIDATED, never on the
synchronous response alone.
Is VALIDATION_ERROR a problem with the invoice? No. It means the platform’s
verification channel failed (e.g. SEFAZ throttling); the invoice itself was not judged.
Resubmit later with forceRevalidate: true.
Signature never matches (401, code 10009003)? The most frequent cause on this
endpoint is CR/LF not stripped from the XML before concatenation, followed by a GET that
concatenated "null" instead of the empty string. The full checklist is in
Authentication & Signing.