Skip to content
Live in production, in shadow mode: FingerTrackID recommends, your system decides.Status →

Integration guidance

This page complements the implementation handoff with the operational rules that matter once your integration runs: what to send, how to stay independent of FingerTrackID, how to judge the shadow period and how to test without a sandbox.

Your backend sends every signed call to the production assessment API origin:

https://api.fingertrackid.com

For example, POST https://api.fingertrackid.com/v1/assessments. Configure the origin exactly as written: https://, lowercase host, no trailing slash and no path. The dash shows the same value, with a copy button, as Endereço da API de risco (risk API address) in the Credencial servidor a servidor step of Início, once your credential exists. The downloadable OpenAPI file keeps a historical example server; do not use that one.

New integrations send the v1.1 request (event-assessment-request.v1.1) and name each assessed moment with their own tag: login for a login, and one tag per other moment you integrate (the journey templates suggest ten). The examples on this page use v1.1.

1. Send an assessment for every completed login

Section titled “1. Send an assessment for every completed login”

Send one assessment with the tag login (in v1: LOGIN, action: "LOGIN_ATTEMPT") every time your backend completes a login, including the required MFA, whatever the browser produced. Do the same for every other moment you integrate. Skipping events makes the shadow period impossible to evaluate, because the missing ones are exactly the cases you cannot see. Each login carries the authenticated user’s subject_ref and account_ref, built as stable keyed pseudonyms (person and account references): without account_ref, the console’s account items stay unevaluated.

Browser evidence for that login has to be fresh: under the managed production profile the event context must be used within your application’s evidence window after the collection: 60 seconds for applications provisioned before the 300-second window was introduced (October 2026), 300 seconds for those provisioned since. In the browser, collect before the submit (at the first focus of the login form), keep that collection fresh inside the window and send the context with the request your backend assesses: the MFA step when your login has one (the SDK lifecycle).

event_context_token is a required field that accepts null; the request schema (and its v1 counterpart) defines null as “no collection supplied”. Send null whenever the browser could not produce a token: SDK not loaded, blocked or failed, page on an origin that is not registered, a timeout, a token whose expiresAt leaves too little time to store the assessment, or a login that never passed through your browser code. Never invent, reuse or decode a token.

Without browser evidence the server stores and returns a normal assessment rather than an error. Under the current production profile, a login without a context comes back as 201 (or 200 on a retry of the same operation) with:

Field Value
assessment_status INCONCLUSIVE
trust_score null (no score; not 0, not 100)
recommendation_basis INSUFFICIENT_EVIDENCE
recommended_action MONITOR, the fallback for every no-score assessment: every v1.1 tag, and LOGIN, WITHDRAWAL and TRADE in v1
signals All 13 indicators NOT_EVALUATED
limitation_codes The reason evidence is missing, for example COLLECTOR_NOT_LOADED
fallback_policy_version The fallback that produced the recommendation

A synthetic excerpt of that response:

INCONCLUSIVE — no browser context (excerpt)
{
"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"
}

A login completed through Google sign-in or another OAuth/OIDC provider is still a login (tag login; in v1, LOGIN): assess it once your backend has accepted the provider’s result and finished any MFA of its own.

  • subject_ref and account_ref are your own pseudonyms for the person and account, the same ones you use for password logins. Do not send the provider’s subject identifier or the email.
  • With a redirect flow, the browser page that could create a context navigates away before your callback runs on the server, and the SDK keeps its state only in page memory. Collect before the redirect or on a page after the callback, as described in login with Google, OAuth and other redirects. If no request of yours carries a context to the backend after authentication completes, send event_context_token: null.
  • Never place a token in the OAuth state, a redirect URL or a query string. If your flow finishes with a request from your own page to your backend (a one-tap credential posted with fetch, for example), that request can carry the token like any other business request.

2. Fail open, and make misconfiguration visible

Section titled “2. Fail open, and make misconfiguration visible”

Your users’ login must never depend on FingerTrackID. At the same time, an integration that fails silently looks exactly like one that works. Both rules hold together:

  • Never block the business action because of the integration. Every FingerTrackID failure — timeout, network error, 429, 503, a thrown signer error, a disabled feature flag — ends with your product’s normal path.
  • Never fail silently. A configuration error must produce a clear, searchable log entry and, ideally, an alert on your side.

Check the configuration once, when your backend starts, and log a clear error if it is wrong instead of quietly skipping assessments:

  • The API origin is exactly https://api.fingertrackid.com.
  • The runtime has crypto.randomUUIDv7(): Node.js 24.16.0 or later on the 24 line (or 26.1.0 or later), as the wrapper below explains.
  • key_id is a lowercase UUIDv7.
  • The secret decodes: the reference signer’s decodeSecret(text, 'hex') throws FTID_SECRET_ENCODING for text that is not the 64-character hex value, and FTID_SECRET when the decoded key is too short.
  • If your own feature flag disables the integration, log that state once at startup so a disabled integration is a visible decision, not a mystery.

Never log the secret, the token, cookies or the request body.

Result Meaning Log level Login
201 / 200 Assessment stored info, with assessment_id and correlation_id Continue
401 authentication-failed, 403 forbidden, 400 validation-failed, 415 Your configuration or code is wrong; retrying will not help error, alert Continue
400 event-context-invalid The token expired or was already used before the assessment was stored; nothing was stored warn; error if frequent (collect closer to the action) Continue; assess once more as a new operation with null
401 timestamp-out-of-window, 401 request-replayed Clock or request_id generator problem warn; error if repeated Continue
409 binding-conflict A reference mapping changed: an account_ref arrived with another subject_ref than the first time. Nothing was stored, and retrying will not help error, alert Continue
409 idempotency-conflict The same operation IDs arrived with different content; nothing new was stored error Continue; fix the code that reuses operation IDs
429, 503, network error, timeout, FTID_INDETERMINATE Unavailable or unknown state warn Continue

Retry rules are on idempotency and retry; each type is described on errors.

FingerTrackID does not publish an end-to-end latency SLA. In production on 2026-10-15, on one application and a small sample (not an SLA), getEventContext on an accepted collection took 0.15 s at the median and 0.17 s at p95, while a collection started at the click took 2.6 s at the median, 3.9 s at p95 and at most 4.4 s from session open to finished evaluation (measured in production): prepare at the first intent, not at the click. These are the documented budgets to plan your own timeout with:

Where Budget Source
Browser, SDK 0.2.x and 0.3.0 Session open 3000 ms per attempt, collection 4000 ms, event context 5000 ms per action including renewal; your own submit ceiling (the helper’s waitMs) bounds what the user waits Timing and latency
Browser evidence and token 60 s or 300 s, per application, from when the collection reaches FingerTrackID; store the assessment before the token’s expiresAt The evidence window
Server, v1.1 (every tag) deadline_ms 5000 ms by default, 250–5000 ms allowed Journeys and tags
Server, v1 LOGIN deadline_ms 5000 ms by default, 250–5000 ms allowed Journeys and tags
Server, v1 TRADE deadline_ms 750 ms by default, 250–750 ms allowed Journeys and tags
Server, v1 WITHDRAWAL deadline_ms 1000 ms by default, 500–1000 ms allowed Journeys and tags
Your HTTP client The server budget plus your network latency: above about 6 s for a null LOGIN or v1.1 body; the reference example uses 6500 ms Authentication

deadline_ms is an internal budget that starts when the request reaches the service; it is not an SLA. Since 2026-10-14 the LOGIN and v1.1 default is 5000 ms (it was 1000 ms), so a client timeout that must not cut a null body has to exceed about 6 s. A body that signs deadline_ms explicitly may send any value up to 5000, or null; one already signed with 750 or 1000 stays valid and keeps exactly that budget. Pick an HTTP timeout your login can afford, and when it expires continue the login and, if you want the result, reconcile later with the same body (see timeout without a response).

Reconciling with the same body returns the stored assessment (200) whenever the first attempt was stored, even after the token expired. If it was not stored and the body carries a token, the same body succeeds only before that token’s expiresAt; after it, it returns 400 event-context-invalid and those operation IDs cannot be completed any more. Then assess the event as a new operation (new external_event_id and idempotency_key) with event_context_token: null, which a new operation accepts only while its occurred_at is within ±300 s of the server clock.

A sketch for a Node.js backend (for example a Next.js route handler or server action), built on the reference signer. log stands for your own structured logger.

The signer needs crypto.randomUUIDv7(), which Node.js has from 24.16.0 on the 24 line and from 26.1.0; Node.js 22 and 25 do not have it, and there the signer fails as soon as it is imported. Run the backend on Node.js 24 (24.16.0 or later; on Vercel, set the project’s Node.js version to 24.x) and check process.version in your startup validation. The wrapper loads the signer with await import(...) inside its try, so a runtime without it degrades to “no assessment” instead of breaking the login route.

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
}
}

The token check applies only before the first send of an operation. A retry or a later reconciliation of an operation already sent resends its stored bytes unchanged, token included.

The wrapper validates every 200/201 body against the response schema of the profile you sent (v1.1 or v1) before it reads a field. Validate Problem bodies against the Problem schema the same way before relying on anything beyond type, and adapt the retry policy to your login’s deadline.

In SHADOW, FingerTrackID assesses and recommends; nothing is enforced and your product keeps making every decision. The point of the period is to compare recommendations with what your product decided and with what later turned out to be true.

  • Your own operation IDs: external_event_id and idempotency_key.
  • assessment_id, assessment_status, trust_score, recommended_action, recommendation_basis, limitation_codes, evidence_coverage and the *_version fields, exactly as received.
  • The decision your product actually took, and why (your own rule, a manual review, a fallback).
  • Whether the event had a browser context (event_context_token was a token or null).
  • Whether the event carried subject_ref and account_ref.

Send what happened through the feedback API, against the assessment_id. It needs the credential permissions action:write and outcome:write.

  • Customer action: the action your product executed. In shadow your own policy decided, so the usual pair is basis: "CLIENT_POLICY" with reason_code: "CLIENT_POLICY_RULE"; a human decision is MANUAL_REVIEW; a decision taken under your fallback because the result was not available in time is FALLBACK with CLIENT_FALLBACK_APPLIED. Use ASSESSMENT only when you actually followed the recommendation.
  • Outcome: later evidence, such as LEGITIMATE or FRAUD_CONFIRMED with its evidence, or PENDING while you do not know yet. Corrections append; they never overwrite.

Each customer-action receipt carries recommendation_alignment (ALIGNED, NOT_ALIGNED or NOT_COMPARABLE), derived by FingerTrackID. Keep the receipts: the console does not yet show the customer action or outcome on an access.

  • Agreement rate: the share of ALIGNED among comparable actions. In shadow, NOT_ALIGNED means your policy and the recommendation differ; it does not say which one is right.
  • Known-good accounts: the recommendations for accounts you trust, such as internal staff or long-standing customers with no incidents. Frequent STEP_UP, REVIEW or BLOCK_RECOMMENDED among completed assessments there is a false-positive signal.
  • Known-bad cases: the recommendations on events you later confirmed as fraud.
  • Coverage: the share of COMPLETED against INCONCLUSIVE, and the limitation_codes behind them. Analyze no-score assessments separately: their recommendation is the fixed fallback, not an evaluation.

There is no sandbox or test environment: production, in shadow mode, is where you test. Because nothing is enforced, a test assessment cannot block anyone, but it is real production data. It is stored under your company, appears in the console and may count in usage estimates.

  • Use your production inputs only. The public signature self-check and the synthetic v1.1 examples (and the v1 examples) let you test signing and parsing without any credential.
  • Gate the integration behind your own feature flag and turn it on first for internal or test accounts, then for a small share of logins, then for everyone. Keep the flag as your kill switch.
  • Only the allowed origins under the collection domain. Collection through your collection domain is accepted from the application’s allowed origins whose host is that domain or one of its subdomains (exact HTTPS, no path, port or wildcard; up to 10, listed in Integração e saúde › Origens das páginas). Every other origin, including local development hosts and preview deployments, is refused without CORS approval, so the browser hides the response and the SDK returns UNAVAILABLE with reason TRANSPORT_ERROR. Your helper continues with null, and the backend assessment still works and returns a no-score result. Test browser collection on an allowed origin. A removed origin stops collecting at once (a browser may still hold a cached CORS preflight for a short time).
  • Keep the secret server-side. Store it only in the server-side environment of the deployment that calls the API, never in a variable exposed to the browser (in Next.js, never in a NEXT_PUBLIC_ variable), and only in environments you are willing to send production data from.

Questions: contato@fingertrackid.com.