TF Fiscal
Documentação

Autenticação

Modelo de credenciais, a especificação de assinatura MD5 com três cabeçalhos, vetores de assinatura conhecidos, códigos de erro do gateway e implementações de referência em curl, Java, Node.js, Python e C#.

Todas as chamadas à Open API da TF Fiscal (/openapi/**) são autenticadas por requisição com um esquema de assinatura MD5. Não há sessão nem troca de token OAuth: cada requisição é assinada de forma independente com o segredo da sua aplicação (app_secret).

Modelo de credenciais

CredencialFinalidade
App KeyIdentificador público da aplicação (devolvido pelo endpoint de echo)
App SecretCredencial de chamada; enviada no cabeçalho token e usada para calcular sign
  • O App Secret é exibido apenas uma vez, na criação da aplicação ou na rotação do segredo. Não pode ser recuperado depois.
  • Rotação: se o segredo vazar, solicite a rotação. O segredo antigo torna-se inválido imediatamente, então planeje com a operação uma janela de troca antes de rotacionar.
  • Subscrição: a plataforma subscreve a sua aplicação aos endpoints de que você precisa. Um endpoint fora da subscrição responde HTTP 403 com o código 10009005, mesmo com assinatura válida.
  • Mantenha o segredo apenas no servidor. Nunca o embuta em aplicativos móveis, código de front-end ou repositórios públicos.

Cabeçalhos da requisição

Toda requisição deve carregar três cabeçalhos:

CabeçalhoValorObservações
tokenapp_secretCredencial da aplicação
timestampTimestamp Unix em segundosDeve estar dentro de ±300 segundos do horário do servidor (proteção contra replay)
signAssinatura da requisiçãoAlgoritmo abaixo; 32 caracteres hexadecimais, minúsculos

Algoritmo de assinatura

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

Regras de concatenação (ordem fixa, concatenação simples de strings, sem separadores):

ElementoRegra
tokenO app_secret, sem alteração
pathCaminho da requisição incluindo o prefixo /openapi, excluindo a query string, sem esquema nem host. As variáveis de caminho (empresaId, nfeId, cteId, dceId, chave, cnpj, cpf, nascimento) fazem parte do caminho e são assinadas
bodyCorpo bruto da requisição com todo CR (\r) e LF (\n) removidos. Requisições GET e DELETE sem corpo usam a string vazia; requisições multipart (upload de certificado) usam a string vazia
timestampExatamente a mesma string enviada no cabeçalho timestamp

Regras do corpo em detalhe:

  • Corpos JSON: os bytes enviados devem ser idênticos byte a byte à string assinada. Serialize uma vez e use essa mesma string tanto para assinar quanto como corpo da requisição; nunca serialize duas vezes. Isso vale também para um DELETE com corpo (cancelamento de CT-e e DC-e com motivo).
  • Corpos XML (endpoint de verificação por XML): o XML participa da assinatura após a remoção de todo CR e LF, enquanto o corpo da requisição é enviado sem alteração. Um arquivo XML formatado com quebras de linha é assinado, portanto, como uma única linha.
  • Sem corpo (GET, DELETE) e multipart: concatene a string vazia "", não "null", "{}" nem qualquer marcador.
  • Cabeçalhos opcionais como forceRevalidate ou Language não fazem parte da assinatura.

Exemplos de assinatura

Valores fixos que você pode recalcular para validar a sua implementação antes de uma chamada real.

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"

Para os endpoints de verificação por XML e por chave vale a mesma regra:

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)

Semântica dos status HTTP

Status HTTPSignificadoFormato do corpo
200Requisição aceita pelo endpoint; inspecione o corpo da resposta para o resultado de negócioEspecífico do endpoint
400 / 404Erro de negócio ou de requisição gerado pelo endpointFormato de erro específico do endpoint
401Falha de autenticação: cabeçalhos ausentes, timestamp inválido, token desconhecido ou assinatura divergenteEnvelope da plataforma
403Falha de autorização: aplicação desativada ou não vigente, integrador desativado ou endpoint não subscritoEnvelope da plataforma
429Limite de taxa excedidoEnvelope da plataforma

Os erros da camada de autenticação são produzidos pelo gateway da plataforma antes de a requisição chegar ao endpoint e sempre usam o envelope da plataforma:

json
{ "success": false, "errorType": 1, "code": 10009003, "message": "Signature error" }
HTTPcodeSignificadoAção
40110009000Cabeçalhos de assinatura ausentes (token / sign / timestamp)Envie os três cabeçalhos em toda requisição
40110009001Timestamp inválido ou desvio de relógio acima de ±300 sSincronize com NTP; gere um novo por requisição
40110009002Token inválidoVerifique o app_secret; atualize-o após uma rotação
40110009003Assinatura divergenteVeja Solução de problemas
40310009004Aplicação desativadaContate a plataforma
40310009015Aplicação não vigente (aguardando aprovação ou rejeitada)Aguarde a aprovação / contate a plataforma
40310009014Conta do integrador desativadaContate a plataforma
40310009005API não subscritaSolicite a subscrição do endpoint
42910009006Limite de taxa excedidoTente de novo com backoff exponencial (comece em 1 s, dobre até 30 s, adicione jitter)

401 e 403 são erros de configuração: repetir sem corrigir é inútil e pode acionar o limite de taxa. Os formatos de erro de negócio devolvidos pelos próprios endpoints estão descritos em Convenções gerais.

Validar a assinatura com o echo

POST /openapi/demo/echo é a primeira chamada recomendada de toda integração: ele valida toda a cadeia de assinatura e devolve a identidade da aplicação chamadora. Diferente dos endpoints padrão, o echo responde com o envelope da plataforma. Confira uma vez com um POST com corpo e uma vez com um GET assinando o corpo vazio antes de prosseguir. Referência de requisição, resposta e erros: Echo de teste.

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 é o relógio do servidor em UTC; compare-o com o seu relógio para descartar desvio de timestamp.

Implementações de referência

curl (bash), POST com corpo JSON

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 com corpo vazio

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();

Solução de problemas

Assinatura divergente (401, código 10009003)?

Verifique, em ordem de frequência:

  1. No POST, o JSON assinado difere dos bytes efetivamente enviados (serializado duas vezes, ordem dos campos ou espaços alterados).
  2. GET / DELETE / multipart não usou a string vazia "" como corpo (foi usado "null", um objeto vazio ou um texto de marcador).
  3. path sem o prefixo /openapi ou sem um segmento de variável de caminho (na consulta de CPF, tanto o CPF quanto a data de nascimento devem ser assinados), ou incluindo a query string.
  4. sign enviado em maiúsculas (deve ser hexadecimal minúsculo).
  5. O timestamp usado na concatenação difere do cabeçalho (gerado de novo entre os dois).
  6. CR/LF não removidos do corpo antes da concatenação (típico com arquivos XML).
  7. Bytes do corpo recodificados (faça o hash exatamente dos bytes UTF-8 enviados na rede).

Recalcule primeiro os valores fixos em Exemplos de assinatura; quando a sua implementação local coincidir, passe a inspecionar os parâmetros da requisição real.

Desvio de relógio (401, código 10009001)?

O relógio do seu servidor difere do nosso em mais de 300 segundos. Use NTP e nunca armazene em cache nem reutilize timestamps entre requisições. O serverTime devolvido pelo endpoint de echo mostra o relógio da plataforma.