Collection and event context
This page describes SDK 0.3.0, the package the dash offers
(release notes). Its API adds
createActionContextHelper, which runs the whole lifecycle below for you; everything else keeps
the API of 0.2.x. If your frontend still runs an earlier version, read
earlier versions as well.
The lifecycle
Section titled “The lifecycle”The SDK keeps its state in page memory and turns one accepted collection into an opaque, short-lived token per business action. Your backend sends that token with its assessment. For a login:
- Initialize once per page, in browser code, and keep the client in a module that outlives
your login components.
initializeSdk()makes no request. - Prepare on the first intent. At the first focus or pointer-down on the login form, call
prefetch(): it collects and starts the event context, so the token is usually ready by the submit. Not at page load: every load would open a session and a collection that counts as usage (the usage unit), also for visitors who never log in. - Keep it fresh. Browser evidence is usable only inside your application’s
evidence window, 60 or 300 seconds from when the collection reaches
FingerTrackID. The helper learns the window from the tokens’ own
expiresAtand collects again only when it has to: the evidence would expire before your margin, the collection’s session ended, two lookups in a row were refused or expired on a collection that already gave a token, or a transient limitation came back (once per page). A collection that never gave a token is replaced only when its session ended (once in a row), for that one retry, or 300 seconds after it was sent, so an outage of event contexts alone costs at most one extra collection per 300 seconds (stages and reasons). Its clock counts time the device spent asleep. - At submit, take this action’s context with
take(), which waits at mostwaitMsand never aborts the SDK. Send the token and itsexpiresAtwith the business request, ornulltogether with the reason, for your logs. - Close only when you are done. Call
close()after your backend has answered the request that carried the token, on logout or on account switch. Closing earlier terminates the session that issued the token, and a token whose assessment is not stored yet stops working.
In production on 2026-10-15 (one application, a small sample, not an SLA), getEventContext on
an accepted collection took 0.15 s at the median, while a collection started at the click took
2.6 s (measured in production): design for the token to be
ready before the submit instead of fetching it at the submit.
The recipe
Section titled “The recipe”One module per page. It uses only the public SDK API, never throws and never aborts the SDK.
prepareNextAction()runs at the firstfocusinorpointerdownon the form, and on every later one; for a sign-in button, at itspointerenter,focusandpointerdown. Never call it at page load: each load opens a collection that counts as usage (the usage unit). It never waits and never retries a failed collection by itself.takeActionContext()runs once per business action, at its submit, and resolves withinSUBMIT_WAIT_MSwith{ context, failure }. Use the object it returns for that action and every retry of it, includingnull: do not take another context to change a request already sent. A token is never handed to two actions.- A click that is the first intent, or that follows a collection whose evidence ran out, waits up
to
SUBMIT_WAIT_MSwhile the helper collects; show progress during the wait. On touch devicespointerdowncomes just before the click, so the first tap often waits. - Freshness comes from the token’s
expiresAt, and the age of a collection counts time the device spent asleep. The helper never hands over a token with less thanminRemainingMsleft (15000 ms by default; option ofcreateActionContextHelper). Keep it above the margin your backend needs to store the assessment (integration guidance). - With the helper, do not call
prepare(),collect()orgetEventContext()yourself, and do not call the client’sclose()directly: usecloseSdk(). Acollect()of your own is adopted, but it costs a collection of its own. - A
nullcontext is never silent:failuregives the stage and the code (stages and reasons). Count these on your side; an integration where every login reaches the backend withnulllooks, from FingerTrackID’s side, exactly like one where the SDK never ran. The code is for your logs only: the assessment request has no field for it. - Send
expiresAtwith the token so your backend can drop a token that expired before it could be used. - Choose
SUBMIT_WAIT_MSfrom your own measurements (0 to 10000 ms). With the prefetch the context has the time the user spends on the form to settle. A collection started at the click took 2.6 s at the median in production (measured in production), above this recipe’s 2500 ms: preparing at the first intent, so that an accepted, fresh collection is waiting at the click, matters more than raising the ceiling.
import { createActionContextHelper, initializeSdk } from '@ftid/sdk-web'
const configuration = { publicKey: 'REPLACE_WITH_ISSUED_PUBLIC_APPLICATION_KEY', collectionOrigin: 'https://collect.customer.example',}// Your choice, not an SDK value: the longest a submit waits for its context.const SUBMIT_WAIT_MS = 2_500
let helper = nulllet unavailable = null // Why there is no helper (initializeSdk threw), never a token.
function actionContexts() { if (helper || unavailable) return helper try { helper = createActionContextHelper(initializeSdk(configuration), { waitMs: SUBMIT_WAIT_MS }) } catch (error) { // initializeSdk and createActionContextHelper throw only fixed codes. const reason = /^[A-Z_]{1,64}$/.test(error?.message) ? error.message : 'UNEXPECTED' const failure = Object.freeze({ stage: 'COLLECT', reason }) unavailable = Object.freeze({ context: null, failure }) } return helper}
// First focus or pointer-down on the form, and every later one: cheap, never waits.// Not at page load.function prepareNextAction() { actionContexts()?.prefetch()}
// At submit: this action's { context, failure } within SUBMIT_WAIT_MS.async function takeActionContext() { return (await actionContexts()?.take()) ?? unavailable}
// After the backend answered and this page is done with the client, on logout or account switch.function closeSdk() { const closing = helper?.close() helper = null unavailable = null return closing}Password login
Section titled “Password login”The recipe above serves both a classic form and a single-page application. In both, the token
goes with the request your backend assesses: the password form, or the MFA code form when your
login asks for one (call prepareNextAction() when that form gets focus or a pointer-down and
takeActionContext() at its submit, as for the password form). Never send form values to the
SDK.
Classic form POST
Section titled “Classic form POST”A submitted form unloads the page, and the unload cancels the SDK’s requests. Hold the submit until the context is settled or your ceiling ends, write it into hidden fields, then submit.
- Keep
method="post": aGETform would put the token in the URL. submit()does not send the clicked button’s name and value; if your backend reads them, put them in a hidden field. Calling it throughHTMLFormElement.prototypekeeps it working even if a field is namedsubmit.- Do not call
close()here: the page that answers has no SDK client. The session ends by itself after 30 minutes and holds one session slot until then.
<form id="login" method="post" action="/login"> <!-- Your existing fields. --> <input type="hidden" name="ftid_event_context_token" /> <input type="hidden" name="ftid_event_context_expires_at" /> <input type="hidden" name="ftid_context_failure" /> <button type="submit">Sign in</button></form>// prepareNextAction and takeActionContext come from the recipe above.const loginForm = document.getElementById('login')loginForm.addEventListener('focusin', () => prepareNextAction())loginForm.addEventListener('pointerdown', () => prepareNextAction())if (loginForm.contains(document.activeElement)) prepareNextAction() // An autofocused field.
let submitting = falseloginForm.addEventListener('submit', async (event) => { event.preventDefault() // Hold the navigation until the context is settled or the ceiling ends. if (submitting) return submitting = true const { context, failure } = await takeActionContext() const fields = loginForm.elements fields.ftid_event_context_token.value = context?.token ?? '' fields.ftid_event_context_expires_at.value = context?.expiresAt ?? '' fields.ftid_context_failure.value = failure ? `${failure.stage}:${failure.reason}` : '' // Submits without firing this listener again; the page navigates only now. HTMLFormElement.prototype.submit.call(loginForm)})// A page restored from the back/forward cache must be able to submit again.window.addEventListener('pageshow', (event) => { if (event.persisted) submitting = false})Single-page application (fetch)
Section titled “Single-page application (fetch)”- Call
prepareNextAction()from the form’s focus and pointer-down handlers. In React,onFocusandonPointerDownon the<form>element receive those of every field. - Keep the SDK out of component state. A component that unmounts at submit (a route change, a spinner replacing the form) must not take the client or its pending work with it.
- Do not call
closeSdk()in an effect cleanup. React’sStrictModeruns cleanups an extra time in development, and a cleanup that closes the client terminates the session of a token not sent yet. - Do not pass an
AbortSignaltied to the component or the route to SDK calls, and do not abort to stop waiting:takeActionContext()stops waiting by itself and leaves the SDK running. - Navigate after
fetchresolves, never before.
Logins that leave the page (Google sign-in, other OAuth providers) need one more step: login with Google, OAuth and other redirects.
// takeActionContext and closeSdk come from the recipe above, kept in a module that outlives the// login component. csrfToken stands for your existing CSRF protection.async function submitLogin(credentials) { const { context, failure } = await takeActionContext() const response = await fetch('/api/login', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify({ ...credentials, eventContextToken: context?.token ?? null, eventContextExpiresAt: context?.expiresAt ?? null, contextFailure: failure, }), }) // The backend has answered: a successful login is done with the client. A failed attempt keeps // it, and the next attempt prepares its own context. if (response.ok) void closeSdk() return response}The evidence window
Section titled “The evidence window”Browser evidence is usable for a limited time, counted from the moment FingerTrackID receives
the collection sent by collect(), not from page load or from initializeSdk(). The length is
fixed per application by the managed production collection setup when the application is
provisioned:
| Application | Evidence window |
|---|---|
| Provisioned before the 300-second window was introduced (October 2026), including the first pilot applications | 60 seconds |
| Provisioned since then | 300 seconds |
An existing application keeps its window until FingerTrackID changes it with you; nothing changes it silently. To see yours:
- Every assessment response carries it in
data_freshness.maximum_required_age_ms(60000or300000), including assessments sent withevent_context_token: null. - For a specific token, the limit that counts is always its
expiresAt, described below. - If in doubt, ask FingerTrackID support for your application’s value. Code that respects
expiresAtworks with either window; only your collection margin needs to fit the shorter one.
How the window applies:
getEventContextcan return a token only inside the window. Once the collection’s window has ended, it returnsUNAVAILABLEwith reasonSERVER_UNAVAILABLE(orINCONCLUSIVE, if the collection was only evaluated after its window).- The returned
expiresAtis the earliest of: the window length after the token was issued, the end of the collection’s window, the end of the SDK session and the end of the SDK’s current capability. It is therefore never later than the window length after the collection reached FingerTrackID. CallinggetEventContextagain on the same handle afterexpiresAtreturnsUNAVAILABLEwith reasonEVENT_CONTEXT_EXPIRED. - The assessment that carries the token must be stored before
expiresAt. A new assessment with an expired (or already used) token is refused with400 event-context-invalid: not retryable, nothing is stored, and the operation IDs of that request stay bound to that token. Assess the event as a new operation withevent_context_token: nullinstead (integration guidance). - An assessment that was already stored is not affected: a retry with the same body returns the
stored result (
200) even afterexpiresAt.
From the browser to the API
Section titled “From the browser to the API”The browser sends only the token and its expiresAt (or null) to your backend, in the
business request it already makes. Your backend puts the token in the event_context_token
field of POST /v1/assessments, or null if expiresAt leaves too little
time to store the assessment (the evidence window).
- The token is opaque and single-use: it is consumed once per business event. Do not decode it, write it to logs or analytics, put it in a URL, or use it as your own authorization token.
- Your backend never accepts person and account references or
occurred_atfrom the browser. - When there is no token, your backend still sends the assessment with
event_context_token: null(integration guidance). - A request already sent to FingerTrackID keeps its original token (or
null) on every retry. - Keep the SDK client alive until your backend has answered. Until the assessment that carries
the token is stored, terminating the session that issued it (
close()) invalidates the token. Never extend the original context or request deadline to wait for completion. - Under the current managed production profile, a stored assessment without a context is
INCONCLUSIVEwithtrust_score: null, neverCOMPLETED. With a context it isCOMPLETEDwhen at least 30% of the 13 indicators were evaluated with fresh evidence (evidence_coverage≥ 30, counted across browser, network and device-history indicators together) and both indicators that count toward the score,BOT_AUTOMATIONandBROWSER_SPOOFING, were evaluated; otherwise it isINCONCLUSIVE. With nothing adverse found thetrust_scoreis100, and the managed recommendation isMONITORin every band: the score informs, it never decides. Once your company activates its own risk rules, the score and the bands follow them, and every indicator you activate becomes required: an active indicator without usable fresh evidence (for example a network indicator while network evidence is unavailable) leaves the assessment without a score (trust_score: null,MONITOR), even above 30% coverage. The context is what attaches the browser evidence (see how to tell that SDK evidence arrived). The call can still end in an operational Problem (for example503). Validate the actual response or Problem under your application’s issued policy.
Session limits
Section titled “Session limits”A client opens a browser session for your application only when it first needs server
authority: at prepare() or the first collect(). initializeSdk() alone opens no session and
consumes no quota. The limits are per application, the same for every application, and need no
configuration:
| Limit | Value |
|---|---|
| New sessions | 5 per second, with a burst of 20 |
| Concurrent live sessions | Up to 1000 per application |
| Session lifetime | 30 minutes from the operation that opens it, never extended |
| Requests per session | One collection or event-context request per 250 ms, which the SDK respects by itself |
| Event contexts per session | 128 |
A session stops counting toward the concurrent limit when it ends: when it is terminated
(close(), or the SDK retiring it, below) or when its 30 minutes run out. A session that is
never closed holds its slot for the full 30 minutes, even after the visitor has left. When a
limit is reached, a new session is refused and collect() returns status: 'UNAVAILABLE' with
reason RATE_LIMITED, both above the new-session rate and at the concurrent limit; at the
concurrent limit the Retry-After the SDK honours is the time until the first of the held
sessions expires, at most 30 minutes.
Continue the business action with a null context, as with any other collection failure, and
never retry in a loop. See also the API limits.
How the SDK uses sessions (since 0.2.0):
- Each new collection after an accepted one opens a new session, at most one per second per
client (a
collect()sooner than that returnsRATE_LIMITED, and the previous collection stays usable). The replaced session is terminated by the client once the last token it issued has expired (at most 300 seconds), at once if it issued none, or atclose(). A client keeps at most three replaced sessions waiting; beyond that it terminates the oldest one. - After 30 minutes the client opens the next session by itself on its next
collect(),prepare()orrenew(); nothing reopens in the background. Handles from the old session returnCAPABILITY_EXPIRED(createEventandgetEventContexton them open no session), and the helper collects again. Since 0.3.0 those 30 minutes count time the device spent asleep.
To keep within 1000 concurrent sessions:
- Prepare only on pages that have an assessed action (login, withdrawal, trade), never on every page view, and only on the first intent (focus or pointer-down) of its form: then sessions open only for visitors who start the action.
- Call
close()once your backend has answered and the page is done with the client, and on logout or account switching.
Timing and latency
Section titled “Timing and latency”FingerTrackID publishes no latency SLA. The first figures measured in production are
below; they come from one application and a small sample.
Plan with the limits that follow, measure on your own page (from collect() to a COMPLETED
context) and choose SUBMIT_WAIT_MS from what you see. These limits are protocol bounds, not
expected latency or an SLA.
Each request has a client deadline longer than the server’s budget for that route, because a browser request also spends DNS, TLS, the CORS preflight and the trip back:
| Step | SDK 0.2.0 and later | Server budget |
|---|---|---|
Session open (prepare() or the first collect()) |
3000 ms per attempt; a timed-out, lost, 429 or 503 attempt is repeated once in the same call when the wait for it fits in one second |
1000 ms |
| Collection | 4000 ms per attempt | 2000 ms |
| Event context | 1500 ms per request, inside a budget of 5000 ms per event, at most 8 event-context requests per event | 500 ms per request |
| Capability renewal and session termination | 1500 ms per attempt | 1000 ms |
- The 5000 ms of an event start with its first
getEventContextcall and never reset. Inside them the SDK asks again 400 ms after eachPROCESSINGanswer (or after a longerRetry-After) and repeats a timed-out, lost,429or503lookup by itself. No lookup starts with less than 1000 ms left. When the budget ends, the event keeps its last answer, which can bePROCESSING. - One session sends one collection or event-context request at a time, at least 250 ms after the previous one settled.
- A submit with nothing prepared needs at least three round trips to your collection domain before a token exists (session open, collection, event context), plus FingerTrackID’s processing of the collection. That is why the recipe prefetches at the first intent.
- The first request to each route may also need a CORS preflight. 0.2.0 lets the browser reuse it (the server allows caching it for 600 seconds); 0.1.1 sent a preflight before every request.
- Request headers added by page instrumentation make every request fail (tracing headers).
Measured in production (2026-10-15)
Section titled “Measured in production (2026-10-15)”Measured on 2026-10-15 in production, on one application:
| Measurement | Sample | Median | p95 | Maximum |
|---|---|---|---|---|
getEventContext on an accepted collection, until the token |
10 | 0.15 s | 0.17 s | — |
| A collection started at the click, from session open to the end of the collection’s evaluation | 13 | 2.6 s | 3.9 s | 4.4 s |
This is a small sample from a single application: read it as indicative, not as an SLA, and measure your own page, network and users’ devices. The second row is roughly what a submit with nothing prepared waits before its token can exist; preparing at the first intent leaves only the first row at the submit.
What throws and what returns a reason
Section titled “What throws and what returns a reason”| Call | Throws | Otherwise returns |
|---|---|---|
initializeSdk(configuration) |
An Error whose message is INVALID_CONFIGURATION, UNSUPPORTED_RUNTIME or CONFIGURATION_CONFLICT (and CLOCK_INVALID in a browser whose monotonic clock misbehaves) |
The client, synchronously |
createEvent(observation) |
An Error whose message is CLOSED, INVALID_HANDLE or CAPABILITY_EXPIRED (and CLOCK_INVALID or UNSUPPORTED_RUNTIME in a broken browser) |
The event handle, synchronously |
prepare({ signal }?) |
Never; the promise never rejects | READY, or UNAVAILABLE with a reason |
collect({ signal }?) |
Never | ACCEPTED with its observation, or UNAVAILABLE with a reason and the pending observation (or null) |
retryCollection(observation, { signal }?) |
Never | The same as collect(); an already accepted observation answers ACCEPTED without a request |
getEventContext(event, { signal }?) |
Never | COMPLETED with eventContextToken and expiresAt; PROCESSING when its budget ended first; INCONCLUSIVE or FAILED with a limitationCode; or UNAVAILABLE with a reason |
renew({ signal }?) |
Never | RENEWED, NOT_DUE, or UNAVAILABLE with a reason |
close() |
Never | TERMINATED, UNCERTAIN or LOCAL_ONLY |
createActionContextHelper(client, { minRemainingMs, waitMs }?) |
An Error whose message is INVALID_INPUT (options other than those two integers, minRemainingMs 1000–30000 and waitMs 0–10000), INVALID_HANDLE (not a client of this SDK copy), CLOSED or CONFIGURATION_CONFLICT (the client already has a helper with other options) |
The helper, synchronously; the same options return the same helper |
helper.prefetch() |
Never | Nothing; it never waits |
helper.take({ waitMs }?) |
Never; the promise never rejects | { context: { token, expiresAt }, failure: null } or { context: null, failure: { stage, reason } } |
helper.close() |
Never | The client’s close() result |
UNAVAILABLE carries retryAfterMs after a server 429 or 503 and when the SDK’s own back-off
refuses a request. The SDK renews its short-lived capability by itself inside these calls;
renew() only does it ahead of time and never extends the 30-minute session. close() returns
TERMINATED when every session it held confirmed its termination; UNCERTAIN when any did not,
when a session open was still in flight, or when it held no session after trying to open one;
and LOCAL_ONLY when it never tried to open one.
Failure reasons
Section titled “Failure reasons”Every UNAVAILABLE result carries a reason (type SdkFailureReason), and createEvent throws
an Error whose message is one of the same codes. In every case continue the business action
with a null context.
reason |
Likely cause | What to do |
|---|---|---|
TRANSPORT_ERROR |
The request did not complete, or the browser hid the answer: network failure, a CSP connect-src without the collection origin, a blocking extension, a page origin that is not one of the application’s allowed origins, a CORS preflight refused because page instrumentation added request headers, or the platform-wide new-session limit (100 per second shared by all applications) |
Check the origin in Integração e saúde › Origens das páginas, your connect-src and your tracing configuration; otherwise treat as transient |
AUTHENTICATION_FAILED |
401/403 from your collection domain on a correctly allowed origin: a publicKey that is not this application’s, or an SDK version your application’s collection is not set up for |
Check the public key issued in the dash; install the SDK version the dash offers, and contact support before you change it |
RATE_LIMITED |
Above the application’s new-session rate or its 1000 concurrent sessions, more than 128 event contexts in one session, a collect() within one second of the previous session open, or the SDK’s own back-off after a Retry-After |
Do not retry in a loop; continue with null; free sessions with close() |
SERVER_UNAVAILABLE |
503 (or another unexpected status), including an event whose collection’s evidence window has ended |
Continue with null; collect closer to the action |
TIMEOUT |
A request ran past its client deadline (after the in-call retry), or the event’s 5000 ms budget ended | Continue with null; if frequent, prepare the context earlier |
CAPABILITY_EXPIRED |
A handle from a session the client no longer uses. Since 0.2.0 a new collect() after an accepted one opens a new session and retires the earlier observation: createEvent on it throws this code and a new lookup for an event created from it is refused, while a token it already returned stays usable until its expiresAt and a lookup already started finishes. The end of the session’s 30 minutes has the same effect |
Use the latest collection. With the helper, take() reports it, as EVENT or CONTEXT, only when that take cannot start the replacing collection: it already started its one collection, a collection back-off is running, or the limit on replacements a failure causes holds it (300 seconds since the previous one). While the helper’s last collection failed and the current one is stale, the COLLECT reason is reported instead (stages and reasons). A collect() of your own beside the helper makes it more likely |
EVENT_CONTEXT_EXPIRED |
getEventContext called again for an event whose token passed expiresAt |
Use null for that action; a new action creates a new event |
CONFLICT |
A 409 conflict with the session or event state. With 0.1.1, also a second collect() on the same client |
Continue with null; with 0.1.1, see below |
ABORTED |
The signal you passed was aborted |
Expected after your own cancellation; never pass a signal tied to page teardown |
CLOSED |
The client was closed | Initialize a new client for later actions |
INVALID_HANDLE |
An observation or event handle that this client does not know (from another client, or from before close()), or an observation that was not accepted |
Pass the handles this client returned |
INVALID_INPUT |
An options argument other than { signal } with an AbortSignal |
Fix the call |
INVALID_RESPONSE |
A response that failed the SDK’s checks, for example altered by a proxy, CDN or WAF in front of the collection domain | Keep the collection CNAME DNS-only, with nothing in front |
CLOCK_INVALID |
The browser’s clock or monotonic timer behaved invalidly | Continue with null |
BODY_TOO_LARGE |
An SDK request above its size limit | Report it to support with the SDK version |
UNSUPPORTED_RUNTIME |
The browser lacks a required feature, such as cryptographic randomness | Continue with null |
Stages and reasons
Section titled “Stages and reasons”When take() has no context, failure.stage says where the action stopped and
failure.reason why. The reasons are the failure reasons above plus these.
Each take() starts at most one collection and two lookups.
stage |
reason |
What the helper does next |
|---|---|---|
COLLECT |
SERVER_UNAVAILABLE, RATE_LIMITED, TIMEOUT, TRANSPORT_ERROR, INVALID_RESPONSE, AUTHENTICATION_FAILED… |
The helper’s collection failed (AUTHENTICATION_FAILED when the session open is refused, for example because your application’s collection is not set up for this SDK version). prefetch() never retries it. Each take() retries it once, after a back-off of 1, 2, 4… seconds (at most 300, never below Retry-After); the retry resends the same observation, so it is not a new collection that counts as usage. Until a collection is accepted, a take() whose current collection is stale keeps reporting this reason, inside the back-off too, even when its lookup on the stale collection fails with EVENT or CONTEXT; a usable token from that lookup still wins |
COLLECT |
INVALID_INPUT, BODY_TOO_LARGE, UNSUPPORTED_RUNTIME |
The collection can never succeed on this page: the helper sends no more requests and keeps answering this reason |
COLLECT |
INVALID_CONFIGURATION, UNSUPPORTED_RUNTIME, CONFIGURATION_CONFLICT, UNEXPECTED |
initializeSdk() or createActionContextHelper() threw; the recipe keeps that code until closeSdk() |
EVENT |
CAPABILITY_EXPIRED, INVALID_HANDLE |
The collection’s session ended: the helper drops the collection and the next intent collects again, under the limit on replacements a failure causes (next row) |
CONTEXT |
AUTHENTICATION_FAILED, CAPABILITY_EXPIRED, INVALID_HANDLE |
The helper drops the collection. Under the limit on replacements a failure causes (this one, two failed events in a row, a transient limitation’s retry, or a collection that never gave a token, whether or not the collection gave a token before), each is sent no sooner than 300 seconds after the previous one was. The first may come at the next intent (prefetch() or take()); if the collection it brings fails too, only a take() replaces it. A token does not lift the limit. A collection replaced for its age or its 128 tokens, or one your page made with collect(), lets prefetch() replace again; the 300 seconds still apply |
CONTEXT |
SERVER_UNAVAILABLE, RATE_LIMITED |
The next lookup waits 1, 2, 4… seconds (at most 60, never below Retry-After). Two such events in a row, EVENT_CONTEXT_EXPIRED included, on a collection that already gave a token make the next intent collect again, under the limit above; until it can, the helper keeps looking the collection up and hands over its token if the events recover. A collection that never gave a token is replaced only 300 seconds after it was sent |
CONTEXT |
EVENT_CONTEXT_EXPIRED |
The lookup answered expired, which counts toward the two in a row above, with no wait; or the token came with less than minRemainingMs left, so it is never handed over |
CONTEXT |
TIMEOUT, TRANSPORT_ERROR, INVALID_RESPONSE |
The next lookup on the same collection waits, as above. These never count toward the two in a row and never start a new collection |
CONTEXT |
PROCESSING |
The collection was still processing when the lookup’s budget ended. The count of events in a row starts over, and the next decision looks it up again |
CONTEXT |
INCONCLUSIVE:<limitation code>, FAILED:<limitation code> |
The server’s technical limitation. DETECTOR_TIMEOUT, DETECTOR_ERROR, PROVIDER_UNAVAILABLE and REQUIRED_EVIDENCE_MISSING start one new collection per code per page, under the limit above; any other code stays the answer, with no request, until the collection can be replaced |
WAIT |
WAIT_EXCEEDED |
waitMs ended first; the work still running continues and serves the next action |
WAIT |
CLOSED, INVALID_INPUT, CLOCK_INVALID |
The client was closed, take() got an invalid option, or the browser’s clock misbehaved |
Logout and account switching
Section titled “Logout and account switching”On logout or when the account changes, call closeSdk() from the recipe (it closes the client
and discards its handles) before the next account’s first action. close() returns
TERMINATED, UNCERTAIN or LOCAL_ONLY; closing in the browser does not replace your backend’s
logout. Never send one user’s context under another user’s account. Do not delay logout or
account switching for an optional assessment. If either interrupts an uncommitted context,
preserve the normal error/fallback and the existing logical request.
Earlier versions
Section titled “Earlier versions”Page code written for SDK 0.2.x
Section titled “Page code written for SDK 0.2.x”Page code written for 0.2.x runs unchanged on 0.3.0: every 0.2.x export and type is the same. Your application’s collection accepts one SDK version at a time, so contact FingerTrackID support before moving it (upgrading). Moving that code to the recipe above is recommended but can come in a later deploy of yours.
If you still run SDK 0.1.1
Section titled “If you still run SDK 0.1.1”Everything above applies, except the helper, with these differences:
- One collection per client. A second
collect()after an accepted one returnsUNAVAILABLEwith reasonCONFLICT, and the client keeps its session. To collect again,await close()and callinitializeSdk()for a new client, but only when no token from the old client is still pending, that is, sent or about to be sent and not yet stored by an assessment: closing terminates its session and that token stops working. While the accepted collection is fresh, a new action can instead create a new event from it. - No
prepare()and nocreateActionContextHelper(). The firstcollect()opens the session. getEventContexthas a single 2000 ms budget per event and at most three event-context requests.- Every SDK request is preceded by a CORS preflight, and the tracing headers rule applies the same way.
SDK 0.1.0 also never opens a second session: after its 30 minutes, collect() returns
CAPABILITY_EXPIRED until you await close() and call initializeSdk() again. Install the
package the dash offers.
What the SDK collects
Section titled “What the SDK collects”The full list, for your privacy policy, is on
data collected and processing. In short: a browser profile
limited by your application’s collection policy: browser and user-agent data (including
low-entropy client hints), screen, time zone, language, automation markers such as
navigator.webdriver, privacy and consistency checks, and, under the current managed
collection policy, rendering characteristics of canvas, WebGL, offline audio and installed
fonts. Rendering characteristics leave the browser only as derived values: fixed-length hashes,
a GPU family, booleans, a count and a presence mask over a fixed list of fonts. Pixels, audio
samples, WebGL vendor and renderer strings and font names never leave the browser.
The SDK does not collect high-entropy client hints or pointer and keystroke behaviour, and its
code does not read or write cookies, localStorage or sessionStorage. To tell a private
window from a normal one it uses browser storage briefly: on Chromium-based browsers it writes
zero-filled blocks to a temporary IndexedDB database named __ftid_pp_… and deletes it at the
end of the check (a database left by a page closed mid-check is deleted by a later check), and
reads the storage quota estimate; on Safari and Firefox it opens the origin’s private file
system once, which may create that origin’s empty root. None of this stores data about the
user.
The browser manages a signed HttpOnly, Secure cookie from your collection domain for
continuity; the script never reads it. If the cookie is missing or rejected, continuity across visits is
reduced, but collection still completes normally.
Support
Section titled “Support”For support, keep operation IDs, request_id, correlation_id, public error codes, timestamps
and versions. Never send tokens, secrets, cookies, raw headers, personal data or full payloads.