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:

MethodPurpose
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

MethodPurpose
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. awaitingOtpinputting).
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:

PropertyMeaning
streamThe MediaStream to bind to your <video> element.
captureStatusSub-state: initializing, detecting, capturing, uploading, uploadError, success.
detectionStatusPer-frame feedback: noFace, tooClose, blur, glare, manualCapture, etc. Use to drive on-screen guidance.
attemptsRemainingCapture 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

MethodPurpose
load()Start. Goes to tutorial if showTutorial: true, else straight to permissions.
nextStep()Advance from tutorialpermissions. For ID capture, also advance from capture.successfrontFinished / 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

MethodPurpose
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. createAeSignatureManager and createQeSignatureManager are thin wrappers around createElectronicSignatureManager that hardcode variant: 'ae' or 'qe' — same state machine, same API, different consent keys (AE_CONSENT_KEYS vs QE_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 idlecapture (canvas drawing) → submittingfinished.

Common API methods

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

PropertyMeaning
flowThe full flow configuration from the backend.
stepsOrdered list of step keys.
currentStepStep key currently active (e.g. 'PHONE', 'ID', 'SELFIE').
currentStepIndexPosition in steps.
configSub-module config for the current step. Type is unknown; cast via FlowModuleConfig[<step>].
moduleStateSub-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

MethodPurpose
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