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
| Credencial | Finalidade |
|---|---|
| App Key | Identificador público da aplicação (devolvido pelo endpoint de echo) |
| App Secret | Credencial 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çalho | Valor | Observações |
|---|---|---|
token | app_secret | Credencial da aplicação |
timestamp | Timestamp Unix em segundos | Deve estar dentro de ±300 segundos do horário do servidor (proteção contra replay) |
sign | Assinatura da requisição | Algoritmo abaixo; 32 caracteres hexadecimais, minúsculos |
Algoritmo de assinatura
sign = MD5( token + path + body + timestamp ) -> lowercase hex
Regras de concatenação (ordem fixa, concatenação simples de strings, sem separadores):
| Elemento | Regra |
|---|---|
token | O app_secret, sem alteração |
path | Caminho 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 |
body | Corpo 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 |
timestamp | Exatamente 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
DELETEcom 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
forceRevalidateouLanguagenã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.
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:
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 HTTP | Significado | Formato do corpo |
|---|---|---|
| 200 | Requisição aceita pelo endpoint; inspecione o corpo da resposta para o resultado de negócio | Específico do endpoint |
| 400 / 404 | Erro de negócio ou de requisição gerado pelo endpoint | Formato de erro específico do endpoint |
| 401 | Falha de autenticação: cabeçalhos ausentes, timestamp inválido, token desconhecido ou assinatura divergente | Envelope da plataforma |
| 403 | Falha de autorização: aplicação desativada ou não vigente, integrador desativado ou endpoint não subscrito | Envelope da plataforma |
| 429 | Limite de taxa excedido | Envelope 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:
{ "success": false, "errorType": 1, "code": 10009003, "message": "Signature error" }
| HTTP | code | Significado | Ação |
|---|---|---|---|
| 401 | 10009000 | Cabeçalhos de assinatura ausentes (token / sign / timestamp) | Envie os três cabeçalhos em toda requisição |
| 401 | 10009001 | Timestamp inválido ou desvio de relógio acima de ±300 s | Sincronize com NTP; gere um novo por requisição |
| 401 | 10009002 | Token inválido | Verifique o app_secret; atualize-o após uma rotação |
| 401 | 10009003 | Assinatura divergente | Veja Solução de problemas |
| 403 | 10009004 | Aplicação desativada | Contate a plataforma |
| 403 | 10009015 | Aplicação não vigente (aguardando aprovação ou rejeitada) | Aguarde a aprovação / contate a plataforma |
| 403 | 10009014 | Conta do integrador desativada | Contate a plataforma |
| 403 | 10009005 | API não subscrita | Solicite a subscrição do endpoint |
| 429 | 10009006 | Limite de taxa excedido | Tente 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.
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"
{"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
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
API_PATH="/openapi/v2/empresas/1934811222334455/nf-e/NFe-000014553"TIMESTAMP=$(date +%s)# body is the empty string: token + path + timestampSIGN=$(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
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);}}}
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
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
import hashlibdef 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#
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:
- No POST, o JSON assinado difere dos bytes efetivamente enviados (serializado duas vezes, ordem dos campos ou espaços alterados).
- GET / DELETE / multipart não usou a string vazia
""como corpo (foi usado"null", um objeto vazio ou um texto de marcador). pathsem o prefixo/openapiou 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.signenviado em maiúsculas (deve ser hexadecimal minúsculo).- O
timestampusado na concatenação difere do cabeçalho (gerado de novo entre os dois). - CR/LF não removidos do corpo antes da concatenação (típico com arquivos XML).
- 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.
