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.
A origem da API de produção
Seção intitulada “A origem da API de produção”Seu backend envia toda chamada assinada para a origem da API de avaliação de produção:
https://api.fingertrackid.comPor 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.
O perfil da requisição
Seção intitulada “O perfil da requisição”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).
Quando o navegador não tem contexto do evento
Seção intitulada “Quando o navegador não tem contexto do evento”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:
{ "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"}Logins sociais e OAuth
Seção intitulada “Logins sociais e OAuth”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_refeaccount_refsã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
statedo 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 comfetch, 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.
Valide a configuração na inicialização
Seção intitulada “Valide a configuração na inicialização”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çaFTID_SECRET_ENCODINGpara um texto que não seja o valor hexadecimal de 64 caracteres, eFTID_SECRETquando 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.
Classifique os resultados
Seção intitulada “Classifique os resultados”| 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.
Orçamento de tempo
Seção intitulada “Orçamento de tempo”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 wrapper fail-open mínimo
Seção intitulada “Um wrapper fail-open mínimo”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 + 1000const 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.
3. Como analisar o período em modo shadow
Seção intitulada “3. Como analisar o período em modo shadow”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.
Registre, para cada avaliação
Seção intitulada “Registre, para cada avaliação”- Os seus próprios IDs de operação:
external_event_ideidempotency_key. assessment_id,assessment_status,trust_score,recommended_action,recommendation_basis,limitation_codes,evidence_coveragee 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_tokenera um token ounull). - Se o evento trouxe
subject_refeaccount_ref.
Informe ações e desfechos
Seção intitulada “Informe ações e desfechos”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"comreason_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 éFALLBACKcomCLIENT_FALLBACK_APPLIED. UseASSESSMENTsó quando você de fato seguiu a recomendação. - Desfecho: evidência posterior, como
LEGITIMATEouFRAUD_CONFIRMEDcom a respectiva evidência, ouPENDINGenquanto 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.
O que observar
Seção intitulada “O que observar”- Taxa de concordância: a proporção de
ALIGNEDentre as ações comparáveis. Em shadow,NOT_ALIGNEDsignifica 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,REVIEWouBLOCK_RECOMMENDEDfrequentes 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
COMPLETEDem relação aINCONCLUSIVE, e oslimitation_codespor trás delas. Analise separadamente as avaliações sem nota: a recomendação delas é o fallback fixo, não uma análise.
4. Como testar antes da produção
Seção intitulada “4. Como testar antes da produção”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
UNAVAILABLEcom o motivoTRANSPORT_ERROR. O seu helper continua comnull, 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.