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

Server SDK for Node.js

Live · shadow modeNode.js 24.16+

@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.

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.

Terminal
npm install --save-exact @fingertrackid/server@0.1.2
npx --no fingertrackid-server self-check

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.

ftid.ts
import { createFtidServerClient, type FtidServerClient } from '@fingertrackid/server'
let ftid: FtidServerClient | null = null
try {
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 })
}

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.

login.ts
// `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.

  • 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 a 429 or 503. A 429, 502, 503 or 504 without 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-invalid to 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 that result.operation carries. 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. Set retryWithoutEventContext: false to turn this off.
  • Returns INVALID_INPUT instead of throwing for an argument outside the documented types, such as an ISO string where occurredAt expects a Date.
  • Reports each attempt, retry and result to an optional onEvent callback, 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

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.

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.

feedback.ts
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 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.

Terminal
your-secret-manager read ftid-hmac-secret | FTID_KEY_ID=<key_id> npx --no fingertrackid-server check

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.