TF Fiscal
Documentation

Authentication

Credential model, the three-header MD5 signature specification, known signing vectors, gateway error codes and reference implementations in curl, Java, Node.js, Python and C#.

All calls to the TF Fiscal Open API (/openapi/**) are authenticated per request with an MD5 signature scheme. There is no session or OAuth token exchange: each request is independently signed with your application secret (app_secret).

Credential model

CredentialPurpose
App KeyPublic identifier of the application (returned by the echo endpoint)
App SecretCalling credential; sent as the token header and used to compute sign
  • The App Secret is displayed only once, at application creation or at secret rotation. It cannot be retrieved later.
  • Rotation: if the secret leaks, request a rotation. The old secret becomes invalid immediately, so plan a switch-over window with your operations before rotating.
  • Subscription: the platform subscribes your application to the endpoints you need. An endpoint outside the subscription answers HTTP 403 with code 10009005, even with a valid signature.
  • Keep the secret server-side only. Never embed it in mobile apps, front-end code or public repositories.

Request headers

Every request must carry three headers:

HeaderValueNotes
tokenapp_secretApplication credential
timestampUnix timestamp in secondsMust be within ±300 seconds of server time (replay protection)
signRequest signatureAlgorithm below; 32 hex characters, lowercase

Signature algorithm

text
sign = MD5( token + path + body + timestamp ) -> lowercase hex

Concatenation rules (fixed order, plain string concatenation, no separators):

ElementRule
tokenThe app_secret, verbatim
pathRequest path including the /openapi prefix, excluding the query string, without scheme or host. Path variables (empresaId, nfeId, cteId, dceId, chave, cnpj, cpf, nascimento) are part of the path and are signed
bodyRaw request body with every CR (\r) and LF (\n) removed. GET and DELETE requests without a body use the empty string; multipart requests (certificate upload) use the empty string
timestampThe exact same string sent in the timestamp header

Body rules in detail:

  • JSON bodies: the bytes you send must be byte-for-byte identical to the string you signed. Serialize once, then use that same string both for signing and as the request body; never serialize twice. This also applies to a DELETE that carries a body (CT-e and DC-e cancellation with a reason).
  • XML bodies (the XML verification endpoint): the XML participates in the signature after stripping every CR and LF, while the request body itself is sent unchanged. A pretty-printed XML file therefore signs as a single line.
  • No body (GET, DELETE) and multipart: concatenate the empty string "", not "null", "{}" or any placeholder.
  • Optional headers such as forceRevalidate or Language are not part of the signature.

Signing examples

Fixed values you can recompute to verify your implementation before making a live call.

text
appSecret = "sk_live_9f8e7d6c5b4a"
timestamp = "1786843552"
GET path = "/openapi/v2/empresas/1934811222334455/nf-e/NFe-000014553" body = ""
sign = "24a450c3ca24d01700c534700c1b2343"
POST path = "/openapi/v2/empresas/1934811222334455/nf-e" body = {"id":"NFe-000014553"}
sign = "667b8127e211ff8c058f9640a9af672f"
GET path = "/openapi/v3/consultas/cpf/40710536828/09011997" body = ""
sign = "84a877ec34052db54cf41bb736f7c585"

For the XML and chave verification endpoints the same rule applies:

text
POST path = "/openapi/v3/consultas/nf-e/xml"
body = xml.replace("\r", "").replace("\n", "")
sign = md5Hex(appSecret + path + body + timestamp)
GET path = "/openapi/v3/consultas/nf-e/35260764962869000108550990001366171195929648"
body = ""
sign = md5Hex(appSecret + path + "" + timestamp)

HTTP status semantics

HTTP statusMeaningBody shape
200Request accepted by the endpoint; inspect the response body for the business resultEndpoint-specific
400 / 404Business or request-level error raised by the endpointEndpoint-specific error shape
401Authentication failed: missing headers, invalid timestamp, unknown token or signature mismatchPlatform envelope
403Authorization failed: application disabled or not effective, integrator disabled, or endpoint not subscribedPlatform envelope
429Rate limit exceededPlatform envelope

Authentication-layer errors are produced by the platform gateway before the request reaches the endpoint and always use the platform envelope:

json
{ "success": false, "errorType": 1, "code": 10009003, "message": "Signature error" }
HTTPcodeMeaningAction
40110009000Missing signature headers (token / sign / timestamp)Send all three headers on every request
40110009001Timestamp invalid or clock skew beyond ±300 sSync with NTP; regenerate per request
40110009002Invalid tokenCheck the app_secret; update it after a rotation
40110009003Signature mismatchSee 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 a subscription for the endpoint
42910009006Rate limit exceededRetry with exponential backoff (start at 1 s, double up to 30 s, add jitter)

401 and 403 are configuration errors: retrying without a fix is pointless and may trip rate limits. The business error shapes returned by the endpoints themselves are described in General conventions.

Verify signing with echo

POST /openapi/demo/echo is the recommended first call of every integration: it validates the whole signing pipeline and returns the identity of the calling application. Unlike the standard endpoints, echo responds with the platform envelope. Cross-check once with a POST carrying a body and once with a GET signing the empty body before moving on. Request, response and error reference: Echo test.

bash
HOST="https://api.v2.tffiscal.com"
APP_SECRET="<APP_SECRET>"
API_PATH="/openapi/demo/echo"
BODY='{"message":"hello tffiscal"}'
TIMESTAMP=$(date +%s)
SIGN=$(printf '%s%s%s%s' \
"$APP_SECRET" "$API_PATH" "$(printf '%s' "$BODY" | tr -d '\r\n')" "$TIMESTAMP" \
| md5sum | awk '{print $1}')
curl -sS -X POST "$HOST$API_PATH" \
-H "Content-Type: application/json" \
-H "token: $APP_SECRET" \
-H "timestamp: $TIMESTAMP" \
-H "sign: $SIGN" \
-d "$BODY"
json
{
"success": true,
"message": "OK",
"data": {
"echo": "hello tffiscal",
"appKey": "tfapp_0123456789abcdef",
"appName": "My Integration",
"serverTime": "2026-07-18T16:33:54.450Z"
}
}

serverTime is the server clock in UTC; compare it with your own clock to rule out timestamp drift.

Reference implementations

curl (bash), POST with a JSON body

bash
HOST="https://api.v2.tffiscal.com"
APP_SECRET="<APP_SECRET>"
API_PATH="/openapi/v2/empresas/1934811222334455/nf-e"
BODY='{"id":"NFe-000014553","ambienteEmissao":"Homologacao"}'
TIMESTAMP=$(date +%s)
SIGN=$(printf '%s%s%s%s' \
"$APP_SECRET" "$API_PATH" "$(printf '%s' "$BODY" | tr -d '\r\n')" "$TIMESTAMP" \
| md5sum | awk '{print $1}')
curl -sS -X POST "$HOST$API_PATH" \
-H "Content-Type: application/json" \
-H "token: $APP_SECRET" \
-H "timestamp: $TIMESTAMP" \
-H "sign: $SIGN" \
-d "$BODY"

curl (bash), GET with an empty body

bash
API_PATH="/openapi/v2/empresas/1934811222334455/nf-e/NFe-000014553"
TIMESTAMP=$(date +%s)
# body is the empty string: token + path + timestamp
SIGN=$(printf '%s%s%s' "$APP_SECRET" "$API_PATH" "$TIMESTAMP" | md5sum | awk '{print $1}')
curl -sS -X GET "$HOST$API_PATH" \
-H "token: $APP_SECRET" \
-H "timestamp: $TIMESTAMP" \
-H "sign: $SIGN"

Java

java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
public final class TffiscalSigner {
/**
* Computes the request signature.
*
* @param appSecret application secret (also sent as the token header)
* @param path request path including the /openapi prefix, without query string
* @param body raw request body exactly as it will be sent; null or "" for GET / DELETE / multipart
* @param timestamp unix time in seconds, same value as the timestamp header
* @return lowercase hex MD5 signature for the sign header
*/
public static String sign(String appSecret, String path, String body, long timestamp) {
String normalizedBody = body == null ? "" : body.replace("\r", "").replace("\n", "");
String payload = appSecret + path + normalizedBody + timestamp;
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] digest = md5.digest(payload.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder(digest.length * 2);
for (byte b : digest) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (Exception e) {
throw new IllegalStateException("MD5 unavailable", e);
}
}
}
java
String appSecret = "<APP_SECRET>";
String path = "/openapi/v2/empresas/1934811222334455/nf-e";
String body = "{\"id\":\"NFe-000014553\",\"ambienteEmissao\":\"Homologacao\"}";
long timestamp = System.currentTimeMillis() / 1000;
String sign = TffiscalSigner.sign(appSecret, path, body, timestamp);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.v2.tffiscal.com" + path))
.header("Content-Type", "application/json")
.header("token", appSecret)
.header("timestamp", String.valueOf(timestamp))
.header("sign", sign)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();

Node.js

javascript
const crypto = require('node:crypto');
/**
* Computes the request signature.
* @param {string} appSecret application secret (also sent as the token header)
* @param {string} path request path including the /openapi prefix, without query string
* @param {string} body raw request body exactly as it will be sent; '' for GET / DELETE / multipart
* @param {number} timestamp unix time in seconds
* @returns {string} lowercase hex MD5 for the sign header
*/
function sign(appSecret, path, body, timestamp) {
const normalizedBody = (body || '').replace(/[\r\n]/g, '');
return crypto
.createHash('md5')
.update(appSecret + path + normalizedBody + timestamp, 'utf8')
.digest('hex');
}
async function queryNfe(empresaId, nfeId) {
const host = 'https://api.v2.tffiscal.com';
const appSecret = '<APP_SECRET>';
const path = `/openapi/v2/empresas/${empresaId}/nf-e/${nfeId}`;
const timestamp = Math.floor(Date.now() / 1000);
const response = await fetch(host + path, {
method: 'GET',
headers: {
token: appSecret,
timestamp: String(timestamp),
sign: sign(appSecret, path, '', timestamp)
}
});
console.log(await response.json());
}
queryNfe('1934811222334455', 'NFe-000014553');

Python

python
import hashlib
def sign(app_secret: str, path: str, body: str, timestamp: str) -> str:
"""body is '' for GET / DELETE / multipart requests."""
normalized = (body or "").replace("\r", "").replace("\n", "")
return hashlib.md5((app_secret + path + normalized + timestamp).encode("utf-8")).hexdigest()

C#

csharp
var raw = appSecret + path + (body ?? "").Replace("\r", "").Replace("\n", "") + timestamp;
var sign = Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(raw))).ToLowerInvariant();

Troubleshooting

Signature mismatch (401, code 10009003)?

Check, in order of frequency:

  1. On POST, the JSON that was signed differs from the bytes actually sent (serialized twice, field order or whitespace changed).
  2. GET / DELETE / multipart did not use the empty string "" as the body ("null", an empty object or placeholder text was used instead).
  3. path missing the /openapi prefix or a path-variable segment (for the CPF lookup, both the CPF and the date of birth must be signed), or including the query string.
  4. sign sent in uppercase (must be lowercase hex).
  5. The timestamp used in concatenation differs from the header (regenerated between the two).
  6. CR/LF not stripped from the body before concatenation (typical with XML files).
  7. Body bytes re-encoded (hash the exact UTF-8 bytes sent on the wire).

Recompute the fixed values in Signing examples first; once your local implementation matches, move on to inspecting the live request parameters.

Clock skew (401, code 10009001)?

Your server clock differs from ours by more than 300 seconds. Use NTP, and never cache or reuse timestamps across requests. The serverTime returned by the echo endpoint shows the platform clock.