Module Patterns
This guide is specific to Web SDK 2.0. If you are still using 1.x, you can find documentation here. We strongly recommend upgrading - contact your Incode Representative for upgrade information.
Most SDK modules fall into one of five implementation patterns. Each pattern dictates how the manager state machine is shaped, which API methods exist, and what UI affordances you'll need to build (or which screens the SDK ships).
This page is the shared lifecycle reference. Per-module pages link here for the pattern they follow, then only document what's specific to that module — config shape, custom states, error codes — without repeating the lifecycle prose.
If you're skimming, the universal lifecycle section is the only thing every module shares. The five patterns below describe how that lifecycle is filled in.
Universal lifecycle
Every manager — regardless of pattern — exposes the same lifecycle methods:
| Method | Purpose |
|---|---|
load() | Start the state machine. Always call first. Idempotent within a flow. |
subscribe(callback) | Listen to state transitions. Returns an unsubscribe function. |
getState() | Current state synchronously, without subscribing. |
reset() | Return to initial state. Only valid from finished or error. |
stop() | Tear down resources (camera streams, timers, server polling). Always call before unmount. |
Every state has a status: string discriminator. Use TypeScript's discriminated-union narrowing:
manager.subscribe((state) => {
switch (state.status) {
case 'idle':
// initial state, before load()
break;
case 'finished':
// success — TypeScript knows the success-state shape here
manager.stop();
break;
}
});Terminal states. finished and closed are XState type: 'final' — the manager won't re-emit further updates from them, and reset() doesn't apply (it's only valid from finished or error). error is recoverable; call reset() to retry.
Lifecycle events. Every manager automatically emits moduleOpened when load() runs and moduleClosed when it finishes. Subscribe via subscribeEvent from @incodetech/core/events if you want to track them. See Dashboard Events.
1. Form-based modules
The user fills inputs, clicks submit, the server validates, and the flow advances. The simplest, most common pattern.
Modules using this pattern: Phone, Email, ID OCR, Consent, CURP Validation, Government Validation, Custom Fields, Watchlist for Business, Identity Reuse (with a candidate-selection variant).
State progression
flowchart LR
idle -->|load| inputting
inputting -->|submit| submitting
submitting --> finished
submitting -.->|error| error
When prefill is enabled, load transitions through an extra loadingPrefill state before reaching inputting.
For modules with OTP verification (Phone, Email), the submitting state expands into an OTP sub-loop:
flowchart LR
submitting --> sendingInitialOtp
sendingInitialOtp --> awaitingOtp
awaitingOtp -->|submitOtp| verifyingOtp
verifyingOtp --> finished
verifyingOtp -.->|wrong code| otpError
otpError -->|submitOtp| verifyingOtp
awaitingOtp -->|resendOtp| resendingOtp
resendingOtp --> awaitingOtp
For Identity Reuse, the inputting step is replaced by candidate-selection states (oneCandidateFound, multiCandidatesFound, noCandidatesFound) that present existing identity matches for the user to pick or skip.
Common API methods
| Method | Purpose |
|---|---|
load() | Start the machine. Fetches prefill data if prefill is enabled in config. |
set<Field>(value, isValid?) | Update a form field. Pass isValid so the manager knows when submit can be enabled. |
submit() | Submit the form. Transitions to submitting. |
setOtpCode(code) / submitOtp(code) | OTP-specific. setOtpCode updates without submitting; submitOtp updates and submits. |
resendOtp() | Re-request the OTP. Only valid when state.canResend === true. |
back() | Return to a previous step (e.g. awaitingOtp → inputting). |
reset() | After finished or error, go back to idle. |
Skeleton
const manager = createPhoneManager({
config: { otpVerification: true, otpExpirationInMinutes: 5, prefill: false },
});
manager.subscribe((state) => {
switch (state.status) {
case 'inputting':
// render input form, hook setPhoneNumber + submit
break;
case 'awaitingOtp':
// render OTP entry form, hook submitOtp + resendOtp
break;
case 'finished':
manager.stop();
break;
}
});
manager.load();2. Camera-capture modules
Real-time camera feed with on-device ML detection, capture (auto or manual), upload, and server-side processing. The SDK handles the camera and ML; you handle the layout and instructions.
Modules using this pattern: Selfie, ID Capture, Document Capture, Authentication (re-auth via selfie).
State progression
flowchart LR
idle -->|load| loading
loading --> tutorial
tutorial -->|nextStep| permissions
permissions -->|granted| capture
capture -->|upload done| processing
processing -->|success| finished
capture -->|retryCapture| capture
capture -->|close| closed
The capture state has rich internal sub-state exposed as flat properties on the state object:
| Property | Meaning |
|---|---|
stream | The MediaStream to bind to your <video> element. |
captureStatus | Sub-state: initializing, detecting, capturing, uploading, uploadError, success. |
detectionStatus | Per-frame feedback: noFace, tooClose, blur, glare, manualCapture, etc. Use to drive on-screen guidance. |
attemptsRemaining | Capture attempts left before the module errors out. |
Specific modules add their own properties (Selfie has assistedOnboarding; ID adds currentMode: 'front' | 'back' and counterValue for the auto-capture countdown; Document Capture has multi-page state). Per-module pages document the diffs.
Common API methods
| Method | Purpose |
|---|---|
load() | Start. Goes to tutorial if showTutorial: true, else straight to permissions. |
nextStep() | Advance from tutorial → permissions. For ID capture, also advance from capture.success → frontFinished / processing. |
requestPermission() | Prompt the user for camera access. Only effective in permissions.idle / permissions.learnMore. |
goToLearnMore(), back() | Help-screen navigation when permission is denied. |
capture() | Manual capture. Only effective when detectionStatus === 'manualCapture'. |
retryCapture() | Retry after upload error or expiration. |
close() | User dismisses; transitions to closed (terminal — see Handling cancellation). |
WASM requirement
Camera-capture modules need WASM for face/document detection and quality scoring. Preload via setup({ wasm: { pipelines: ['selfie', 'idCapture'] } }). See WASM Configuration.
Skeleton
const manager = createSelfieManager({
config: { showTutorial: true, autoCaptureTimeout: 10, /* ... */ },
});
manager.subscribe((state) => {
switch (state.status) {
case 'tutorial':
// render tutorial; nextStep() to advance
break;
case 'permissions':
// prompt; requestPermission() on user click
break;
case 'capture':
// bind state.stream to a <video> element with playsinline + muted + autoplay
// render guidance based on state.detectionStatus
if (state.detectionStatus === 'manualCapture') {
// show a tap-to-capture button → manager.capture()
}
break;
case 'processing':
// server-side ML processing — show a spinner
break;
case 'finished':
manager.stop();
break;
case 'closed':
// user cancelled — see Handling cancellation
break;
}
});
manager.load();3. Backend-process modules
No live user interaction. The module runs a server-side check using data already collected (by the session, by previous modules, or from the user's device) and reports a result. Often runs as a background step in an orchestrated flow rather than standalone.
Modules using this pattern: Face Match, Antifraud, Watchlist, Custom Watchlist, Geolocation, Cross-Document Data Match.
State progression
flowchart LR
idle -->|load| loading
loading --> result
result -->|continue| finished
loading -.->|error| error
Some modules add a transition state (Face Match has animating for its result reveal animation) but the core shape is minimal. The result data is on the state when status === 'result'.
Common API methods
| Method | Purpose |
|---|---|
load() | Trigger the backend call. Result lands in state when status === 'result'. |
continue() / submit() | Acknowledge the result and finish. Naming varies by module. |
reset() | Return to idle after finished or error. |
These modules typically need no user input. They're often invoked from an orchestrated flow that calls load() automatically when the step starts.
Skeleton
const manager = createFaceMatchManager({ config: {} });
manager.subscribe((state) => {
switch (state.status) {
case 'result':
// render the match result (state.result.action / confidence)
// call manager.continue() to acknowledge
break;
case 'finished':
manager.stop();
break;
}
});
manager.load();4. Document-signing modules
Render documents to be signed, collect consent checkboxes, capture a signature (handwritten on canvas or accept-via-button), submit. Used for legal/compliance signing flows.
Modules using this pattern: Signature (handwritten on canvas), Electronic Signature (multi-document PDF flow, plus AE and QE variants on the same page).
AE / QE are variants.
createAeSignatureManagerandcreateQeSignatureManagerare thin wrappers aroundcreateElectronicSignatureManagerthat hardcodevariant: 'ae'or'qe'— same state machine, same API, different consent keys (AE_CONSENT_KEYSvsQE_CONSENT_KEYS). One mental model covers all three.
State progression
flowchart LR
loading --> reviewing
reviewing -->|sign| signing
signing --> uploading
uploading --> processing
processing --> finished
reviewing -->|close| closed
signing -.->|error| error
The handwritten signature module is structurally simpler — it skips the multi-document review and goes straight from idle → capture (canvas drawing) → submitting → finished.
Common API methods
| Method | Purpose |
|---|---|
load() | Fetch the documents to sign. |
setConsent(name, checked) | Toggle a consent checkbox. |
confirmFile(), selectFile(), replaceFile(), viewDocument() | Document review interactions (Electronic Signature). |
setSignatureValid(isValid), submit(image) | Canvas signature interactions (Signature). |
sign() | Trigger signing once consents are checked and the document is confirmed. |
finish(), retry(), close() | Terminal transitions. |
Skeleton
import { createElectronicSignatureManager } from '@incodetech/core/electronic-signature';
import { areAllConsented } from '@incodetech/core/electronic-signature';
const manager = createElectronicSignatureManager({
config: { variant: 'aes', uploadDocument: false, downloadDocument: true },
});
manager.subscribe((state) => {
switch (state.status) {
case 'reviewing':
// render documents + consent checkboxes
// setConsent on each checkbox change
// when areAllConsented(state.consents), enable Sign button → manager.sign()
break;
case 'signing':
// collect signature, then manager.sign()
break;
case 'finished':
manager.stop();
break;
}
});
manager.load();5. Composite / orchestrator modules
These don't capture data themselves — they sequence other modules based on a flow definition (from the dashboard or a workflow definition). The state describes which sub-module is currently active, and your code instantiates that sub-module and drives its lifecycle.
Modules using this pattern: Flow / Orchestrated Flow, Workflow, eKYC, eKYB.
State progression
flowchart LR
idle -->|load| loading
loading --> ready
ready -->|completeModule| ready
ready -->|all steps done| finished
ready -.->|error| error
The ready state cycles back to itself once per sub-module: the orchestrator emits a new ready snapshot with an updated currentStep each time you call completeModule(). The ready state exposes:
| Property | Meaning |
|---|---|
flow | The full flow configuration from the backend. |
steps | Ordered list of step keys. |
currentStep | Step key currently active (e.g. 'PHONE', 'ID', 'SELFIE'). |
currentStepIndex | Position in steps. |
config | Sub-module config for the current step. Type is unknown; cast via FlowModuleConfig[<step>]. |
moduleState | Sub-module state if the orchestrator is also driving the sub-module. |
As each sub-module finishes, you call flowManager.completeModule() and the orchestrator advances to the next step.
Common API methods
| Method | Purpose |
|---|---|
load() | Fetch flow configuration from the backend. |
completeModule() | Mark the current sub-module as complete; advance to next. |
getModuleConfig(stepKey) / isModuleEnabled(stepKey) | Query the resolved flow definition. |
reset() | Return to idle. |
Where to learn more
Headless Mode → Orchestrated Flow Manager is the canonical reference. Module Registration covers how to wire sub-module state machines into the orchestrator.
See also
- Headless Mode: full per-module reference for the four documented modules
- Individual Modules: catalog of every module in the SDK
- WASM Configuration: required for camera-capture modules
- Dashboard Events: automatic and custom event tracking
- Troubleshooting: common integration issues
Updated about 5 hours ago
