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.
The production API origin
Section titled “The production API origin”Your backend sends every signed call to the production assessment API origin:
https://api.fingertrackid.comFor 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.
The request profile
Section titled “The request profile”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).
When the browser has no event context
Section titled “When the browser has no event context”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:
{ "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"}Social and OAuth logins
Section titled “Social and OAuth logins”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_refandaccount_refare 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 withfetch, 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.
Validate configuration at startup
Section titled “Validate configuration at startup”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_idis a lowercase UUIDv7.- The secret decodes: the reference signer’s
decodeSecret(text, 'hex')throwsFTID_SECRET_ENCODINGfor text that is not the 64-character hex value, andFTID_SECRETwhen 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.
Classify outcomes
Section titled “Classify outcomes”| 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.
Time budget
Section titled “Time budget”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 minimal fail-open wrapper
Section titled “A minimal fail-open wrapper”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 + 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 }}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.
3. Evaluating the shadow period
Section titled “3. Evaluating the shadow period”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.
Record, for every assessment
Section titled “Record, for every assessment”- Your own operation IDs:
external_event_idandidempotency_key. assessment_id,assessment_status,trust_score,recommended_action,recommendation_basis,limitation_codes,evidence_coverageand the*_versionfields, 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_tokenwas a token ornull). - Whether the event carried
subject_refandaccount_ref.
Report actions and outcomes
Section titled “Report actions and outcomes”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"withreason_code: "CLIENT_POLICY_RULE"; a human decision isMANUAL_REVIEW; a decision taken under your fallback because the result was not available in time isFALLBACKwithCLIENT_FALLBACK_APPLIED. UseASSESSMENTonly when you actually followed the recommendation. - Outcome: later evidence, such as
LEGITIMATEorFRAUD_CONFIRMEDwith its evidence, orPENDINGwhile 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.
What to look at
Section titled “What to look at”- Agreement rate: the share of
ALIGNEDamong comparable actions. In shadow,NOT_ALIGNEDmeans 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,REVIEWorBLOCK_RECOMMENDEDamong completed assessments there is a false-positive signal. - Known-bad cases: the recommendations on events you later confirmed as fraud.
- Coverage: the share of
COMPLETEDagainstINCONCLUSIVE, and thelimitation_codesbehind them. Analyze no-score assessments separately: their recommendation is the fixed fallback, not an evaluation.
4. Testing before production
Section titled “4. Testing before production”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
UNAVAILABLEwith reasonTRANSPORT_ERROR. Your helper continues withnull, 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.