TF Fiscal
Documentation

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):

TierWhat it doesWhen
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 integrityReturned 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 recordsRuns 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

text
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)
validationStatusMeaning
VALIDATEDSEFAZ confirms the invoice is authentic and effective (authorized, no cancellation, protocol matches)
REJECTEDVerification failed: a Tier-1 blocking error, or SEFAZ reports the invoice is cancelled / denied / voided / not found / protocol mismatch. reason is provided
VALIDATION_ERRORVerification could not complete (SEFAZ throttling or query failure after retries). Not a judgment on the invoice itself; resubmit later

Onboarding

  1. Application credential: apply for an application and receive an app_secret. It is displayed only once; store it securely. If leaked, request rotation.
  2. API subscription: the platform enables your application for POST /openapi/v3/consultas/nf-e/xml and GET /openapi/v3/consultas/nf-e/{chave}.
  3. 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 with event_type=webhook.verify; your endpoint must return 2xx for the save to succeed (returning 200 without processing is fine for the test event).

Endpoints

EndpointPurpose
POST /openapi/v3/consultas/nf-e/xmlSubmit 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

HeaderDescription
X-Tffiscal-Eventinvoice.verify.completed
X-Tffiscal-Event-IdEvent id, the idempotency key: unchanged across retries; deduplicate on this
X-Tffiscal-Delivery-IdDelivery id, unique per attempt
X-Tffiscal-TimestampUnix seconds, regenerated on every attempt
X-Tffiscal-Signaturehex( HMAC-SHA256( secret, timestamp + "." + body ) ), lowercase; secret = your app_secret

Payload

json
{
"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:

FieldTypePresenceDescription
versionstringalwaysPayload schema version, currently 1.0
event_idstringalwaysEvent id, the idempotency key; identical across retries
event_typestringalwaysinvoice.verify.completed
occurred_atstringalwaysEvent time, ISO-8601 UTC
dataobjectalwaysVerdict body, below

data fields:

FieldTypePresenceDescription
chaveAcessostringalways44-digit access key of the verified invoice; join key back to your submission
validationStatusstringalwaysFinal verdict: VALIDATED / REJECTED / VALIDATION_ERROR (see Verification lifecycle)
statusstringalwaysSEFAZ fiscal status: Autorizada / Cancelada / Denegada / Inutilizada / NaoEncontrada / Desconhecida
cStatstring | nullnullableRaw SEFAZ return code (e.g. 100 = authorized, 101 = cancelled); null when SEFAZ was not reached
xMotivostring | nullnullableRaw SEFAZ return message (Portuguese, verbatim)
protocoloobject | nullnullableProtocol object (numero, digestValue), as on the XML verification response; from the official record
dataAutorizacaostring | nullnullableSEFAZ authorization time, ISO-8601 UTC
eventos[]arrayalways (may be empty)Fiscal events registered against the invoice (cancellation, correction letters); empty when none
verifiedAtstringalwaysWhen Tier-2 verification completed, ISO-8601 UTC
reasonstringonly on failurePresent only for REJECTED / VALIDATION_ERROR; stable English text explaining the verdict

Note: webhook payloads carry only language-independent enum values; there are no *Description fields here. Presentation text is up to the receiver.

Receiver requirements

  1. 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).
  2. Return 2xx within 10 seconds. Anything else, including timeouts, counts as a failed delivery.
  3. 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).
  4. Deduplicate on event_id (retries and multi-target fan-out share the same event_id).

Verdict handling

validationStatusTerminal?Action
VALIDATEDYesSafe to proceed (release goods, settle, etc.)
REJECTEDYesDo not proceed; reason explains the SEFAZ verdict (cancelled / denied / voided / not found / protocol mismatch)
VALIDATION_ERRORNoPlatform-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: true on 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 REJECTED has 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:

json
{ "code": 10015004, "message": "Invoice not found" }
FieldTypeDescription
codeintegerPlatform error code
messagestringHuman-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:

json
{ "success": false, "errorType": 1, "code": 10009003, "message": "Signature error" }
FieldTypeDescription
successbooleanfalse for every error
errorTypeintegerError class: 1 API error, 2 SEFAZ rejection, 3 system fault, 4 field validation failure
codeintegerPlatform error code
messagestringHuman-readable explanation, localized
dataobject | nullNot 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

HTTPcodeMeaningAction
40110009000Missing signature headers (token / sign / timestamp)Fix the client: send all three headers on every request
40110009001Timestamp invalid or clock skew beyond ±300 sSync your clock (NTP); regenerate timestamp per request, never reuse
40110009002Invalid tokenCheck the app_secret; if rotated, update your configuration
40110009003Signature mismatchRe-derive the signature; see the checklist in Troubleshooting
40310009004Application disabledContact the platform
40310009015Application not effective (pending approval or rejected)Wait for approval / contact the platform
40310009014Integrator account disabledContact the platform
40310009005API not subscribedRequest subscription for the endpoint being called
42910009006Rate limit exceededBack 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_MALFORMED to XML_VERSION_UNSUPPORTED (HTTP 200, inside validation.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

HTTPScenarioBody shape
200Request accepted, including failed validation (check the validation block)Bare data
400Request itself invalid (empty / oversized / DTD / encoding / malformed chave / not found)Bare {code, message}
401Authentication failed (token / sign / timestamp)Platform envelope
403Application disabled / not effective / integrator disabled / not subscribedPlatform envelope
429Rate limit exceededPlatform envelope
5xxPlatform-side faultRetry 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

  1. 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).
  2. POST /openapi/v3/consultas/nf-e/xml with a genuine authorized nfeProc: HTTP 200, all five validation checks true, validationStatus=PENDING_SEFAZ.
  3. Receive the invoice.verify.completed webhook: signature verifies, deduplicated by event_id, verdict VALIDATED.
  4. 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.
  5. Negative cases: submit a tampered XML (SIGNATURE_INVALID); submit an XML without protNFe (warning only, still accepted).
  6. Idempotency: resubmit the same chave and get the reused verdict; submit with forceRevalidate: true and get a re-verification with a new verdict via webhook.
  7. 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:

  1. CR/LF not stripped from the body before concatenation;
  2. GET request concatenated "null" instead of the empty string for the body;
  3. path missing the /openapi prefix, or including the query string;
  4. sign sent in uppercase (must be lowercase hex);
  5. the timestamp value used in concatenation differs from the header (regenerated between the two);
  6. 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.