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:
apiUrlmust behttps://(plainhttp://is only allowed againstlocalhost/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 throwsAttestIDErrorwith codeINSECURE_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:
| Config | Where it's used | Fields |
|---|---|---|
| Browser config | Frontend applications | apiUrl, getSdkToken, optional identityPoolId / region |
| Server config | Backend services | apiUrl, 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
| Prop | Type | Required | Description |
|---|---|---|---|
client | AttestID | ✓ | The configured SDK client |
defaultUserId | string | Pre-fills the User ID field | |
onVerified | (result: KYCResult) => void | Called when verification completes (pass or fail) | |
onError | (error: string) => void | Called 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
| Property | Type | Description |
|---|---|---|
state | VerifyState | 'idle' | 'quality_check' | 'uploading' | 'liveness' | 'completing' | 'done' | 'error' |
error | string | null | Last API error message |
qualityIssues | string[] | Human-readable photo quality problems |
livenessToken | string | null | Raw session ID for LivenessDetector |
livenessCredentials | LivenessCredentials | null | Credentials for the liveness check |
result | KYCResult | null | Final verification result |
isLoading | boolean | true during quality_check, uploading, completing |
startVerification | (options) => Promise<void> | Starts the flow |
completeVerification | (deviceId?) => Promise<void> | Completes after liveness passes |
reset | () => void | Resets 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
| Prop | Type | Required | Description |
|---|---|---|---|
sessionId | string | ✓ | Raw liveness session ID (already stripped of provider prefix) |
credentials | LivenessCredentials | ✓ | From client.getLivenessCredentials(userId) |
onComplete | () => void | ✓ | Called when the Rekognition analysis finishes |
onError | (error: unknown) => void | Called 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_reason | Meaning |
|---|---|
liveness_failed | Could not confirm a live person |
face_mismatch | Selfie didn't match the ID photo |
document_expired | ID document is past its expiry date |
fraud_detected | Spoofing or fraudulent content detected |
ocr_failed | Could not extract data from the ID image |
duplicate_identity | Person 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.