Pular para o conteúdo
No ar em produção, em modo shadow: o FingerTrackID recomenda, o seu sistema decide.Status →

Orientações de integração

Esta página complementa a entrega da implementação com as regras operacionais que importam quando sua integração está rodando: o que enviar, como continuar independente do FingerTrackID, como julgar o período em modo shadow e como testar sem sandbox.

Seu backend envia toda chamada assinada para a origem da API de avaliação de produção:

https://api.fingertrackid.com

Por exemplo, POST https://api.fingertrackid.com/v1/assessments. Configure a origem exatamente como está escrita: https://, host em letras minúsculas, sem barra final e sem caminho. O console mostra o mesmo valor, com um botão de copiar, como Endereço da API de risco na etapa Credencial servidor a servidor do Início, assim que sua credencial existir. O arquivo OpenAPI para download mantém um servidor de exemplo histórico; não use esse.

Integrações novas enviam a requisição v1.1 (event-assessment-request.v1.1) e nomeiam cada momento avaliado com a sua própria etiqueta: login para um login, e uma etiqueta para cada outro momento que você integrar (os modelos de jornada sugerem dez). Os exemplos desta página usam o v1.1.

1. Envie uma avaliação para todo login concluído

Seção intitulada “1. Envie uma avaliação para todo login concluído”

Envie uma avaliação com a etiqueta login (no v1: LOGIN, action: "LOGIN_ATTEMPT") toda vez que seu backend concluir um login, incluindo o MFA exigido, independentemente do que o navegador tenha produzido. Faça o mesmo para todo outro momento que você integrar. Pular eventos torna impossível analisar o período em modo shadow, porque os que faltam são exatamente os casos que você não consegue ver. Cada login traz o subject_ref e o account_ref do usuário autenticado, construídos como pseudônimos com chave estáveis (referências de pessoa e de conta): sem account_ref, os itens de conta do console ficam não avaliados.

A evidência do navegador para esse login precisa ser atual: no perfil gerenciado de produção, o contexto do evento precisa ser usado dentro da janela de evidência da sua aplicação depois da coleta: 60 segundos para aplicações provisionadas antes da introdução da janela de 300 segundos (outubro de 2026), 300 segundos para as provisionadas desde então. No navegador, colete antes do envio (no primeiro foco do formulário de login), mantenha essa coleta atual dentro da janela e envie o contexto com a requisição que seu backend avalia: a etapa de MFA, quando seu login tiver uma (o ciclo de vida do SDK).

event_context_token é um campo obrigatório que aceita null; o schema da requisição (e o equivalente v1) define null como “nenhuma coleta fornecida”. Envie null sempre que o navegador não conseguir produzir um token: SDK não carregado, bloqueado ou com falha, página numa origem que não está registrada, um timeout, um token cujo expiresAt deixa tempo de menos para armazenar a avaliação, ou um login que nunca passou pelo seu código do navegador. Nunca invente, reutilize nem decodifique um token.

Sem evidência do navegador, o servidor armazena e retorna uma avaliação normal, e não um erro. No perfil atual de produção, um login sem contexto volta como 201 (ou 200 numa nova tentativa da mesma operação) com:

Campo Valor
assessment_status INCONCLUSIVE
trust_score null (sem nota; não é 0, não é 100)
recommendation_basis INSUFFICIENT_EVIDENCE
recommended_action MONITOR, o fallback de toda avaliação sem nota: toda etiqueta v1.1, e LOGIN, WITHDRAWAL e TRADE no v1
signals Os 13 indicadores, todos NOT_EVALUATED
limitation_codes O motivo de a evidência faltar, por exemplo COLLECTOR_NOT_LOADED
fallback_policy_version O fallback que produziu a recomendação

Um trecho sintético dessa resposta:

INCONCLUSIVE — sem contexto do navegador (trecho)
{
"assessment_id": "01996f2a-0000-7000-8000-000000000401",
"tag": "login",
"assessment_status": "INCONCLUSIVE",
"retryable": false,
"trust_score": null,
"evidence_coverage": 0,
"limitation_codes": [
"COLLECTOR_NOT_LOADED",
"REQUIRED_EVIDENCE_MISSING"
],
"recommended_action": "MONITOR",
"recommendation_basis": "INSUFFICIENT_EVIDENCE",
"fallback_policy_version": "production-observation-fallback-v2"
}

Um login concluído com o login do Google ou com outro provedor OAuth/OIDC continua sendo um login (etiqueta login; no v1, LOGIN): avalie-o depois que seu backend tiver aceitado o resultado do provedor e concluído o MFA próprio dele, se houver.

  • subject_ref e account_ref são os seus próprios pseudônimos da pessoa e da conta, os mesmos que você usa nos logins com senha. Não envie o identificador de sujeito do provedor nem o e-mail.
  • Num fluxo com redirecionamento, a página do navegador que poderia criar um contexto navega para outro endereço antes de o seu callback rodar no servidor, e o SDK guarda o estado dele só na memória da página. Colete antes do redirecionamento ou numa página depois do callback, como descrito em login com Google, OAuth e outros redirecionamentos. Se nenhuma requisição sua levar um contexto ao backend depois que a autenticação for concluída, envie event_context_token: null.
  • Nunca coloque um token no state do OAuth, numa URL de redirecionamento ou numa query string. Se o seu fluxo termina com uma requisição da sua própria página para o seu backend (uma credencial one-tap enviada com fetch, por exemplo), essa requisição pode levar o token como qualquer outra requisição de negócio.

2. Use fail-open e torne visível o erro de configuração

Seção intitulada “2. Use fail-open e torne visível o erro de configuração”

O login dos seus usuários nunca pode depender do FingerTrackID. Ao mesmo tempo, uma integração que falha em silêncio parece exatamente uma que funciona. As duas regras valem juntas:

  • Nunca bloqueie a ação de negócio por causa da integração. Toda falha do FingerTrackID — timeout, erro de rede, 429, 503, um erro lançado pelo assinador, uma feature flag desligada — termina no caminho normal do seu produto.
  • Nunca falhe em silêncio. Um erro de configuração precisa gerar uma entrada de log clara e pesquisável e, de preferência, um alerta do seu lado.

Confira a configuração uma vez, quando seu backend inicia, e registre um erro claro se ela estiver errada, em vez de pular avaliações em silêncio:

  • A origem da API é exatamente https://api.fingertrackid.com.
  • O runtime tem crypto.randomUUIDv7(): Node.js 24.16.0 ou posterior na série 24 (ou 26.1.0 ou posterior), como explica o wrapper abaixo.
  • key_id é um UUIDv7 em letras minúsculas.
  • O segredo pode ser decodificado: o decodeSecret(text, 'hex') do assinador de referência lança FTID_SECRET_ENCODING para um texto que não seja o valor hexadecimal de 64 caracteres, e FTID_SECRET quando a chave decodificada é curta demais.
  • Se a sua própria feature flag desliga a integração, registre esse estado uma vez na inicialização, para que uma integração desligada seja uma decisão visível, e não um mistério.

Nunca registre em log o segredo, o token, cookies nem o corpo da requisição.

Resultado Significado Nível de log Login
201 / 200 Avaliação armazenada info, com assessment_id e correlation_id Continue
401 authentication-failed, 403 forbidden, 400 validation-failed, 415 Sua configuração ou seu código está errado; uma nova tentativa não resolve error, alerta Continue
400 event-context-invalid O token expirou ou já tinha sido usado antes de a avaliação ser armazenada; nada foi armazenado warn; error se for frequente (colete mais perto da ação) Continue; avalie mais uma vez como uma nova operação com null
401 timestamp-out-of-window, 401 request-replayed Problema no relógio ou no gerador de request_id warn; error se for repetido Continue
409 binding-conflict Um mapeamento de referência mudou: um account_ref chegou com um subject_ref diferente do da primeira vez. Nada foi armazenado, e uma nova tentativa não resolve error, alerta Continue
409 idempotency-conflict Os mesmos IDs de operação chegaram com conteúdo diferente; nada novo foi armazenado error Continue; corrija o código que reutiliza IDs de operação
429, 503, erro de rede, timeout, FTID_INDETERMINATE Indisponível ou estado desconhecido warn Continue

As regras de nova tentativa estão em idempotência e nova tentativa; cada tipo é descrito em erros.

O FingerTrackID não publica um SLA de latência de ponta a ponta. Em produção, em 15/10/2026, numa aplicação e com uma amostra pequena (não é um SLA), o getEventContext sobre uma coleta aceita levou 0,15 s na mediana e 0,17 s no p95, enquanto uma coleta iniciada no clique levou 2,6 s na mediana, 3,9 s no p95 e no máximo 4,4 s da abertura da sessão até o fim da avaliação (medido em produção): prepare na primeira intenção, não no clique. Estes são os orçamentos documentados para planejar o seu próprio timeout:

Onde Orçamento Fonte
Navegador, SDK 0.2.x e 0.3.0 Abertura da sessão 3000 ms por tentativa, coleta 4000 ms, contexto do evento 5000 ms por ação, incluindo a renovação; o seu próprio teto no envio (o waitMs do helper) limita quanto o usuário espera Tempo e latência
Evidência do navegador e token 60 s ou 300 s, por aplicação, a partir de quando a coleta chega ao FingerTrackID; armazene a avaliação antes do expiresAt do token A janela de evidência
Servidor, v1.1 (toda etiqueta) deadline_ms 5000 ms por padrão, 250–5000 ms permitidos Jornadas e etiquetas
Servidor, v1 LOGIN deadline_ms 5000 ms por padrão, 250–5000 ms permitidos Jornadas e etiquetas
Servidor, v1 TRADE deadline_ms 750 ms por padrão, 250–750 ms permitidos Jornadas e etiquetas
Servidor, v1 WITHDRAWAL deadline_ms 1000 ms por padrão, 500–1000 ms permitidos Jornadas e etiquetas
Seu cliente HTTP O orçamento do servidor mais a sua latência de rede: acima de cerca de 6 s para um corpo LOGIN ou v1.1 com null; o exemplo de referência usa 6500 ms Autenticação

deadline_ms é um orçamento interno que começa a contar quando a requisição chega ao serviço; não é um SLA. Desde 14/10/2026, o padrão de LOGIN e do v1.1 é 5000 ms (era 1000 ms), então um timeout de cliente que não pode cortar um corpo com null precisa passar de cerca de 6 s. Um corpo que assina deadline_ms explicitamente pode enviar qualquer valor até 5000, ou null; um corpo já assinado com 750 ou 1000 continua válido e mantém exatamente esse orçamento. Escolha um timeout HTTP que o seu login comporte e, quando ele esgotar, continue o login e, se quiser o resultado, reconcilie depois com o mesmo corpo (veja timeout sem resposta).

Reconciliar com o mesmo corpo retorna a avaliação armazenada (200) sempre que a primeira tentativa foi armazenada, mesmo depois que o token expirou. Se ela não foi armazenada e o corpo traz um token, o mesmo corpo só tem sucesso antes do expiresAt desse token; depois dele, retorna 400 event-context-invalid, e esses IDs de operação não podem mais ser concluídos. Nesse caso, avalie o evento como uma nova operação (novos external_event_id e idempotency_key) com event_context_token: null, que uma nova operação só aceita enquanto o occurred_at dela estiver dentro de ±300 s do relógio do servidor.

Um esboço para um backend Node.js (por exemplo, um route handler ou uma server action do Next.js), construído sobre o assinador de referência. log representa o seu próprio logger estruturado.

O assinador precisa de crypto.randomUUIDv7(), que o Node.js tem a partir da 24.16.0 na série 24 e a partir da 26.1.0; o Node.js 22 e o 25 não têm, e neles o assinador falha assim que é importado. Rode o backend no Node.js 24 (24.16.0 ou posterior; na Vercel, defina a versão do Node.js do projeto como 24.x) e confira process.version na sua validação de inicialização. O wrapper carrega o assinador com await import(...) dentro do try dele, então um runtime sem essa função se degrada para “sem avaliação” em vez de quebrar a rota de login.

const API_ORIGIN = 'https://api.fingertrackid.com'
// Above the 5000 ms default budget of a `null` LOGIN/v1.1 body plus the network.
const HTTP_TIMEOUT_MS = 6500
// Send the token only if it stays valid longer than this: your HTTP timeout plus an
// allowance for clock skew between your backend and FingerTrackID. Choose your own value, and
// keep the browser helper's minRemainingMs (15000 ms by default) above it plus the trip from
// the browser to this backend.
const CONTEXT_MARGIN_MS = HTTP_TIMEOUT_MS + 1000
const EVENT_CONTEXT_INVALID = 'https://fingertrackid.dev/problems/event-context-invalid'
// Problems that mean your configuration or integration code is wrong: logged as errors.
const CONFIG_PROBLEMS = new Set([
'https://fingertrackid.dev/problems/authentication-failed',
'https://fingertrackid.dev/problems/forbidden',
'https://fingertrackid.dev/problems/validation-failed',
'https://fingertrackid.dev/problems/unsupported-media-type',
'https://fingertrackid.dev/problems/binding-conflict',
'https://fingertrackid.dev/problems/idempotency-conflict',
])
// Signer codes that mean the configuration itself is wrong.
const CONFIG_CODES = new Set([
'FTID_API_ORIGIN',
'FTID_KEY_ID',
'FTID_SECRET',
'FTID_SECRET_ENCODING',
'FTID_TIMEOUT',
])
// Returns the assessment or null. It never throws, so the login cannot depend on it.
// `request` is the first send of a new operation, with the browser's token or null.
// `contextExpiresAt` is the expiresAt the browser sent with the token (untrusted: it only
// decides whether to drop the token). `newOperationIds()` returns a fresh
// { external_event_id, idempotency_key } and records it with your operation.
// `isAssessment(body)` is your validator for the response schema of the profile you send
// (event-assessment.v1.1 for a v1.1 request, event-assessment.v1 for v1), for example compiled
// once at startup with a JSON Schema library; it returns true only for a valid body.
export async function assessLoginFailOpen({
enabled,
secret,
keyId,
request,
contextExpiresAt,
newOperationIds,
isAssessment,
log,
}) {
if (!enabled) return null // The disabled state was logged once at startup.
try {
// Inside the try: without crypto.randomUUIDv7 the import fails and the login continues.
const { postAttempt, prepareAttempt } = await import('./ftid-hmac-v1.mjs')
const send = async (operation) => {
const body = Buffer.from(JSON.stringify(operation), 'utf8') // Keep for reconciliation.
const attempt = prepareAttempt({ secret, keyId, body })
const result = await postAttempt(API_ORIGIN, attempt, HTTP_TIMEOUT_MS)
return { result, parsed: JSON.parse(result.body) }
}
let operation = request
const remainingMs = Date.parse(contextExpiresAt) - Date.now()
if (operation.event_context_token !== null && !(remainingMs > CONTEXT_MARGIN_MS)) {
operation = { ...operation, event_context_token: null } // Expired or about to expire.
}
let { result, parsed } = await send(operation)
if (
result.status === 400 &&
parsed.type === EVENT_CONTEXT_INVALID &&
operation.event_context_token !== null
) {
// Expired or already used: nothing was stored, but these operation IDs stay bound to
// that token. Assess once more as a new operation, without a context.
log.warn('ftid.event_context_invalid', { correlation_id: result.correlationId })
operation = { ...operation, ...newOperationIds(), event_context_token: null }
;({ result, parsed } = await send(operation))
}
if (result.status === 201 || result.status === 200) {
if (!isAssessment(parsed)) {
// A 2xx whose body breaks the response schema is not an assessment: continue the login
// and keep the stored request bytes to reconcile later.
log.warn('ftid.assessment_invalid_body', {
http_status: result.status,
correlation_id: result.correlationId,
})
return null
}
log.info('ftid.assessment', {
assessment_id: parsed.assessment_id,
status: parsed.assessment_status,
recommended_action: parsed.recommended_action,
correlation_id: result.correlationId,
})
return parsed
}
log[CONFIG_PROBLEMS.has(parsed.type) ? 'error' : 'warn']('ftid.assessment_problem', {
http_status: result.status,
type: parsed.type,
correlation_id: result.correlationId,
})
return null
} catch (error) {
// FTID_INDETERMINATE (timeout, network, unreadable response), a signer error, a failed
// signer import or a parse failure: continue the login. Log only the fixed signer code,
// never an arbitrary message.
const message = error instanceof Error ? error.message : ''
const code = /^FTID_[A-Z_]+$/.test(message) ? message : 'UNEXPECTED'
log[CONFIG_CODES.has(code) ? 'error' : 'warn']('ftid.assessment_unavailable', { code })
return null
}
}

A verificação do token só se aplica antes do primeiro envio de uma operação. Uma nova tentativa ou uma reconciliação posterior de uma operação já enviada reenvia os bytes armazenados dela sem alteração, token incluído.

O wrapper valida todo corpo 200/201 contra o schema de resposta do perfil que você enviou (v1.1 ou v1) antes de ler qualquer campo. Valide os corpos de Problem contra o schema do Problem da mesma forma antes de confiar em qualquer coisa além de type, e adapte a política de novas tentativas ao prazo do seu login.

Em SHADOW, o FingerTrackID avalia e recomenda; nada é imposto, e o seu produto continua tomando todas as decisões. O objetivo do período é comparar as recomendações com o que o seu produto decidiu e com o que depois se revelou verdadeiro.

  • Os seus próprios IDs de operação: external_event_id e idempotency_key.
  • assessment_id, assessment_status, trust_score, recommended_action, recommendation_basis, limitation_codes, evidence_coverage e os campos *_version, exatamente como recebidos.
  • A decisão que o seu produto de fato tomou, e por quê (uma regra sua, uma revisão manual, um fallback).
  • Se o evento teve um contexto do navegador (event_context_token era um token ou null).
  • Se o evento trouxe subject_ref e account_ref.

Envie o que aconteceu pela API de feedback, referenciando o assessment_id. Ela exige as permissões de credencial action:write e outcome:write.

  • Ação do cliente: a ação que o seu produto executou. Em shadow, quem decidiu foi a sua própria política, então o par usual é basis: "CLIENT_POLICY" com reason_code: "CLIENT_POLICY_RULE"; uma decisão humana é MANUAL_REVIEW; uma decisão tomada sob o seu fallback porque o resultado não estava disponível a tempo é FALLBACK com CLIENT_FALLBACK_APPLIED. Use ASSESSMENT só quando você de fato seguiu a recomendação.
  • Desfecho: evidência posterior, como LEGITIMATE ou FRAUD_CONFIRMED com a respectiva evidência, ou PENDING enquanto você ainda não sabe. Correções são acrescentadas; nunca sobrescrevem.

Cada recibo de ação do cliente traz recommendation_alignment (ALIGNED, NOT_ALIGNED ou NOT_COMPARABLE), derivado pelo FingerTrackID. Guarde os recibos: o console ainda não mostra a ação do cliente nem o desfecho num acesso.

  • Taxa de concordância: a proporção de ALIGNED entre as ações comparáveis. Em shadow, NOT_ALIGNED significa que a sua política e a recomendação divergem; não diz qual das duas está certa.
  • Contas sabidamente boas: as recomendações para contas em que você confia, como as da equipe interna ou de clientes antigos sem incidentes. STEP_UP, REVIEW ou BLOCK_RECOMMENDED frequentes entre as avaliações concluídas dessas contas são um sinal de falso positivo.
  • Casos sabidamente ruins: as recomendações nos eventos que você depois confirmou como fraude.
  • Cobertura: a proporção de COMPLETED em relação a INCONCLUSIVE, e os limitation_codes por trás delas. Analise separadamente as avaliações sem nota: a recomendação delas é o fallback fixo, não uma análise.

Não existe sandbox nem ambiente de teste: é em produção, em modo shadow, que você testa. Como nada é imposto, uma avaliação de teste não pode bloquear ninguém, mas ela é dado real de produção. Ela fica armazenada na sua empresa, aparece no console e pode entrar nas estimativas de uso.

  • Use somente as suas entradas de produção. A autoverificação da assinatura pública e os exemplos v1.1 sintéticos (e os exemplos v1) permitem testar a assinatura e o parsing sem nenhuma credencial.
  • Coloque a integração atrás da sua própria feature flag e ligue-a primeiro para contas internas ou de teste, depois para uma pequena parte dos logins, depois para todos. Mantenha a flag como o seu kill switch.
  • Somente as origens permitidas sob o domínio de coleta. A coleta pelo seu domínio de coleta é aceita a partir das origens permitidas da aplicação cujo host é esse domínio ou um dos subdomínios dele (HTTPS exato, sem caminho, porta nem curinga; até 10, listadas em Integração e saúde › Origens das páginas). Toda outra origem, inclusive hosts de desenvolvimento local e deploys de preview, é recusada sem aprovação de CORS, então o navegador esconde a resposta e o SDK retorna UNAVAILABLE com o motivo TRANSPORT_ERROR. O seu helper continua com null, e a avaliação no backend continua funcionando e retorna um resultado sem nota. Teste a coleta no navegador numa origem permitida. Uma origem removida para de coletar imediatamente (um navegador ainda pode manter em cache um preflight de CORS por pouco tempo).
  • Mantenha o segredo no servidor. Guarde-o somente no ambiente do lado do servidor do deploy que chama a API, nunca numa variável exposta ao navegador (no Next.js, nunca numa variável NEXT_PUBLIC_), e somente em ambientes a partir dos quais você aceite enviar dados de produção.

Dúvidas: contato@fingertrackid.com.