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

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

  1. Initialize once per page, in browser code, and keep the client in a module that outlives your login components. initializeSdk() makes no request.
  2. 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.
  3. 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 expiresAt and 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.
  4. At submit, take this action’s context with take(), which waits at most waitMs and never aborts the SDK. Send the token and its expiresAt with the business request, or null together with the reason, for your logs.
  5. 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.

One module per page. It uses only the public SDK API, never throws and never aborts the SDK.

  • prepareNextAction() runs at the first focusin or pointerdown on the form, and on every later one; for a sign-in button, at its pointerenter, focus and pointerdown. 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 within SUBMIT_WAIT_MS with { context, failure }. Use the object it returns for that action and every retry of it, including null: 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_MS while the helper collects; show progress during the wait. On touch devices pointerdown comes 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 than minRemainingMs left (15000 ms by default; option of createActionContextHelper). Keep it above the margin your backend needs to store the assessment (integration guidance).
  • With the helper, do not call prepare(), collect() or getEventContext() yourself, and do not call the client’s close() directly: use closeSdk(). A collect() of your own is adopted, but it costs a collection of its own.
  • A null context is never silent: failure gives the stage and the code (stages and reasons). Count these on your side; an integration where every login reaches the backend with null looks, 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 expiresAt with the token so your backend can drop a token that expired before it could be used.
  • Choose SUBMIT_WAIT_MS from 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 = null
let 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
}

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.

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": a GET form 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 through HTMLFormElement.prototype keeps it working even if a field is named submit.
  • 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 = false
loginForm.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
})
  • Call prepareNextAction() from the form’s focus and pointer-down handlers. In React, onFocus and onPointerDown on 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’s StrictMode runs 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 AbortSignal tied 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 fetch resolves, 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
}

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 (60000 or 300000), including assessments sent with event_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 expiresAt works with either window; only your collection margin needs to fit the shorter one.

How the window applies:

  • getEventContext can return a token only inside the window. Once the collection’s window has ended, it returns UNAVAILABLE with reason SERVER_UNAVAILABLE (or INCONCLUSIVE, if the collection was only evaluated after its window).
  • The returned expiresAt is 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. Calling getEventContext again on the same handle after expiresAt returns UNAVAILABLE with reason EVENT_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 with 400 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 with event_context_token: null instead (integration guidance).
  • An assessment that was already stored is not affected: a retry with the same body returns the stored result (200) even after expiresAt.

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_at from 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 INCONCLUSIVE with trust_score: null, never COMPLETED. With a context it is COMPLETED when 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_AUTOMATION and BROWSER_SPOOFING, were evaluated; otherwise it is INCONCLUSIVE. With nothing adverse found the trust_score is 100, and the managed recommendation is MONITOR in 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 example 503). Validate the actual response or Problem under your application’s issued policy.

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 returns RATE_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 at close(). 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() or renew(); nothing reopens in the background. Handles from the old session return CAPABILITY_EXPIRED (createEvent and getEventContext on 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.

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 getEventContext call and never reset. Inside them the SDK asks again 400 ms after each PROCESSING answer (or after a longer Retry-After) and repeats a timed-out, lost, 429 or 503 lookup by itself. No lookup starts with less than 1000 ms left. When the budget ends, the event keeps its last answer, which can be PROCESSING.
  • 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 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.

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.

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

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

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.

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.

Everything above applies, except the helper, with these differences:

  • One collection per client. A second collect() after an accepted one returns UNAVAILABLE with reason CONFLICT, and the client keeps its session. To collect again, await close() and call initializeSdk() 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 no createActionContextHelper(). The first collect() opens the session.
  • getEventContext has 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.

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.

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.