NF-e verification
Two-tier verification of third-party NF-e documents: XML verification, chave lookup, the final verdict delivered by webhook, idempotency and the error model.
Overview
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 (see Receiving the final verdict) |
A standalone chave lookup returns invoice data by access key. It is a pure query endpoint and carries no verification verdict.
Both endpoints require the three signature headers, see Authentication. Responses are bare objects (no platform envelope); request-level errors are a bare {code, message} object.
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: apply for an application and receive an
app_secret. It is displayed only once; store it securely. If leaked, request rotation. - API subscription: the platform enables your application for
POST /openapi/v3/consultas/nf-e/xmlandGET /openapi/v3/consultas/nf-e/{chave}. - Webhook endpoint: register your callback URL through Register webhook 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).
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 |
Receiving the final verdict
When Tier-2 SEFAZ verification settles, the platform POSTs to your registered webhook URL. This is the only push channel for final verdicts; the chave lookup can serve as a polling fallback.
Request headers
| Header | Description |
|---|---|
X-Tffiscal-Event | invoice.verify.completed |
X-Tffiscal-Event-Id | Event id, the idempotency key: unchanged across retries; deduplicate on this |
X-Tffiscal-Delivery-Id | Delivery id, unique per attempt |
X-Tffiscal-Timestamp | Unix seconds, regenerated on every attempt |
X-Tffiscal-Signature | hex( HMAC-SHA256( secret, timestamp + "." + body ) ), lowercase; secret = your app_secret |
Payload
{"version": "1.0","event_id": "1950000000000001","event_type": "invoice.verify.completed","occurred_at": "2026-07-23T17:16:23Z","data": {"chaveAcesso": "35260764962869000108550990001366171195929648","validationStatus": "VALIDATED","status": "Autorizada","cStat": "100","xMotivo": "Autorizado o uso da NF-e","protocolo": { "numero": "135262955451772", "digestValue": "oAEEuC3tGmb2W7ZJxWkNVWuBHwY=" },"dataAutorizacao": "2026-07-23T14:30:09Z","eventos": [],"verifiedAt": "2026-07-23T17:16:23Z"}}
Envelope fields:
| Field | Type | Presence | Description |
|---|---|---|---|
| version | string | always | Payload schema version, currently 1.0 |
| event_id | string | always | Event id, the idempotency key; identical across retries |
| event_type | string | always | invoice.verify.completed |
| occurred_at | string | always | Event time, ISO-8601 UTC |
| data | object | always | Verdict body, below |
data fields:
| Field | Type | Presence | Description |
|---|---|---|---|
| chaveAcesso | string | always | 44-digit access key of the verified invoice; join key back to your submission |
| validationStatus | string | always | Final verdict: VALIDATED / REJECTED / VALIDATION_ERROR (see Verification lifecycle) |
| status | string | always | SEFAZ fiscal status: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida |
| cStat | string | null | nullable | Raw SEFAZ return code (e.g. 100 = authorized, 101 = cancelled); null when SEFAZ was not reached |
| xMotivo | string | null | nullable | Raw SEFAZ return message (Portuguese, verbatim) |
| protocolo | object | null | nullable | Protocol object (numero, digestValue), as on the XML verification response; from the official record |
| dataAutorizacao | string | null | nullable | SEFAZ authorization time, ISO-8601 UTC |
| eventos[] | array | always (may be empty) | Fiscal events registered against the invoice (cancellation, correction letters); empty when none |
| verifiedAt | string | always | When Tier-2 verification completed, ISO-8601 UTC |
| reason | string | only on failure | Present only for REJECTED / VALIDATION_ERROR; stable English text explaining the verdict |
Note: webhook payloads carry only language-independent enum values; there are no
*Descriptionfields here. Presentation text is up to the receiver.
Receiver requirements
- Verify the signature: recompute
HMAC-SHA256(secret, timestamp + "." + rawBody)and compare with the header. Use the raw received bytes; do not deserialize and re-serialize first (field reordering breaks the signature). - Return 2xx within 10 seconds. Anything else, including timeouts, counts as a failed delivery.
- Failed deliveries are retried with backoff 1m / 5m / 30m / 2h / 6h (5 attempts), then parked in a dead-letter queue (manual re-push available on request).
- Deduplicate on
event_id(retries and multi-target fan-out share the same event_id).
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 the header
forceRevalidate: trueon the XML verification 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 model
Two error shapes
Business and request-level errors (HTTP 400) are returned as a bare object:
{ "code": 10015004, "message": "Invoice not found" }
| Field | Type | Description |
|---|---|---|
| code | integer | Platform error code |
| message | string | Human-readable explanation, localized (see Response localization) |
Authentication-layer errors (HTTP 401 / 403 / 429) are produced by the platform gateway before your request reaches the API, and use the platform envelope:
{ "success": false, "errorType": 1, "code": 10009003, "message": "Signature error" }
| Field | Type | Description |
|---|---|---|
| success | boolean | false for every error |
| errorType | integer | Error class: 1 API error, 2 SEFAZ rejection, 3 system fault, 4 field validation failure |
| code | integer | Platform error code |
| message | string | Human-readable explanation, localized |
| data | object | null | Not populated on errors |
Your error handler must accept both shapes. A robust rule: parse the body as JSON, read code from either shape, and treat success=false or HTTP >= 400 as failure.
Authentication and authorization errors
| HTTP | code | Meaning | Action |
|---|---|---|---|
| 401 | 10009000 | Missing signature headers (token / sign / timestamp) | Fix the client: send all three headers on every request |
| 401 | 10009001 | Timestamp invalid or clock skew beyond ±300 s | Sync your clock (NTP); regenerate timestamp per request, never reuse |
| 401 | 10009002 | Invalid token | Check the app_secret; if rotated, update your configuration |
| 401 | 10009003 | Signature mismatch | Re-derive the signature; see the checklist in Troubleshooting |
| 403 | 10009004 | Application disabled | Contact the platform |
| 403 | 10009015 | Application not effective (pending approval or rejected) | Wait for approval / contact the platform |
| 403 | 10009014 | Integrator account disabled | Contact the platform |
| 403 | 10009005 | API not subscribed | Request subscription for the endpoint being called |
| 429 | 10009006 | Rate limit exceeded | Back off and retry; smooth your call rate. Limits are enforced per application and per endpoint group |
Retry guidance: 401 and 403 are configuration errors; retrying without a fix is pointless and may trip rate limits. 429 is retryable with exponential backoff (start at 1 s, double up to 30 s, add jitter).
Request-level and validation errors
Documented on each endpoint page:
- XML verification: request-level errors 10015000 to 10015003 (HTTP 400, bare shape, the request never enters validation) and the Tier-1 validation catalog
XML_MALFORMEDtoXML_VERSION_UNSUPPORTED(HTTP 200, insidevalidation.errors[]). - Chave lookup: 10015104 (malformed chave) and 10015004 (invoice not found), HTTP 400, bare shape.
- Tier-2 outcomes are not HTTP errors; see Verdict handling.
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 |
Response localization
message and *Description fields follow the request language: the Language header (en / pt / es / zh) takes precedence, then Accept-Language (supports pt-BR and q-values). Without a language header, responses default to Portuguese (pt).
For programmatic decisions always use the language-independent enum fields (validationStatus, status, errors[].code); never match on description text.
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 returns 200 with invoice data; a nonexistent chave returns 400 + 10015004; a chave with a wrong check digit returns 400 + 10015104.
- Negative cases: submit a tampered XML (
SIGNATURE_INVALID); submit an XML without protNFe (warning only, still accepted). - Idempotency: resubmit the same chave and get the reused verdict; submit with
forceRevalidate: trueand get a re-verification with a new verdict via webhook. - Failure paths: call with a wrong
sign(401, code 10009003); call an unsubscribed endpoint (403, code 10009005).
Troubleshooting
Signature never matches (401, code 10009003)?
Check, in order of frequency:
- CR/LF not stripped from the body before concatenation;
- GET request concatenated
"null"instead of the empty string for the body; pathmissing the/openapiprefix, or including the query string;signsent in uppercase (must be lowercase hex);- the
timestampvalue used in concatenation differs from the header (regenerated between the two); - body bytes re-encoded (must hash the exact bytes sent on the wire, UTF-8).
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.
Webhook signature keeps failing?
The most common cause is deserializing the payload and re-serializing it before computing the HMAC, which changes field order or whitespace. Always hash the raw received bytes.
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.
Clock skew (401, code 10009001)?
Your server clock differs from ours by more than 300 seconds. Use NTP. Never cache or reuse timestamps across requests.
