Skip to main content

Build a custom UI

Headless delivery gives your team full visual and interaction control. Your frontend also becomes responsible for state rendering, camera UX, accessibility, retries and recovery. Assayra remains authoritative for step order, protected evidence and the final decision.

Prerequisites

  1. Complete the Web SDK installation.
  2. Implement the server-only application creation route.
  3. Return the opaque verification token only to the authenticated customer who owns the journey.
  4. Register and test the final webhook.
  5. Review every enabled workflow and its possible steps.

1. Start or resume the session

Create AssayraApplicant once per journey, then read the current state before rendering:

import { AssayraApplicant } from "@assayra/sdk-web";

const applicant = new AssayraApplicant({
baseUrl: window.appConfig.assayraOrigin,
verificationToken,
});

const session = await applicant.status<{
expiresAt: string;
applicationType: "individual" | "business";
journeySteps: string[];
privacyNoticeVersion: string;
privacyNoticeUrl: string;
application: {
id: string;
status: string;
currentStep: string;
completedSteps: string[];
};
}>();

Call status() after refresh, network recovery and every acknowledged step. Render only the server-returned currentStep; workflows are not guaranteed to share one fixed order.

switch (session.application.currentStep) {
case "identity":
return showIdentityForm();
case "business":
return showBusinessAndOwnersForm();
case "document":
return showDocumentCapture();
case "liveness":
return showProtectedFaceCapture();
case "ecdd":
return showEnhancedDueDiligence();
case "submit":
return showConsent(session.privacyNoticeUrl);
default:
return showPendingOrSupportState();
}

2. Submit identity or business data

await applicant.submitIdentity({
givenName,
familyName,
dateOfBirth: "1992-06-14",
nationality: "SG",
email,
address,
});

Validate for usability in the browser, but display field/server errors from the API as authoritative. Do not silently transform names, nationality or dates.

3. Upload the document

  1. Ask for the document type and country if your workflow requires them.
  2. Prefer direct camera capture on mobile and preserve the original bytes.
  3. Validate basic file type/size before upload for quick feedback.
  4. Show upload progress and keep the screen resumable.
  5. Call uploadDocument once; after a timeout, refresh session state before retrying.
await applicant.uploadDocument({
bytes: selectedFile,
fileName: selectedFile.name,
});

Do not crop, enhance, watermark or repeatedly recompress protected evidence unless the API contract explicitly requires it. Never store the file in local storage, analytics or session replay.

4. Run guided automatic face capture

  1. Request browser camera permission from a clear user action.
  2. Start the front-facing camera in a secure HTTPS context.
  3. Request a fresh livenessChallenge() immediately before capture.
  4. Send reduced JPEG preview observations for positioning guidance.
  5. Show the returned guidance; do not reveal internal thresholds.
  6. After stable, challenge-correct observations, automatically retain the required full-quality frames.
  7. Submit exactly three fresh frames and their monotonic timestamps.
  8. Stop every media track when leaving the screen.
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "user", width: { ideal: 1280 } },
audio: false,
});

video.srcObject = stream;
await video.play();

const challenge = await applicant.livenessChallenge();
const observation = await applicant.observeLiveness({
nonce: challenge.nonce,
frame: reducedPreviewJpeg,
});

showGuidance(observation.guidance);

An observation only says whether the current frame is usable for guided capture. It does not mean liveness or likeness passed. The server evaluates the final evidence against the document-bound portrait and workflow policy.

Always clean up:

for (const track of stream.getTracks()) track.stop();
video.srcObject = null;

Display the exact privacyNoticeUrl returned by the session and record an affirmative action. Then submit the matching version:

await applicant.submit(session.privacyNoticeVersion, true);

Never hard-code a privacy-notice version or preselect consent. If the version changes while the page is open, refresh state and show the current notice before submission.

6. Complete asynchronously

After submission, show Verification received or Checks in progress. Do not let frontend code create an approval state. The backend must verify the webhook, fetch final application state and update your customer record.

Required recovery states

Your UI must include clear paths for:

  • camera permission denied or no camera;
  • no face, multiple faces, poor framing or low light;
  • unsupported/blurred/glare-obscured document;
  • upload timeout or offline transition;
  • expired/revoked invitation or challenge;
  • step already completed in another tab/device;
  • accessible retry, low-bandwidth or assisted route returned by policy; and
  • pending manual review.

Recovery may change capture mode or hand off to assistance. It can never skip a required check or manufacture an approval.

Accessibility and test matrix

  • Provide programmatic labels, keyboard operation, visible focus and announced status updates.
  • Do not encode status only with colour or motion.
  • Respect reduced motion and text scaling.
  • Test screen readers and browser zoom.
  • Test current iOS Safari, Android Chrome and supported desktop browsers.
  • Test slow/unstable networks, background/resume, denied permissions and camera switching.
  • Run every country/document/workflow variant enabled for the tenant.

For teams without this test capacity, use the drop-in hosted mode.