Skip to main content

Using the JavaScript / React SDK

The @attestid/sdk package provides JavaScript and React APIs for integrating AttestID identity verification into web applications.

Features include:

  • Document capture (camera or file upload)
  • Client-side photo quality validation
  • Face liveness detection (AWS Rekognition, via Amplify)
  • Verification result retrieval

React applications can use either the provided UI components or the lower-level SDK APIs.


1. Install

npm install @attestid/sdk

React support is a separate entry point (@attestid/sdk/react). react / react-dom (≥ 18) are optional peer deps — only needed if you use the React entry point.


2. Environment setup

Your API credentials must remain on the backend.

Do not expose apiKey or accessKeyId in client-side environment variables such as VITE_*, NEXT_PUBLIC_*, or REACT_APP_*.

Backend .env (server-only):

ATTESTID_API_URL=https://api.attestid.io
ATTESTID_API_KEY=sk_live_your_key_here
ATTESTID_ACCESS_KEY_ID=ak_live_your_access_key_id_here

Frontend .env (safe to expose — no secrets):

VITE_API_URL=https://api.attestid.io

Runtime enforcement

The SDK validates the supplied configuration at construction time and refuses to run with insecure client-side API key usage:

  • apiUrl must be https:// (plain http:// is only allowed against localhost / 127.0.0.1 / ::1, for local development).
  • A config containing apiKey / accessKeyId (server mode) is rejected outside a real Node.js process — a bundler shipping that config to a browser, React Native, or an edge runtime throws AttestIDError with code INSECURE_CONFIG.

3. Backend endpoint

Your frontend requests an SDK token from your backend. Your backend authenticates with AttestID using the API credentials and returns the SDK token to the client.

// your-backend/routes/attestid-session.ts (Node/Express — never shipped to the browser)
app.post('/api/attestid-session', async (req, res) => {
const response = await fetch(`${process.env.ATTESTID_API_URL}/v1/kyc/session`, {
method: 'POST',
headers: {
'API-Key': process.env.ATTESTID_API_KEY!,
'Access-Key-Id': process.env.ATTESTID_ACCESS_KEY_ID!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: req.body.userId }),
});
res.status(response.status).json(await response.json());
});

4. Configuration

import { AttestID } from '@attestid/sdk';

const client = new AttestID({
apiUrl: import.meta.env.VITE_API_URL, // no trailing slash
// Never put apiKey/accessKeyId here — call your own backend instead.
getSdkToken: (userId) =>
fetch('/api/attestid-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
}).then(r => r.json()),
});

The SDK supports two configuration modes:

ConfigWhere it's usedFields
Browser configFrontend applicationsapiUrl, getSdkToken, optional identityPoolId / region
Server configBackend servicesapiUrl, apiKey, accessKeyId, optional identityPoolId / region

The SDK automatically reuses valid SDK tokens (refreshing 15 seconds before expiry) and requests a new one when needed. Applications do not need to manage token expiration directly.


5. Path A — Drop-in component (fastest)

AttestIDVerifier renders the entire flow: consent screen → document capture with quality checks → liveness detection → result screen.

// main.tsx
import '@attestid/sdk/styles.css';
import { AttestID } from '@attestid/sdk';
import { AttestIDVerifier } from '@attestid/sdk/react';

const client = new AttestID({
apiUrl: import.meta.env.VITE_API_URL,
getSdkToken: (userId) =>
fetch('/api/attestid-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
}).then(r => r.json()),
});

export default function KYCPage() {
return (
<AttestIDVerifier
client={client}
defaultUserId="user-123"
onVerified={result => {
console.log('Verified:', result.verified);
console.log('Document:', result.document);
// update your DB, redirect, etc.
}}
onError={err => console.error('KYC error:', err)}
/>
);
}

AttestIDVerifier props

PropTypeRequiredDescription
clientAttestIDThe configured SDK client
defaultUserIdstringPre-fills the User ID field
onVerified(result: KYCResult) => voidCalled when verification completes (pass or fail)
onError(error: string) => voidCalled on an API or liveness error

6. Path B — Custom UI with the useAttestID hook

The useAttestID hook exposes the verification state and SDK operations required to build a custom interface.

import { AttestID, checkPhotoQuality } from '@attestid/sdk';
import { useAttestID, LivenessDetector } from '@attestid/sdk/react';
import type { IdType } from '@attestid/sdk';

const client = new AttestID({ /* same config as above */ });

export default function KYCPage() {
const {
state, error, qualityIssues,
livenessToken, livenessCredentials, result, isLoading,
startVerification, completeVerification, reset,
} = useAttestID(client);

async function handleUpload(file: File, userId: string, idType: IdType) {
const quality = await checkPhotoQuality(file, idType);
if (!quality.ok) { alert(quality.issues.join('\n')); return; }

// consentGivenAt must be the real timestamp of the user's consent action —
// AttestIDVerifier (Path A) captures this for you automatically.
await startVerification({ userId, idType, idPhoto: file, consentGivenAt: new Date() });
}

if (state === 'idle' || state === 'quality_check' || state === 'uploading') {
return (
<div>
{error && <p>{error}</p>}
{qualityIssues.map(i => <p key={i}>{i}</p>)}
<input type="file" accept="image/*" onChange={e => {
const file = e.target.files?.[0];
if (file) handleUpload(file, 'user-123', 'national_id');
}} />
{isLoading && <p>Working…</p>}
</div>
);
}

if (state === 'liveness' && livenessToken && livenessCredentials) {
return (
<LivenessDetector
sessionId={livenessToken}
credentials={livenessCredentials}
onComplete={() => completeVerification()}
onError={err => console.error(err)}
/>
);
}

if (state === 'done' && result) {
return (
<div>
<h2>{result.verified ? '✓ Verified' : '✗ Not verified'}</h2>
<pre>{JSON.stringify(result, null, 2)}</pre>
<button onClick={reset}>Verify another</button>
</div>
);
}

return null;
}

Flow state machine

startVerification(options)


[quality_check] ← checkPhotoQuality runs client-side

pass │ fail ──► state = 'error', qualityIssues populated, no API call made


[uploading] ← startVerification + getLivenessCredentials run in parallel


[liveness] ← render LivenessDetector

▼ completeVerification()
[completing]


[done] ← result available

useAttestID return values

PropertyTypeDescription
stateVerifyState'idle' | 'quality_check' | 'uploading' | 'liveness' | 'completing' | 'done' | 'error'
errorstring | nullLast API error message
qualityIssuesstring[]Human-readable photo quality problems
livenessTokenstring | nullRaw session ID for LivenessDetector
livenessCredentialsLivenessCredentials | nullCredentials for the liveness check
resultKYCResult | nullFinal verification result
isLoadingbooleantrue during quality_check, uploading, completing
startVerification(options) => Promise<void>Starts the flow
completeVerification(deviceId?) => Promise<void>Completes after liveness passes
reset() => voidResets state back to idle

startVerification options

await startVerification({
userId: 'user-123',
idType: 'national_id', // 'national_id' | 'passport' | 'drivers_licence'
idPhoto: file, // File | Blob
consentGivenAt: new Date(), // required — real user consent timestamp
processingPurpose: 'identity_verification', // optional, this is the default
});

LivenessDetector props

PropTypeRequiredDescription
sessionIdstringRaw liveness session ID (already stripped of provider prefix)
credentialsLivenessCredentialsFrom client.getLivenessCredentials(userId)
onComplete() => voidCalled when the Rekognition analysis finishes
onError(error: unknown) => voidCalled on a liveness SDK error

7. Photo quality

Photo quality validation can be performed before upload using checkPhotoQuality().

import { checkPhotoQuality } from '@attestid/sdk';

const result = await checkPhotoQuality(file, 'national_id');
// result.ok true when all checks pass
// result.issues human-readable strings, safe to show directly to users

Checks performed client-side:

  • File size and JPEG/PNG signature (magic bytes — not filename or Content-Type)
  • Blur (Laplacian variance) and brightness (under/overexposed)
  • Edge cut-off / not-a-document detection — flags images with no printed-text structure at all
  • Document type mismatch — flags an obvious aspect-ratio mismatch (e.g. a passport spread uploaded as national_id, or vice versa)
  • Passport data-page detection/blur — region-aware; only evaluates the data page in a two-page spread, ignoring the opposite page
  • Face count on the document photo (best-effort, Chromium-only, via the browser's Shape Detection API) — flags zero or multiple faces; skipped silently where unsupported

This is intentionally not exhaustive — checks that require real OCR or a facial-attribute model (blurry field reads, sunglasses, closed eyes) can only be determined by the backend pipeline once the photo is uploaded.


8. Error handling

import { AttestIDError, AuthError, KYCError, CameraError, ValidationError } from '@attestid/sdk';

try {
await client.startVerification({ /* ... */ });
} catch (e) {
if (e instanceof AuthError) console.error('Invalid key or expired sdk_token', e.status); // 401
if (e instanceof KYCError) console.error('KYC failed', e.code, e.status);
if (e instanceof CameraError) console.error('Camera error', e.message);
if (e instanceof ValidationError) console.error('Rejected before upload', e.message); // e.g. bad magic bytes
}

Every error extends AttestIDError (message, code, optional status).

result.failure_reasonMeaning
liveness_failedCould not confirm a live person
face_mismatchSelfie didn't match the ID photo
document_expiredID document is past its expiry date
fraud_detectedSpoofing or fraudulent content detected
ocr_failedCould not extract data from the ID image
duplicate_identityPerson already verified under a different account

9. Camera utilities

import { openCameraSession, DOC_ASPECT } from '@attestid/sdk';

const session = await openCameraSession('national_id');
videoElement.srcObject = session.stream;
if (session.torchSupported) await session.setTorch(true);
const file = await session.captureFrame(frameOverlayElement, videoElement);
session.stop();

DOC_ASPECT gives the correct overlay aspect ratio per document type (national_id / drivers_licence: 1.586 — ISO 7810 ID-1; passport: 1.420 — ID-3 data page).


10. Minimal full example

// main.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import '@attestid/sdk/styles.css';
import App from './App';

createRoot(document.getElementById('root')!).render(<StrictMode><App /></StrictMode>);
// App.tsx — browser, no secrets
import { AttestID } from '@attestid/sdk';
import { AttestIDVerifier } from '@attestid/sdk/react';

const client = new AttestID({
apiUrl: import.meta.env.VITE_API_URL,
getSdkToken: (userId) =>
fetch('/api/attestid-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
}).then(r => r.json()),
});

export default function App() {
return (
<AttestIDVerifier
client={client}
defaultUserId="user-123"
onVerified={result => console.log('KYC complete', result)}
onError={err => console.error('KYC error', err)}
/>
);
}
// server.ts — your backend, holds the real API key
app.post('/api/attestid-session', async (req, res) => {
const response = await fetch(`${process.env.ATTESTID_API_URL}/v1/kyc/session`, {
method: 'POST',
headers: {
'API-Key': process.env.ATTESTID_API_KEY!,
'Access-Key-Id': process.env.ATTESTID_ACCESS_KEY_ID!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: req.body.userId }),
});
res.status(response.status).json(await response.json());
});

Demo example

See JavaScript/React Demo App for a complete working example.