Skip to Content
Web SDKReferenceFingerprint reader

Fingerprint reader client

Drives a USB fingerprint reader (the contactful path) via the SladeID companion application. Biometric actions (enroll / verify / search) are dispatched through the middleware orchestrator; only device discovery and testCapture touch the local hardware service. See Use a USB fingerprint reader for the companion-app prerequisite, install links, and worked examples of enroll / verify / search.

Get one from a SladeID instance — sladeId.reader is a getter that needs no extra config and reuses the middleware auth and orchestrator:

const reader = sladeId.reader;

Or construct a standalone client. middlewareUrl is required, and getToken returns a middleware (Keycloak) bearer (not a local-HWS token). localServiceUrl (default http://localhost:18065) points at the local /status + /test service:

import { createFingerprintReader, FingerprintReaderClient } from '@sladeid/slade-id-sdk'; const reader: FingerprintReaderClient = createFingerprintReader({ getToken: () => Promise.resolve('<middleware bearer token>'), middlewareUrl: 'https://middleware.example.com', // integratorId: 'my-integrator', // optional // localServiceUrl: 'http://localhost:18065', // optional // workstationId: 'SQM-MACHINE-ID', // optional, but only together // deviceId: 'scanner-serial', // with each other — see below });

getToken is called on demand and again after a 401, so token rotation is transparent. The bearer is validated once, at the middleware. Throwing inside getToken surfaces as a ReaderAuthError to the caller.

Supplying workstationId and deviceId together pins the dispatch target: the client skips loopback discovery entirely and names that workstation on every orchestrator dispatch, which is what lets a host with no companion application drive enroll / verify / search. Supplying only one of the pair throws a TypeError at construction, because half a pinned target would silently fall back to discovery for the missing half. reader.status then stays empty and no device events fire. See Pin a dispatch target — it is not a no-hardware mode.

Methods

MethodReturnsNotes
reader.statusReaderStatusSynchronous snapshot from the last background poll.
refreshStatus()Promise<ReaderStatus>Force an immediate /status fetch (e.g. before a device picker).
enroll(input)Promise<EnrollmentResult>Register input.enrollee at input.position. Dispatched via the orchestrator.
verify(input)Promise<VerificationResult>1:1 match against the claimed enrollee. Dispatched via the orchestrator.
search(input?)Promise<SearchResult>1:N identify against the enrolled corpus. Optional { position } so the service applies that finger group’s tuned thresholds. Dispatched via the orchestrator.
fetchEnrolled(enrollee)Promise<EnrolledFingerprints>List a subject’s enrolled positions. Direct middleware lookup; no hardware/device needed.
testCapture(opts?)Promise<TestCaptureResult>Grab one frame; { normalized: true } for the preprocessed template. Talks directly to the local hardware service.
createEnrollmentSession(input)EnrollmentSessionEvent-driven wrapper around enroll.
createVerificationSession(input)VerificationSessionEvent-driven wrapper around verify.
createSearchSession(input?)SearchSessionEvent-driven wrapper around search; takes the same optional SearchInput.
dispose()voidStop polling, drop listeners. Subsequent actions throw.

Inputs

EnrollmentInput and VerificationInput share the same shape:

FieldTypeRequiredNotes
enrolleestringyesSubject identifier (e.g. beneficiary code).
positionFingerPosition (0–10)yes15 right hand, 610 left hand; 0 = minor / any.
isMinorbooleannoApply relaxed quality checks.
attachmentsRecord<string, string>noFree-form metadata forwarded to the backend (e.g. encounter ids).

SearchInput is separate, and every field is optional — the capture itself is the query, so a bare search() is still valid:

FieldTypeRequiredNotes
positionFingerPosition (0–10)noThe finger the subject is about to present. Defaults to 0 (“any finger”) server-side.

Naming position is worth doing whenever you know it. The service resolves its matching thresholds per finger group, so telling it which finger is coming lets it apply the values tuned for that group instead of the untuned global ones — measurably fewer false negatives on thumbs. There is no isMinor or attachments on SearchInput: the backend’s search finaliser reads neither, so the SDK does not offer fields it would silently drop.

Results

interface EnrollmentResult { template?: string; // now typically undefined — the orchestrated finaliser returns metadata under `raw`, not a device template verifiedPositions?: number[]; // positions the service confirmed raw: Record<string, unknown>; // backend finaliser payload: { fingerprint_id, enrollee, position, device_id } } interface VerificationResult { matched: boolean; // false is a normal outcome, not an error matchLogId?: string; // present when matched; persist for audit verifiedPositions?: number[]; } interface SearchResult { found: boolean; // true only when an enrollee matched enrollee?: string; // the matched subject id (single best match, not a ranked list) similarity?: number; // 0..1 confidence when reported — threshold on THIS matchScore?: number; // the raw matcher score; NOT the same scale as `similarity` errorCode?: string; // set when the search FAULTED rather than found nobody raw: Record<string, unknown>; // every server field, verbatim } interface EnrolledFingerprints { verified: number[]; // positions already enrolled — safe to skip on resume nonVerified: number[]; // captured but not yet confirmed by the backend total: number; // total fingerprint records on file enrollmentStatus: 'fully_enrolled' | 'partially_enrolled' | 'not_enrolled'; notes?: string; // enrollment notes, when present raw: Record<string, unknown>; }

Reading a SearchResult

similarity and matchScore are not on the same scale and are deliberately kept as separate fields rather than merged. similarity is a 0..1 confidence; matchScore is whatever the matcher that decided the hit reported — sometimes a 0..1 similarity, sometimes an unbounded validator score. Threshold on similarity; record matchScore for audit. Both are promoted from the server payload only when they are finite numbers, so a NaN on the wire arrives as undefined rather than as a comparison that quietly evaluates false. Treat undefined as unknown, never as zero — a middleware revision that reports no scores still returns real matches.

errorCode (for example VALIDATOR_UNAVAILABLE) is the important one. A result with found: false and an errorCode means the search could not complete: it is a fault, not a confident “nobody matched”. The subject may well be enrolled. Branch on it before you show a verdict, and tell the operator to retry — never that the subject is not enrolled.

Reading an EnrolledFingerprints

fetchEnrolled(enrollee) is a direct middleware lookup against the enrolled_fingerprints endpoint (not orchestrated, not the local hardware service) — it selects no device and resolves even with no scanner attached. Use it to resume a paused multi-finger enrollment: skip the verified positions and prompt only for what’s missing.

enrollmentStatus has three members, and 'not_enrolled' is the one worth handling explicitly: it means the middleware has no records at all for the subject, which is what its empty-body response for an unknown subject decodes to. Previously that was indistinguishable from 'partially_enrolled', so a never-enrolled subject and a part-finished enrolment looked the same. Start a fresh enrolment on 'not_enrolled'; resume from verified / nonVerified on 'partially_enrolled'.

ReaderStatus

interface ReaderStatus { connected: boolean; // service reachable isAuthed: boolean; // service accepted the token devices: ReaderDevice[]; // scanners currently visible workstationId?: string; version?: string; lastError?: HwsError; // most recent poll failure; cleared on next success }

The action methods auto-select the first device, so most integrators never read devices — it’s exposed for rendering a device picker.

Client events

connected, disconnected (HwsError), device-attached (ReaderDevice), device-detached (ReaderDevice), poll-failed (HwsError).

poll-failed fires whenever a background /status poll fails — including the very first poll before the client ever went connected. It is de-duped by error code, so a stuck failure (e.g. an empty token) emits once rather than every 3s. The same error is also available synchronously on reader.status.lastError, which is cleared on the next successful poll.

Session events

Sessions are single-shot: call start() once, cancel() to abort an in-flight request. All three emit ready (just before the request goes out — cue “place finger”) and scanning (once in flight — cue spinner), then a terminal event. The terminal events differ per session type — note that verification and search split the outcome so a non-match never looks like an error:

SessionSuccess terminalNon-match terminalFailure terminalResult type
EnrollmentSessioncapturederrorEnrollmentResult
VerificationSessioncapturedmismatcherrorVerificationResult
SearchSessionfoundnot-founderrorSearchResult

error is reserved for transport, auth, device, and server failures (HwsError subclasses). A biometric non-match is delivered through mismatch / not-found, not error.

Errors

All reader errors extend HwsError (which extends SladeIDError) and carry code plus an optional correlationId.

ClassCode
ConnectionErrorREADER_CONNECTION_FAILED
TimeoutErrorREADER_TIMEOUT
ReaderAuthErrorREADER_AUTH_FAILED
DeviceUnavailableErrorREADER_DEVICE_UNAVAILABLE
ReaderValidationErrorREADER_VALIDATION_FAILED
DuplicateEnrollmentErrorREADER_DUPLICATE_TEMPLATE
ReaderServerErrorREADER_SERVER_ERROR
BiometricMismatchErrorREADER_BIOMETRIC_MISMATCH

DuplicateEnrollmentError is thrown by enroll() when the subject already has a live record on that finger — the backend keeps one active record per enrollee + position + source and refuses to store another. Re-capturing will not overwrite it; enroll a different position, or have the existing record deactivated first.

It subclasses ReaderValidationError, so existing instanceof ReaderValidationError handling keeps working unchanged. What it adds is a stable code of READER_DUPLICATE_TEMPLATE instead of the generic READER_VALIDATION_FAILED, so you can single the case out without regex-matching the message text — which is what integrators had to do before, and which broke the moment the backend reworded the detail string.

BiometricMismatchError is thrown only by rejection-style helpers; the built-in verify() resolves with { matched: false } instead. verify() does, however, throw for operational failures: a ReaderValidationError (READER_VALIDATION_FAILED) when there is no enrolled template to match against, and a ReaderServerError (READER_SERVER_ERROR) when the validator or database is unavailable. A genuine no-match is still { matched: false }, never a throw.