attestid_js_sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +453 -2
- package/dist/core/index.cjs +2344 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +585 -0
- package/dist/core/index.d.ts +585 -0
- package/dist/core/index.js +2275 -0
- package/dist/core/index.js.map +1 -0
- package/dist/react/index.cjs +2491 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +411 -0
- package/dist/react/index.d.ts +411 -0
- package/dist/react/index.js +2460 -0
- package/dist/react/index.js.map +1 -0
- package/dist/styles/attestid.css +1103 -0
- package/package.json +57 -4
package/README.md
CHANGED
|
@@ -1,3 +1,454 @@
|
|
|
1
|
-
#
|
|
1
|
+
# attestid_js_sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Official JavaScript / TypeScript SDK for the AttestID KYC API.
|
|
4
|
+
|
|
5
|
+
Handles credential management, ID photo quality checks, liveness sequencing, and fraud detection — so your integration is a few lines, not hundreds.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install attestid_js_sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
React support is included via a separate entry point. Peer deps `react` and `react-dom` (≥ 18) are optional — only install them if you use `attestid_js_sdk/react`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
`apiKey` is a long-lived secret. **Never construct `AttestID` with `apiKey` in browser or mobile app code** — a bundler would ship it to every visitor, and anyone could read it from devtools or a decompiled bundle. `AttestID` enforces this at runtime: passing `apiKey` outside a real Node.js backend process throws `INSECURE_CONFIG` immediately.
|
|
22
|
+
|
|
23
|
+
`apiUrl` must be `https://` — the SDK also enforces this at runtime, throwing `INSECURE_CONFIG` for a plain `http://` URL. The one exception is a loopback host (`localhost` / `127.0.0.1` / `::1`), which stays `http://` so you can point the SDK at a local backend that isn't running TLS. This means the same guard applies whether you're testing locally or integrating against a real environment — if a QA or staging URL isn't `localhost`, it must be `https://` too, so API keys, SDK tokens, and uploaded ID photos are never sent in the clear.
|
|
24
|
+
|
|
25
|
+
In the browser, configure `getSdkToken` instead — a callback that fetches an already-minted `sdk_token` from **your own backend**, which holds `apiKey` server-side and calls `POST /kyc/session` on your behalf:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// your-backend/routes/attestid-session.ts (Node.js — never shipped to the browser)
|
|
29
|
+
export async function attestidSession(userId: string) {
|
|
30
|
+
const res = await fetch(`${process.env.ATTESTID_API_URL}/v1/kyc/session`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'X-API-Key': process.env.ATTESTID_API_KEY!, // sk_live_... — server-only
|
|
34
|
+
'X-Access-Key-Id': process.env.ATTESTID_ACCESS_KEY_ID!,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({ user_id: userId }),
|
|
38
|
+
});
|
|
39
|
+
return res.json(); // { sdk_token, user_id, expires_in }
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// browser code
|
|
45
|
+
import { AttestID } from 'attestid_js_sdk';
|
|
46
|
+
|
|
47
|
+
const client = new AttestID({
|
|
48
|
+
apiUrl: 'https://<your-attestid-api-host>', // from your AttestID dashboard
|
|
49
|
+
// Never embed apiKey here — fetch a short-lived sdk_token from your backend instead.
|
|
50
|
+
getSdkToken: (userId) =>
|
|
51
|
+
fetch('/api/attestid-session', {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({ userId }),
|
|
55
|
+
}).then(r => r.json()),
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Then run the flow in React, either way:
|
|
60
|
+
|
|
61
|
+
- **Drop-in:** render `<AttestIDVerifier client={client} />` — consent, camera capture, quality checks, the face check and the result screen in one component. See [Drop-in React component](#drop-in-react-component).
|
|
62
|
+
- **Custom UI:** drive the steps yourself with `useAttestID(client)`, and mount `<AttestIDLivenessCheck />` for the face check. See [React hook](#react-hook).
|
|
63
|
+
|
|
64
|
+
Either way, send `result.attestation` to your backend and verify it there before acting on the result — see [Verify results on your backend](#verify-results-on-your-backend).
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Account setup
|
|
69
|
+
|
|
70
|
+
Signing up and issuing API keys is a dashboard-only flow, not something this SDK exposes:
|
|
71
|
+
|
|
72
|
+
1. Register your company and owner account, then verify the owner's email address.
|
|
73
|
+
2. Log in to the AttestID dashboard.
|
|
74
|
+
3. Self-issue an API key (test or live) from the dashboard — it's shown once, so store it in your backend's secret store immediately.
|
|
75
|
+
|
|
76
|
+
Use that `api_key` (and its paired `access_key_id`) to construct `AttestID` in server mode, as shown above.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Security
|
|
81
|
+
|
|
82
|
+
The SDK supports the AttestID API's security hardening. Everything below is
|
|
83
|
+
on by default where it applies, and it all degrades gracefully against an
|
|
84
|
+
older API.
|
|
85
|
+
|
|
86
|
+
| Feature | Where it runs | What you need to do |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Safe automatic retries (`Idempotency-Key`) | everywhere | Nothing. Optionally tune `retry` |
|
|
89
|
+
| Device-bound capture signing | browser | Nothing. Opt out with `deviceBinding: false` |
|
|
90
|
+
| Signed results (`result.attestation`) | **your backend** | Call `verifyAttestation()` before trusting `verified` |
|
|
91
|
+
| Signed webhooks + secret rotation | **your backend** | Call `verifyWebhook()` on every delivery |
|
|
92
|
+
| Mutual TLS (optional, per account) | your backend | Pass an mTLS-bound `fetch` |
|
|
93
|
+
|
|
94
|
+
### Safe automatic retries
|
|
95
|
+
|
|
96
|
+
Every `POST` (`/kyc/session`, `/kyc/start`, `/kyc/complete`) carries an
|
|
97
|
+
`Idempotency-Key`, and failed calls are retried automatically (default 3
|
|
98
|
+
attempts, exponential backoff):
|
|
99
|
+
|
|
100
|
+
- **Network error:** retried with the **same** key, so if the first attempt actually reached the server, you get its stored response back instead of a duplicate session or a second billed verification.
|
|
101
|
+
- **`409` (still processing):** waits for `Retry-After`, then retries with the same key.
|
|
102
|
+
- **`5xx` / `429`:** the server stored nothing, so the request is rebuilt with a fresh key, and a fresh device-signing nonce when the request is a photo upload.
|
|
103
|
+
- **Any other `4xx`:** thrown immediately, never retried.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
new AttestID({ apiUrl, getSdkToken, retry: { attempts: 4, baseDelayMs: 750 } });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Device-bound capture signing (browser)
|
|
110
|
+
|
|
111
|
+
In browser mode the SDK creates an ECDSA P-256 key with WebCrypto as
|
|
112
|
+
**non-extractable**: script, including this SDK, can sign with it but can
|
|
113
|
+
never read or export it. It's stored in IndexedDB, with an in-memory
|
|
114
|
+
fallback in private mode or webviews. On every `startVerification()` it:
|
|
115
|
+
|
|
116
|
+
1. registers the public key with AttestID (`POST /kyc/device/challenge` + `/kyc/device/register`). The server de-duplicates, so this is cheap.
|
|
117
|
+
2. signs `attestid-capture-v1|sha256(exact photo bytes)|user_id|nonce|timestamp` with a fresh single-use nonce, and sends the signature with the upload.
|
|
118
|
+
|
|
119
|
+
AttestID records the result as the verification's `capture_assurance`
|
|
120
|
+
(inside the signed attestation). A photo replayed or injected straight
|
|
121
|
+
into the API, with a wrong key, a reused nonce or altered bytes, is **held
|
|
122
|
+
for fraud review**.
|
|
123
|
+
|
|
124
|
+
- **Best-effort:** if WebCrypto, IndexedDB or the device endpoints are unavailable, the photo is uploaded **unsigned** (assurance `none`, not penalised). The SDK never sends a partial signature.
|
|
125
|
+
- **What the web can prove:** browsers have no hardware attestation, so web keys reach `key_bound`, never `hardware_attested` (that needs the native SDKs with App Attest / Play Integrity). It proves the photo came through this SDK on the device that holds the key, not that it came from the physical camera.
|
|
126
|
+
- **Server mode:** always off, because a backend process isn't the user's device.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
new AttestID({ apiUrl, getSdkToken, deviceBinding: false }); // opt out
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Verify results on your backend
|
|
133
|
+
|
|
134
|
+
Every result includes `result.attestation`, an ES256-signed
|
|
135
|
+
record of the decision issued by AttestID. The plain `verified` field
|
|
136
|
+
travels through the browser, so it can be tampered with there. **Before you
|
|
137
|
+
act on a result (open an account, release funds), verify the attestation on
|
|
138
|
+
your backend** against AttestID's public keys:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// your backend (Node 19+, or any runtime with WebCrypto)
|
|
142
|
+
import { verifyAttestation, SignatureError } from 'attestid_js_sdk';
|
|
143
|
+
|
|
144
|
+
const claims = await verifyAttestation(result.attestation, {
|
|
145
|
+
apiUrl: process.env.ATTESTID_API_URL!, // keys fetched from /.well-known/jwks.json (cached)
|
|
146
|
+
verificationId: expectedVerificationId, // must be the session you started
|
|
147
|
+
companyId: process.env.ATTESTID_COMPANY_ID, // optional
|
|
148
|
+
document: result.document, // optional: proves the ID data wasn't altered
|
|
149
|
+
});
|
|
150
|
+
if (!claims.verified) throw new Error('Not verified');
|
|
151
|
+
claims.capture_assurance; // 'hardware_attested' | 'key_bound' | 'none' | 'invalid'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
It throws `SignatureError` on a bad signature, an unexpected algorithm
|
|
155
|
+
(`alg: none` and HS256 are rejected), the wrong verification or company id,
|
|
156
|
+
or document data that doesn't match `document_sha256`. Signing keys rotate
|
|
157
|
+
on the server; the SDK refetches the key set when it sees a new `kid`.
|
|
158
|
+
|
|
159
|
+
### Verify webhooks
|
|
160
|
+
|
|
161
|
+
Pass the **raw** request body (before JSON parsing) and the headers:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import express from 'express';
|
|
165
|
+
import { verifyWebhook } from 'attestid_js_sdk';
|
|
166
|
+
|
|
167
|
+
app.post('/webhooks/attestid', express.raw({ type: 'application/json' }), async (req, res) => {
|
|
168
|
+
try {
|
|
169
|
+
const event = await verifyWebhook(req.body, req.headers, {
|
|
170
|
+
// During a secret rotation, list BOTH secrets: either one verifies.
|
|
171
|
+
secrets: [process.env.ATTESTID_WEBHOOK_SECRET!, process.env.ATTESTID_WEBHOOK_SECRET_OLD].filter(Boolean) as string[],
|
|
172
|
+
apiUrl: process.env.ATTESTID_API_URL, // also verifies the asymmetric AttestID-JWS header
|
|
173
|
+
});
|
|
174
|
+
// ...handle event...
|
|
175
|
+
res.sendStatus(200);
|
|
176
|
+
} catch {
|
|
177
|
+
res.sendStatus(400);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- **`secrets`** checks the HMAC `AttestID-Signature` header. During a rotation AttestID signs with both the new and the old secret until you confirm the rotation (`POST /v1/webhooks/rotation/confirm`) or the overlap window ends.
|
|
183
|
+
- **`apiUrl`** checks `AttestID-JWS`, a signature anyone can verify with AttestID's public key. Unlike the shared HMAC secret, it proves to a third party (an auditor) that AttestID sent it.
|
|
184
|
+
- **Replay protection:** both timestamps must be within `toleranceSeconds` (default 300).
|
|
185
|
+
|
|
186
|
+
### Mutual TLS (server mode)
|
|
187
|
+
|
|
188
|
+
If your account requires a client certificate (dashboard → Client
|
|
189
|
+
certificates), give the SDK a `fetch` that presents it. For example, with
|
|
190
|
+
Node's bundled undici:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { Agent, fetch as undiciFetch } from 'undici';
|
|
194
|
+
import { readFileSync } from 'node:fs';
|
|
195
|
+
|
|
196
|
+
const mtls = new Agent({ connect: { cert: readFileSync('client.crt'), key: readFileSync('client.key') } });
|
|
197
|
+
|
|
198
|
+
const client = new AttestID({
|
|
199
|
+
apiUrl: process.env.ATTESTID_API_URL!,
|
|
200
|
+
apiKey: process.env.ATTESTID_API_KEY!,
|
|
201
|
+
accessKeyId: process.env.ATTESTID_ACCESS_KEY_ID!,
|
|
202
|
+
fetch: (url, init) => undiciFetch(url, { ...init, dispatcher: mtls } as never) as unknown as Promise<Response>,
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Generate the key and CSR on your server (`openssl ecparam -name prime256v1 -genkey -noout -out client.key && openssl req -new -key client.key -subj "/CN=attestid-client" -out client.csr`). Your private key never leaves your infrastructure.
|
|
207
|
+
|
|
208
|
+
### Runtime requirements
|
|
209
|
+
|
|
210
|
+
The verification helpers and device signing use WebCrypto
|
|
211
|
+
(`globalThis.crypto.subtle`): any modern browser, **Node.js 19+**, Deno,
|
|
212
|
+
Bun, or edge runtimes. On a runtime without it, `verifyAttestation` /
|
|
213
|
+
`verifyWebhook` throw `CRYPTO_UNAVAILABLE`, and device signing is skipped
|
|
214
|
+
silently.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Photo quality check
|
|
219
|
+
|
|
220
|
+
Run `checkPhotoQuality` client-side before uploading to give users instant feedback and avoid unnecessary API calls:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { checkPhotoQuality } from 'attestid_js_sdk';
|
|
224
|
+
|
|
225
|
+
const result = await checkPhotoQuality(file, idType);
|
|
226
|
+
// result.ok — true when all checks pass
|
|
227
|
+
// result.issues — array of human-readable strings describing problems
|
|
228
|
+
|
|
229
|
+
if (!result.ok) {
|
|
230
|
+
// Show result.issues to the user before letting them submit
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Checks performed:
|
|
235
|
+
- **File size** — rejects obviously non-image blobs
|
|
236
|
+
- **Blur** — Laplacian variance; flags camera shake or out-of-focus shots
|
|
237
|
+
- **Brightness** — flags underexposed and overexposed images
|
|
238
|
+
- **Document type** — flags a passport uploaded as an ID card (or vice versa) from its shape
|
|
239
|
+
- **Passport MRZ** — the data page must show the machine-readable zone (the two lines of `<<<` text). Rejects the cover, visa pages, a cropped-off MRZ, or glare across it. In a two-page spread the half with the MRZ is treated as the data page, and only its blur is evaluated
|
|
240
|
+
- **ID card / driver's licence text** — the front must show several printed lines in a column (name, date of birth, ID number). Rejects the back of the card, a blank surface, or a photo of something else. No MRZ is needed on cards
|
|
241
|
+
- **Face count** — where the browser supports the Shape Detection API, flags no face or more than one face
|
|
242
|
+
|
|
243
|
+
The MRZ, card-size and text checks look at shapes and layout, not characters: no OCR library is bundled, and together they take about 20-70 ms. Where Chromium's `TextDetector` is available it's used as a second opinion that can only pass a photo, never reject one. A present but partly unreadable MRZ still goes to the API, which reads it properly.
|
|
244
|
+
|
|
245
|
+
`AttestIDVerifier` and `useAttestID` won't submit a photo while `checkPhotoQuality` reports issues. If you call `startKYC` directly, gating on `checkPhotoQuality` is up to you.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Drop-in React component
|
|
250
|
+
|
|
251
|
+
`AttestIDVerifier` is the fastest way to ship KYC in a React app: consent capture, in-browser camera capture with a review-before-submit step, client-side photo quality gating, liveness detection, and a result screen — all built in.
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
import { AttestID } from 'attestid_js_sdk';
|
|
255
|
+
import { AttestIDVerifier } from 'attestid_js_sdk/react';
|
|
256
|
+
import 'attestid_js_sdk/styles.css';
|
|
257
|
+
|
|
258
|
+
const client = new AttestID({
|
|
259
|
+
apiUrl: 'https://<your-attestid-api-host>', // from your AttestID dashboard
|
|
260
|
+
getSdkToken: (userId) =>
|
|
261
|
+
fetch('/api/attestid-session', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { 'Content-Type': 'application/json' },
|
|
264
|
+
body: JSON.stringify({ userId }),
|
|
265
|
+
}).then(r => r.json()),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
function KYCPage() {
|
|
269
|
+
return (
|
|
270
|
+
<AttestIDVerifier
|
|
271
|
+
client={client}
|
|
272
|
+
defaultUserId="user-123"
|
|
273
|
+
onVerified={(result) => console.log('Verified:', result.verified)}
|
|
274
|
+
onError={(error) => console.error('Verification error:', error)}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
| Prop | Type | Description |
|
|
281
|
+
|---|---|---|
|
|
282
|
+
| `client` | `AttestID` | Configured client instance |
|
|
283
|
+
| `defaultUserId` | `string?` | Pre-fills the user ID field |
|
|
284
|
+
| `onVerified` | `(result: KYCResult) => void` | Called once the flow reaches `done` |
|
|
285
|
+
| `onError` | `(error: string) => void` | Called on any error message from the flow |
|
|
286
|
+
| `mobileHandoffUrl` | `string?` | Page a desktop visitor's phone is sent to via QR/link. Defaults to the current page |
|
|
287
|
+
| `allowDesktopCapture` | `boolean?` | Let laptops/desktops capture the document with the webcam (phone QR still offered as an option). Default `true`; `false` makes capture phone-only |
|
|
288
|
+
| `allowPhotoUpload` | `boolean?` | Show an "Upload photo" option to pick an existing image instead of using the camera. For testing — uploads bypass live capture. Default `false` |
|
|
289
|
+
|
|
290
|
+
Need more control over the steps, styling, or layout? Build on `useAttestID` directly — see below.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## React hook
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
import { useAttestID, AttestIDLivenessCheck } from 'attestid_js_sdk/react';
|
|
298
|
+
|
|
299
|
+
function KYCPage() {
|
|
300
|
+
const verification = useAttestID(client);
|
|
301
|
+
const { state, error, qualityIssues, result, isLoading,
|
|
302
|
+
startVerification, canRetryLiveness, retryLiveness, reset } = verification;
|
|
303
|
+
|
|
304
|
+
// consentGivenAt must be when the user actually agreed, not when the upload fires.
|
|
305
|
+
const upload = (file: File) =>
|
|
306
|
+
startVerification({ userId: 'user-123', idType: 'national_id', idPhoto: file, consentGivenAt });
|
|
307
|
+
|
|
308
|
+
if (state === 'liveness') {
|
|
309
|
+
// Runs the face check, then completes the verification.
|
|
310
|
+
return <AttestIDLivenessCheck verification={verification} onError={console.error} />;
|
|
311
|
+
}
|
|
312
|
+
if (state === 'done' && result) {
|
|
313
|
+
return (
|
|
314
|
+
<>
|
|
315
|
+
<p>{result.verified ? 'Verified' : result.message}</p>
|
|
316
|
+
{canRetryLiveness && <button onClick={retryLiveness}>Retry face check</button>}
|
|
317
|
+
<button onClick={reset}>Start again</button>
|
|
318
|
+
</>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
// ...your consent + document capture UI, calling upload(file)...
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`startVerification` runs the quality check first, then on pass uploads the ID and prepares the face check. If quality fails, `state` becomes `'error'` and `qualityIssues` is populated — no API call is made.
|
|
326
|
+
|
|
327
|
+
### Return values
|
|
328
|
+
|
|
329
|
+
| Property | Type | Description |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| `state` | `VerifyState` | Current flow state (see below) |
|
|
332
|
+
| `error` | `string \| null` | Last API error message |
|
|
333
|
+
| `qualityIssues` | `string[]` | Human-readable quality problems (populated on `error` after a quality failure) |
|
|
334
|
+
| `result` | `KYCResult \| null` | Final verification result |
|
|
335
|
+
| `isLoading` | `boolean` | `true` while in `quality_check`, `uploading`, `retrying`, or `completing` |
|
|
336
|
+
| `startVerification` | `(opts: StartKYCOptions) => Promise<void>` | Start the flow |
|
|
337
|
+
| `canRetryLiveness` | `boolean` | `true` when `result` failed only at the face check, so `retryLiveness()` can be offered |
|
|
338
|
+
| `retryLiveness` | `() => Promise<void>` | Get a fresh face-check session on the same verification and go back to `liveness`, without uploading the ID again |
|
|
339
|
+
| `reset` | `() => void` | Reset all state back to `idle` |
|
|
340
|
+
|
|
341
|
+
### States
|
|
342
|
+
|
|
343
|
+
| State | Meaning |
|
|
344
|
+
|---|---|
|
|
345
|
+
| `idle` | Ready to start |
|
|
346
|
+
| `quality_check` | Running client-side photo quality checks |
|
|
347
|
+
| `uploading` | Uploading ID photo and preparing the face check |
|
|
348
|
+
| `retrying` | Getting a fresh face-check session for `retryLiveness()` |
|
|
349
|
+
| `liveness` | Waiting for the face check: mount `AttestIDLivenessCheck` |
|
|
350
|
+
| `completing` | Fetching the final result |
|
|
351
|
+
| `done` | Result available in `result` |
|
|
352
|
+
| `error` | Something went wrong; see `error` or `qualityIssues` |
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Face check
|
|
357
|
+
|
|
358
|
+
`AttestIDVerifier` runs the face check for you. With the hook, mount `AttestIDLivenessCheck` while `state === 'liveness'`: it runs the check and completes the verification when it finishes. It renders nothing in any other state.
|
|
359
|
+
|
|
360
|
+
| Prop | Type | Description |
|
|
361
|
+
|---|---|---|
|
|
362
|
+
| `verification` | `ReturnType<typeof useAttestID>` | The object returned by `useAttestID()` (required) |
|
|
363
|
+
| `onError` | `(error: unknown) => void` | Called on a camera or face-check error |
|
|
364
|
+
| `deviceId` | `string?` | Forwarded to the backend when the verification completes |
|
|
365
|
+
| `showFlashNotice` | `boolean?` | Show the flashing-colours notice first. Default `true` |
|
|
366
|
+
| `lightingCheckMs` | `number?` | How long to coach the user towards better light before starting. Default `10000`; `0` skips it |
|
|
367
|
+
|
|
368
|
+
Things to know:
|
|
369
|
+
- **Timing.** A face-check session expires about 3 minutes after it starts. A failed or expired check needs a new session (see "Retrying a failed face check" below).
|
|
370
|
+
- **Photosensitivity.** The check can flash colours on screen. The SDK shows a notice before starting (`showFlashNotice`, default on). Keep it, or show your own.
|
|
371
|
+
- **Device binding.** With device binding on, the face-check capture is signed with the same device key as the ID photo.
|
|
372
|
+
- **Browser support.** Current Chrome, Edge, Firefox, Safari 14.1+, and iOS 14.3+, in a secure context (https, or http://localhost).
|
|
373
|
+
|
|
374
|
+
### Retrying a failed face check
|
|
375
|
+
|
|
376
|
+
When a verification fails only at the face check (`liveness_failed`, `liveness_confidence_too_low`, or `face_match_failed` with a score), the user can retry it without uploading the ID photo again; the document checks already done still count. `AttestIDVerifier` shows a **Try again** button (retake the selfie, same ID) next to **Start over** (upload a new ID) when this applies. Each failed face match still counts toward the API's per-user face-match strike limit. With the hook, offer your own button when `canRetryLiveness` is `true` and call `retryLiveness()`: `state` goes back to `liveness` and `AttestIDLivenessCheck` runs the check again.
|
|
377
|
+
|
|
378
|
+
The API refuses a retry (`KYCError`, HTTP 409/410) in these cases:
|
|
379
|
+
- suspected spoofs, face-match failures and fraud holds, which always need a new verification;
|
|
380
|
+
- after 2 retries on one verification;
|
|
381
|
+
- more than 30 minutes after the ID was uploaded.
|
|
382
|
+
|
|
383
|
+
When it refuses, start over with `reset()` / a new `startVerification()`.
|
|
384
|
+
|
|
385
|
+
---
|
|
386
|
+
|
|
387
|
+
## Camera utilities
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
import { openCameraSession, DOC_ASPECT } from 'attestid_js_sdk';
|
|
391
|
+
|
|
392
|
+
const session = await openCameraSession('national_id');
|
|
393
|
+
// session.stream — raw MediaStream
|
|
394
|
+
// session.captureFrame(frameEl, videoEl) → Promise<File> — crop to document frame
|
|
395
|
+
// session.stop() — release camera
|
|
396
|
+
|
|
397
|
+
// Aspect ratios for sizing your document overlay frame
|
|
398
|
+
// DOC_ASPECT.national_id === 85.6 / 54 (ISO 7810 ID-1)
|
|
399
|
+
// DOC_ASPECT.drivers_licence === 85.6 / 54 (ISO 7810 ID-1)
|
|
400
|
+
// DOC_ASPECT.passport === 125 / 88 (ISO 7810 ID-3)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Supported ID types
|
|
406
|
+
|
|
407
|
+
| `IdType` value | Document |
|
|
408
|
+
|---|---|
|
|
409
|
+
| `national_id` | National ID card (any country, ISO 7810 ID-1) |
|
|
410
|
+
| `passport` | Passport booklet data page (ISO 7810 ID-3) |
|
|
411
|
+
| `drivers_licence` | Driver's licence (ISO 7810 ID-1) |
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Error handling
|
|
416
|
+
|
|
417
|
+
All errors extend `AttestIDError` and expose `code` and optionally `status`:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
import { AttestIDError, AuthError, KYCError, CameraError } from 'attestid_js_sdk';
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
await client.startVerification({ ... });
|
|
424
|
+
} catch (e) {
|
|
425
|
+
if (e instanceof AuthError) console.error('Auth failed', e.status); // 401/403
|
|
426
|
+
if (e instanceof KYCError) console.error('KYC failed', e.code, e.status);
|
|
427
|
+
if (e instanceof CameraError) console.error('Camera error', e.message);
|
|
428
|
+
if (e instanceof AttestIDError) console.error(e.code, e.message); // e.g. CONSENT_REQUIRED, INSECURE_CONFIG
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
| Class | `code` | When thrown |
|
|
433
|
+
|---|---|---|
|
|
434
|
+
| `AuthError` | `AUTH_ERROR` | Invalid API key/access key, an expired or invalid `sdk_token` (401 / 403), or `getSdkToken()` resolving without a valid token |
|
|
435
|
+
| `KYCError` | `KYC_ERROR` | API returned a non-auth error |
|
|
436
|
+
| `CameraError` | `CAMERA_ERROR` | Camera access denied or capture failed |
|
|
437
|
+
| `AttestIDError` | `CONSENT_REQUIRED` / `INSECURE_CONFIG` | Missing `consentGivenAt`; `apiKey`/`accessKeyId` configured outside a trusted Node.js backend; or `apiUrl` isn't `https://` (non-loopback host) |
|
|
438
|
+
| `SignatureError` | `SIGNATURE_INVALID` | `verifyAttestation()` / `verifyWebhook()`: bad signature, wrong verification/company id, altered document data, or a webhook timestamp outside the replay window |
|
|
439
|
+
| `AttestIDError` | `LIVENESS_UNSUPPORTED` / `LIVENESS_RECORDING_FAILED` / `LIVENESS_ABORTED` / `LIVENESS_TIMEOUT` | Face check: the browser can't run it; it failed or was cancelled; or no decision arrived in time (default 120 s) |
|
|
440
|
+
| `AttestIDError` | `JWKS_UNAVAILABLE` / `CRYPTO_UNAVAILABLE` / `INVALID_ARGUMENT` | AttestID's public keys couldn't be fetched; the runtime has no WebCrypto; `verifyWebhook()` called without `secrets` or `apiUrl` |
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Development
|
|
445
|
+
|
|
446
|
+
```bash
|
|
447
|
+
npm install
|
|
448
|
+
npm run build # compile to dist/
|
|
449
|
+
npm test # run tests
|
|
450
|
+
npm run test:watch # watch mode
|
|
451
|
+
npm run test:coverage # coverage report
|
|
452
|
+
npm run lint # type-check only
|
|
453
|
+
npm run dev # build in watch mode
|
|
454
|
+
```
|