Server SDK for Node.js
@fingertrackid/server is the FingerTrackID client for a Node.js backend. It builds each request
with the same rules the API applies, signs it with the
reference signer, retries only what
the published Problem catalog marks retryable, and returns every outcome as a result. It never
throws after startup, so your product keeps its own decision when FingerTrackID is slow or
unavailable.
The SDK is optional. Any backend that follows the authentication rules and reproduces the public test vector integrates the same way; the published contracts win over the SDK, as over any example.
Install
Section titled “Install”Node.js 24.16 or newer on the 24 line, or 26.1 or newer. Pin the exact version.
self-check reproduces the public signature vector on the Node.js that runs it and needs no
credential.
The package has no install-time dependency and no install script: it is one unminified bundle you
can read, with the open-source licenses it carries in THIRD-PARTY-NOTICES.txt. Compare the
integrity in your lockfile with npm view @fingertrackid/server@0.1.2 dist.integrity.
npm install --save-exact @fingertrackid/server@0.1.2npx --no fingertrackid-server self-checkConfigure
Section titled “Configure”The client takes its values as options; the examples read them from these variables, which the
check command also reads:
| Variable | Value |
|---|---|
FTID_KEY_ID |
The key_id of the server credential, from the console or from fingertrackid.integration.json |
FTID_HMAC_SECRET_HEX |
The 64 hexadecimal characters shown once in the console, read from your secret manager |
The API origin defaults to https://api.fingertrackid.com; leave apiOrigin unset in production.
Create one client when the backend starts. A missing or malformed value throws
FtidConfigurationError with a fixed code, and never echoes the value.
import { createFtidServerClient, type FtidServerClient } from '@fingertrackid/server'
let ftid: FtidServerClient | null = nulltry { ftid = createFtidServerClient({ keyId: process.env.FTID_KEY_ID ?? '', secretHex: process.env.FTID_HMAC_SECRET_HEX ?? '', })} catch (error) { // Alert on it, and keep the product running without FingerTrackID. logger.error('ftid.configuration_error', { code: (error as { code?: string }).code })}Assess an action
Section titled “Assess an action”Call assess after your own checks, with the tag you chose for the moment
(journeys and tags) and the token the Web SDK produced
for this action.
// `ftid` is null after a configuration error: `result` is then undefined, and the action goes on.const result = await ftid?.assess({ tag: 'login', subjectRef: pseudonymOf(user), // a keyed pseudonym, never an email or tax ID eventContextToken: body.ftidToken ?? null, eventContextExpiresAt: body.ftidTokenExpiresAt ?? null,})result.kind |
Meaning | What your backend does |
|---|---|---|
ASSESSED |
result.assessment is the response |
Record it; your own policy decides |
INVALID_INPUT |
The request never left: result.issues names each field |
Fix the call; nothing was sent |
PROBLEM |
The API answered a catalogued Problem | Follow result.problem.category |
UNAVAILABLE |
Rate limit, outage, clock or timeout, after the allowed retries | Keep your decision; the request may have been stored |
UNEXPECTED_RESPONSE |
An answer outside the contract | Keep your decision and report the correlationId |
Every result carries severity (info, warn or error) for your logs, and every sent result
carries correlationId for support. Log identifiers and verdicts only: never the secret, the
token, the signature or the request body.
What the client does for you
Section titled “What the client does for you”- Validates the request with the same schema the API applies, and serializes the body once.
- Sends at most 3 attempts within 10 seconds, each with a 6.5-second timeout, above the
5-second server budget. The budget runs on a clock that never goes back, so a change of the
system clock cannot extend it, and the first attempt always starts. A retry resends the same
bytes with a new
X-FTID-Request-Id, the current timestamp and a new signature. - Retries only Problems the published catalog marks retryable and attempts whose outcome is
unknown, waiting
Retry-After(at least 1 second) after a429or503. A429,502,503or504without a Problem body, as a proxy may send, counts as an unknown outcome. - Drops an event context token that would expire before the first attempt ends, and assesses without it.
- When the API answers
event-context-invalidto a request that carried a token, assesses the event once more as a new operation without the token, with a new external event ID and idempotency key thatresult.operationcarries. That second operation keeps the instant of the action and spends only what is left of the call’s attempts and budget; without enough left, the refusal is the result. SetretryWithoutEventContext: falseto turn this off. - Returns
INVALID_INPUTinstead of throwing for an argument outside the documented types, such as an ISO string whereoccurredAtexpects aDate. - Reports each attempt, retry and result to an optional
onEventcallback, without the secret, the token, the signature or the body.
| Option | Default | Range |
|---|---|---|
apiOrigin |
https://api.fingertrackid.com |
An https: origin |
timeoutMs |
6500 | Per attempt |
budgetMs |
10000 | Up to 60000 |
maxAttempts |
3 | 1 to 5 |
retryWithoutEventContext |
true |
|
onEvent |
none | Exceptions it throws, and rejections of a promise it returns, are ignored |
Reconcile an unknown outcome
Section titled “Reconcile an unknown outcome”A sent result carries result.operation: the path, the exact body, its external ID and its
idempotency key. Store it with your own record when the outcome matters, and send it again with
the method of its kind: ftid.resend(operation) for an assessment,
ftid.resendCustomerAction(operation) or ftid.resendOutcome(operation) for feedback. Each sends
the same bytes with a new signature, as idempotency and retry
requires; never build a new body for the same action.
operation.body holds the event context token and subject_ref as sent: keep it with the
record, out of logs.
Report actions and outcomes
Section titled “Report actions and outcomes”They need the action:write and outcome:write capabilities and return the same result kinds,
with RECORDED and the receipt instead of ASSESSED. See feedback.
A LEGITIMATE or FRAUD_CONFIRMED outcome also needs the evidence fields: evidenceSource,
evidenceReference and evidenceConfidence.
await ftid?.recordCustomerAction(assessmentId, { action: 'ALLOWED', basis: 'CLIENT_POLICY', reasonCode: 'CLIENT_POLICY_RULE',})
await ftid?.recordOutcome(assessmentId, { truthStatus: 'PENDING', labelMaturity: 'PROVISIONAL', reasonCode: 'INITIAL_OBSERVATION',})Check the real credential
Section titled “Check the real credential”check sends one assessment in shadow mode, tagged integration_check and without a browser
token, and prints the result. The key_id comes from FTID_KEY_ID or, when that is unset, from
the integration file (--manifest <path>, or
fingertrackid.integration.json in the current directory) when it lists exactly one credential
that may assess; with several, set FTID_KEY_ID. The secret comes only from
FTID_HMAC_SECRET_HEX or standard input (at most 4,096 bytes, within 30 seconds), never from an
argument. FTID_API_ORIGIN, when set for
check, may name only the production API or a loopback test double.
--no makes npx run only the copy your project installed: it never downloads a package of the
same name from the registry, which matters when the secret is on standard input.
your-secret-manager read ftid-hmac-secret | FTID_KEY_ID=<key_id> npx --no fingertrackid-server checkOther languages
Section titled “Other languages”Implement the canonical string and check it against the public test vector before the first real call. An AI coding agent can compare its signer with the vector through the MCP server.