# Idswyft API Documentation

> Open-source identity verification platform. Verify government-issued IDs with OCR, cross-validation, liveness detection, and face matching.
> Base URL: https://api.idswyft.app (replace with your own domain for self-hosted deployments)
> Version: v1.8.52 — April 2026
> Source of truth: backend/src/api-docs/apiDocsMarkdown.ts

## Links

- [Developer Portal](https://idswyft.app/developer): Register, get API keys, manage webhooks
- [Review Dashboard](https://idswyft.app/admin/verifications): Review, approve, reject verifications
- [Live Demo](https://idswyft.app/demo): Try the verification flow in sandbox mode
- [Documentation](https://idswyft.app/docs): Full interactive documentation
- [SDK](https://www.npmjs.com/package/@idswyft/sdk): JavaScript/TypeScript SDK
- [GitHub](https://github.com/team-idswyft/idswyft-community): Source code (MIT license)
- [Full API Docs (Markdown)](https://api.idswyft.app/api/docs/markdown): Machine-readable API docs

---

## Authentication

Every API request must include your API key in the `X-API-Key` header.

```
X-API-Key: ik_your_api_key
```

All keys use the `ik_` prefix. Sandbox mode is a property of the key itself (a boolean flag set when creating the key in the Developer Portal), not a prefix distinction. Both production and sandbox keys look like `ik_` followed by 64 hex characters.

### Developer Authentication

Developers authenticate via passwordless email OTP:

1. `POST /api/auth/developer/otp/send` — send OTP to registered email
2. `POST /api/auth/developer/otp/verify` — verify OTP, receive JWT (7-day expiry)
3. Include JWT as `Authorization: Bearer <token>` for developer portal endpoints

GitHub OAuth is also supported as an alternative login method.

---

## Verification Flow (5 Steps)

The verification pipeline is a 5-step sequence. Each step unlocks the next.

```
1. Initialize → 2. Front Doc → 3. Back Doc (+ cross-validation) → 4. Live Capture (+ face match) → 5. Results
```

**Hard rejection:** If any gate fails, the session status becomes `HARD_REJECTED` and subsequent steps return HTTP 409.

### Step 1: Start Session

```
POST /api/v2/verify/initialize
Content-Type: application/json
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| user_id | UUID string | Yes | Your unique identifier for the user |
| sandbox | boolean | No | Use sandbox mode (default: false) |
| addons | object | No | Optional add-on features |
| addons.aml_screening | boolean | No | Override AML screening (default: auto-enabled when `AML_PROVIDER` is configured; set `false` to disable for this session) |
| verification_mode | string | No | Flow preset: `'full'` (default), `'document_only'`, `'identity'`, or `'age_only'`. See Verification Flows below |
| age_threshold | integer | No | Minimum age required (1-99, default: 18). Only used when `verification_mode` is `'age_only'` |

**Response (201):**

```json
{
  "success": true,
  "verification_id": "550e8400-e29b-41d4-a716-446655440001",
  "status": "AWAITING_FRONT",
  "current_step": 1,
  "total_steps": 5
}
```

### Verification Flows (Custom Gate Pipeline)

The `verification_mode` parameter controls which gates run per session:

| Preset | Gates Run | Steps | Use Case |
|--------|----------|-------|----------|
| `full` (default) | Front → Back → CrossVal → Liveness → FaceMatch [→ AML] | 5 | Full identity verification |
| `document_only` | Front → Back → CrossVal | 3 | Compliance document checks, no biometric |
| `identity` | Front → Liveness → FaceMatch | 3 | Quick identity check — no back doc, no crossval |
| `age_only` | Front (DOB extraction + age check) | 1 | Age-gated content |

**Endpoint guards per flow:**

- `document_only` / `age_only`: Calling `POST /:id/live-capture` returns HTTP 400
- `identity` / `age_only`: Calling `POST /:id/back-document` returns HTTP 400

**Final result determination per flow:**

| Flow | Result Based On |
|------|----------------|
| `full` | Cross-validation verdict + face match |
| `document_only` | Cross-validation verdict only (PASS → verified, REVIEW → manual_review) |
| `identity` | Face match only (no crossval data) |
| `age_only` | DOB extraction + age threshold check |

### Alternative: Re-verify a Returning User

For returning users who have already been verified, use the re-verification endpoint to perform a liveness-only re-check instead of the full 5-step flow.

```
POST /api/v2/verify/re-verify
Content-Type: application/json
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| user_id | UUID string | Yes | Same user_id used in the original verification |
| previous_verification_id | UUID string | Yes | ID of a previously completed verification (status must be `verified`) |
| source | string | No | 'api' | 'vaas' | 'demo' (default: 'api') |

**Response (201):**

```json
{
  "success": true,
  "verification_id": "660e8400-e29b-41d4-a716-446655440002",
  "parent_verification_id": "550e8400-e29b-41d4-a716-446655440001",
  "verification_mode": "liveness_only",
  "status": "AWAITING_LIVE",
  "current_step": 4,
  "total_steps": 5,
  "message": "Re-verification initialized — ready to upload live capture (liveness-only mode)"
}
```

**Two modes based on face embedding availability:**

| Mode | `verification_mode` | Starting Step | When |
|------|---------------------|---------------|------|
| Liveness-only | `liveness_only` | `AWAITING_LIVE` (step 4) | Face embedding from parent is still available |
| Document refresh | `document_refresh` | `AWAITING_FRONT` (step 1) | Face embedding was GDPR-stripped — user re-uploads front doc, then skips back doc + cross-validation |

**Constraints:**
- Parent verification must have `status: "verified"`
- Parent must belong to the same developer and user
- Cannot chain re-verifications — parent must be a full (`verification_mode: "full"`) verification
- After initialization, proceed to `POST /api/v2/verify/:id/live-capture` (liveness_only) or `POST /api/v2/verify/:id/front-document` (document_refresh)

### Alternative: Age Verification (18+/21+ Check)

For age-gated use cases (alcohol delivery, cannabis, gambling), use age-only mode to extract DOB from a document and check against an age threshold — no face match, no liveness, no back document.

**Two-step flow:**

1. Initialize with `verification_mode: 'age_only'`
2. Upload front document — OCR extracts DOB, age is checked, session auto-completes

```
POST /api/v2/verify/initialize
Content-Type: application/json

{
  "user_id": "550e8400-e29b-41d4-a716-446655440001",
  "verification_mode": "age_only",
  "age_threshold": 21
}
```

**Response (201):**

```json
{
  "success": true,
  "verification_id": "770e8400-e29b-41d4-a716-446655440003",
  "status": "AWAITING_FRONT",
  "current_step": 1,
  "total_steps": 1,
  "verification_mode": "age_only",
  "age_threshold": 21
}
```

Then upload the front document as usual (`POST /api/v2/verify/:id/front-document`). The response includes:

```json
{
  "success": true,
  "verification_id": "770e8400-e29b-41d4-a716-446655440003",
  "status": "COMPLETE",
  "current_step": 1,
  "age_verification": {
    "is_of_age": true,
    "age_threshold": 21
  },
  "final_result": "verified"
}
```

**Privacy:** The response never includes the actual date of birth — only `is_of_age` (boolean) and `age_threshold` (integer).

**Rejection reasons:**
- `DOB_NOT_FOUND` — date of birth could not be extracted from the document
- `UNDERAGE` — subject does not meet the minimum age requirement

**Webhook:** Fires `verification.age_check` event on completion.

### Step 2: Upload Front Document

```
POST /api/v2/verify/:id/front-document
Content-Type: multipart/form-data
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| document_type | string | No | 'passport' | 'drivers_license' | 'national_id' | 'other' | 'auto'. Defaults to 'auto' (auto-detect from OCR text). If provided explicitly, the given type is used with confidence 1.0 |
| document | File | Yes | JPEG, PNG, WebP, or PDF. Max 10 MB |
| country_code | string | No | ISO 3166-1 alpha-2 country code (e.g. 'US', 'GB'). Improves OCR accuracy for international documents |

**Response (201):** Includes `ocr_data` with extracted fields (name, DOB, document number, expiry, address) and `confidence_scores` per field. Also includes `detected_document_type` (the auto-detected or user-specified document type) and `classification_confidence` (0.50-1.0 indicating detection reliability). Auto-classification signals: MRZ patterns (TD1/TD2/TD3), keyword matching (PASSPORT, DRIVER LICENSE, NATIONAL ID), and field-pattern heuristics (AAMVA codes, DL tokens).

### Step 3: Upload Back Document

```
POST /api/v2/verify/:id/back-document
Content-Type: multipart/form-data
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| document_type | string | Yes | Must match Step 2 document_type |
| document | File | Yes | JPEG, PNG, or WebP. Max 10 MB |

**Response (201):** Includes `barcode_data` and `cross_validation_results` with verdict (PASS/REVIEW), score, and failures.

Cross-validation checks: PDF417/QR barcode decoding, MRZ parsing (TD1/TD2/TD3 for international IDs), ID number consistency, expiry date matching, name matching with Levenshtein distance and token-set similarity, address matching (supplementary — word-overlap scoring with abbreviation normalization, does not affect verdict).

### Step 4: Submit Live Capture

```
POST /api/v2/verify/:id/live-capture
Content-Type: multipart/form-data
```

One endpoint, two gates, two liveness modes:

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| selfie | File | Yes | JPEG or PNG image captured live from the user's camera via getUserMedia(). Max 10 MB. Static file uploads will fail liveness. |
| liveness_metadata | JSON string | No | Challenge data for head_turn liveness. Omit for passive mode |

> **Important:** The selfie must be a live camera capture, not a file upload. The liveness engine runs anti-spoofing checks on every image — even in passive mode. A static photo uploaded from disk will fail with `LIVENESS_FAILED` because it lacks camera EXIF metadata and has re-compression artifacts. Always use `getUserMedia()` to capture directly from the device camera.

#### Liveness Modes

| Mode | challenge_type | Security | Best For |
|------|---------------|----------|----------|
| Passive | _(omit metadata)_ | Basic | Low-risk onboarding, sandbox |
| Head Turn | `head_turn` | Strong | All production identity verification |

**Head-turn metadata shape:**

```json
{
  "challenge_type": "head_turn",
  "challenge_direction": "left",
  "frames": [
    { "frame_base64": "/9j/4AAQ...", "timestamp": 1000,  "phase": "turn1_start" },
    { "frame_base64": "/9j/4BBR...", "timestamp": 4000,  "phase": "turn1_peak" },
    { "frame_base64": "/9j/4CCG...", "timestamp": 7000,  "phase": "turn1_return" },
    { "frame_base64": "/9j/4DDH...", "timestamp": 8200,  "phase": "turn_start" },
    { "frame_base64": "/9j/4EEI...", "timestamp": 11200, "phase": "turn_peak" },
    { "frame_base64": "/9j/4FFJ...", "timestamp": 14200, "phase": "turn_return" }
  ],
  "start_timestamp": 0,
  "end_timestamp": 15000
}
```

Zero ML dependencies on the client — just `getUserMedia()` + `canvas.toDataURL()`. The server handles all face detection and yaw estimation.

> **Note:** Invalid liveness_metadata returns HTTP 400 with a `VALIDATION_ERROR` code. It does not silently fall back to passive mode. Always check your metadata format matches the schema above.

**Response (201):**

```json
{
  "success": true,
  "verification_id": "...",
  "status": "COMPLETE",
  "face_match_results": {
    "passed": true,
    "similarity_score": 0.94,
    "threshold_used": 0.6
  },
  "liveness_results": {
    "liveness_passed": true,
    "liveness_score": 0.96,
    "liveness_mode": "head_turn"
  },
  "final_result": "verified"
}
```

### Step 5: Get Results

```
GET /api/v2/verify/:id/status
```

Returns the full verification record: OCR data, cross-validation, liveness, face match, AML screening, and final decision.

**Polling conditions:**

| After | Poll until | Then |
|-------|-----------|------|
| Step 2 (front doc) | `ocr_data` is not null | Proceed to Step 3 |
| Step 3 (back doc) | `cross_validation_results` is not null | If not failed, proceed to Step 4 |
| Step 4 (live capture) | `final_result` is not null | Verification complete |

---

## Optional: Phone OTP Verification

Add SMS-based phone verification as an optional step during any verification session. Requires the developer to configure their own SMS provider (Twilio or Vonage) via the Developer Settings API.

### Configure SMS Provider

```
PUT /api/developer/settings/sms
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `provider` | string | Yes | `twilio` or `vonage` (send `null` to clear) |
| `api_key` | string | Yes | Twilio Account SID / Vonage API key |
| `api_secret` | string | Yes | Twilio Auth Token / Vonage API secret |
| `phone_number` | string | Yes | Sender phone in E.164 format (e.g. `+15551234567`) |

Credentials are encrypted at rest (AES-256-GCM). When no SMS provider is configured, the OTP code is returned in the API response for self-hosted/testing use.

### Send Phone OTP

```
POST /api/v2/verify/:id/phone-otp/send
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `phone_number` | string | Yes | Recipient phone in E.164 format |

**Rate limit:** 3 codes per session per hour. Code expires after 10 minutes.

### Verify Phone OTP

```
POST /api/v2/verify/:id/phone-otp/verify
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `code` | string | Yes | 6-digit verification code |

**Response:** `{ "success": true, "phone_verified": true }`

Max 3 attempts per code. After exhaustion, request a new code.

---

## Verification Page Branding

White-label the hosted verification page with your company's logo, accent color, and name.

### Configure Branding

```
GET /api/developer/settings/branding          — Get current branding settings
PUT /api/developer/settings/branding          — Save branding (logo_url, accent_color, company_name)
POST /api/developer/branding/logo             — Upload logo (JPEG/PNG, max 2 MB)
```

| Field | Type | Description |
|-------|------|-------------|
| logo_url | string | null | URL to your company logo |
| accent_color | string | null | 6-digit hex color (e.g. `#ff6600`) |
| company_name | string | null | Company name (max 100 chars) |

Send all fields as `null` to clear branding and revert to defaults.

### Get Page Config (Public)

```
GET /api/v2/verify/page-config?api_key=ik_...
```

Public endpoint — returns developer branding for the hosted verification page. No auth header required; uses API key in query string. Response cached for 5 minutes. `company_name` falls back to the developer's profile company name.

---

## Integration Options

Three ways to add identity verification — from zero-code to full control. All use the same hosted page:

```
https://idswyft.app/user-verification
```

### Option 1: Redirect (Easiest)

Send users to the hosted page with a link or redirect. After verification, they return to your `redirect_url`.

```javascript
window.location.href = 'https://idswyft.app/user-verification'
  + '?api_key=ik_your_api_key'
  + '&user_id=user-123'
  + '&redirect_url=' + encodeURIComponent('https://yourapp.com/done')
  + '&theme=dark';
```

**Redirect callback parameters:** When verification completes, the user is redirected to your `redirect_url` with these query parameters appended:

| Parameter | Type | Description |
|-----------|------|-------------|
| verification_id | UUID string | The verification session ID. Use this to fetch full results via `GET /api/v2/verify/:id/status` |
| status | string | Result status: `verified`, `failed`, or `manual_review`. If `manual_review`, poll the status endpoint or listen for a webhook to get the final decision |
| user_id | string | The user_id you passed when starting verification |

### Option 2: Iframe Embed (No SDK needed)

Embed the hosted page inside your app. Users never leave your site.

```html
<iframe
  src="https://idswyft.app/user-verification?api_key=ik_your_api_key&user_id=user-123&theme=dark"
  width="100%" height="700" frameborder="0"
  allow="camera; microphone"
  style="border: none; border-radius: 8px;"
></iframe>
```

> **Important:** The iframe requires `allow="camera; microphone"` for liveness detection to work.

### Option 3: SDK Embed (Recommended for SPAs)

Use the `@idswyft/sdk` IdswyftEmbed component for modal or inline mode with event callbacks.

### URL Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| api_key | string | Yes | Your API key |
| user_id | UUID | Yes | User identifier |
| redirect_url | URL | No | Where to redirect after completion (must be `http:` or `https:`). Required for redirect; optional for iframe/embed. Works on both desktop and mobile devices |
| theme | 'light' | 'dark' | No | UI theme (default: dark) |
| verification_mode | 'full' | 'document_only' | 'identity' | 'age_only' | No | Override the verification mode for this session |
| age_threshold | number | No | Minimum age to verify (used with `age_only` mode, default: 18) |
| address_verif | 'true' | No | Enable address verification step |

---

## Mobile Handoff

Let users start on desktop and continue on mobile. Useful when the desktop lacks a camera.

### Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/verify/handoff/create` | Create handoff session (body: `api_key`, `user_id`) |
| GET | `/api/verify/handoff/:token/status` | Poll session status from desktop |
| PATCH | `/api/verify/handoff/:token/link` | Link a verification_id to the handoff session (mobile calls this) |

### Flow

1. Desktop calls `POST /api/verify/handoff/create` with `api_key` and `user_id` in request body — returns `token` (30-min expiry) + `expires_at`
2. Build a verification URL with the token and display as a QR code
3. User scans QR on mobile — hosted page handles verification
4. Mobile links the verification to the handoff session via `PATCH /api/verify/handoff/:token/link`
5. Desktop polls `GET /api/verify/handoff/:token/status` until `status` is `completed`
6. Status response includes `verification_id` for fetching full results

> **Note:** Handoff uses `api_key` in the request body, not the `X-API-Key` header. Expired sessions return HTTP 410.

---

## JavaScript SDK

```bash
npm install @idswyft/sdk
```

```javascript
const { IdswyftSDK } = require('@idswyft/sdk');

const sdk = new IdswyftSDK({
  apiKey: 'ik_your_api_key',
  baseURL: 'https://api.idswyft.app',
  sandbox: false,
});
```

### SDK Methods

| Method | Returns | Description |
|--------|---------|-------------|
| startVerification() | InitializeResponse | Create new session |
| uploadFrontDocument() | VerificationResult | Upload front of ID |
| uploadBackDocument() | VerificationResult | Upload back of ID |
| uploadSelfie() | VerificationResult | Submit live capture |
| getVerificationStatus() | VerificationResult | Get session status |
| watch() | EventEmitter | Real-time event stream |
| createBatch() | BatchJobResponse | Create batch job |
| uploadAddressDocument() | AddressResult | Upload proof-of-address |
| createMonitoringSchedule() | ScheduleResponse | Create re-verification schedule |

### Real-Time Events with watch()

```javascript
const watcher = sdk.watch(verificationId);
watcher.on('status_changed', (e) => console.log(e.status));
watcher.on('step_completed', (e) => console.log(e.step, e.data));
watcher.on('verification_complete', (e) => console.log(e.data.final_result));
watcher.on('verification_failed', (e) => console.log(e.data.rejection_reason));
watcher.on('error', (e) => console.error(e.message));
watcher.destroy(); // cleanup
```

---

## Embed Component

Drop a complete verification UI into your app:

```javascript
import { IdswyftEmbed } from '@idswyft/sdk';

const embed = new IdswyftEmbed({ mode: 'modal', theme: 'dark' });
embed.open(sessionToken, {
  onComplete: (result) => console.log(result.finalResult),
  onError: (error) => console.error(error.message),
  onStepChange: (step) => console.log('Step:', step),
  onClose: () => console.log('Embed closed'),
});
```

### Modes

| Mode | Description |
|------|-------------|
| `modal` | Full-screen overlay with backdrop. Closes on backdrop click (configurable) |
| `inline` | Fits your container. Set `container` option to a DOM element |

### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| mode | 'modal' | 'inline' | 'modal' | Display mode |
| container | HTMLElement | — | Required for inline mode |
| theme | 'light' | 'dark' | 'dark' | UI theme |
| width | string | '100%' | Container width (inline mode) |
| height | string | '700px' | Container height (inline mode) |
| closeOnBackdropClick | boolean | true | Allow closing modal by clicking backdrop |
| verificationUrl | string | 'https://idswyft.app' | Override verification page URL |

## React Component

Drop-in React component for SPAs. Install:

```bash
npm install @idswyft/react
```

### Component Usage

```tsx
import { IdswyftVerification } from '@idswyft/react';

function App() {
  return (
    <IdswyftVerification
      apiKey="ik_your_api_key"
      userId="user-123"
      mode="modal"
      theme="dark"
      onComplete={(result) => console.log('Verified!', result.finalResult)}
      onError={(error) => console.error(error.message)}
      onClose={() => setShowVerification(false)}
    />
  );
}
```

### Hook Usage

```tsx
import { useIdswyftVerification } from '@idswyft/react';

function VerifyButton() {
  const { open, isOpen, result } = useIdswyftVerification({
    apiKey: 'ik_your_api_key',
  });

  return (
    <>
      <button onClick={() => open('user-123')}>Verify Identity</button>
      {result && <p>Result: {result.finalResult}</p>}
    </>
  );
}
```

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| apiKey | string | — | API key (required) |
| userId | string | — | User ID (required) |
| mode | 'modal' \| 'inline' | 'inline' | Display mode |
| theme | 'light' \| 'dark' | 'dark' | UI theme |
| documentType | string | — | Limit to specific document type |
| onComplete | function | — | Called when verification succeeds |
| onError | function | — | Called on error |
| onStepChange | function | — | Called on step progress |
| onClose | function | — | Called when user closes modal |

Works with Next.js, Vite, and Create React App. Requires React 17+.

---

## Analysis Engine

All verification decisions are **deterministic** — no LLMs or probabilistic models are used for pass/fail decisions. LLMs are only used for OCR text extraction behind a provider interface.

| Category | Capabilities |
|----------|-------------|
| OCR Extraction | PaddleOCR/Tesseract, name/DOB/ID number, AAMVA field parsing (US DLs), MRZ parsing (TD1/TD2/TD3 for international IDs), state format validation, per-field confidence scores, multi-language awareness (Latin scripts fully supported; Cyrillic/Arabic/CJK/Devanagari/Thai detection with custom model support) |
| Document Quality | Sobel edge blur detection, brightness/contrast stats, resolution check (≥800x600), file size validation, overall quality score, auto-reject below threshold |
| Cross-Validation | PDF417/QR barcode decode, MRZ parsing, Levenshtein distance matching, token-set name similarity, front OCR vs back barcode/MRZ check, date & ID number consistency, weighted field scoring, address cross-validation (supplementary) |
| Liveness & Face Match | EXIF metadata analysis, JPEG artifact detection, color histogram analysis, byte entropy scoring, pixel variance & edge density, face detection (SSDMobilenetv1), 128-d face embeddings, cosine similarity scoring, deepfake detection |
| Voice Authentication | Random digit challenge (6 digits), ASR transcription verification, 512-d speaker embeddings (CAM++), cosine similarity matching, configurable threshold (0.55 production / 0.50 sandbox), optional per-developer toggle |

---

## Face Age Estimation

Cross-checks apparent face age against declared date of birth. Extracts age estimates from both the document photo and the live capture, then flags discrepancies as a fraud signal (e.g., identity borrowing).

Age estimation runs automatically during live capture — no additional API call needed. Results appear in the `age_estimation` object of the live-capture and status responses.

**Response shape (`age_estimation` object):**

```json
{
  "age_estimation": {
    "document_face_age": 34,
    "live_face_age": 22,
    "declared_age": 35,
    "age_discrepancy": 13,
    "flag": "Age discrepancy between live capture and declared DOB"
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| document_face_age | integer | Estimated age from the document photo |
| live_face_age | integer | Estimated age from the live capture |
| declared_age | integer | Age calculated from OCR-extracted date of birth |
| age_discrepancy | integer | Absolute difference between `live_face_age` and `declared_age` |
| flag | string \| null | Human-readable flag when discrepancy exceeds threshold; `null` when ages are consistent |

High discrepancies contribute to the composite risk score. The `age_estimation` field is `null` when the engine is unavailable or no face is detected.

---

## Velocity / Fraud Detection

Analyzes verification velocity during the live-capture step to detect fraud patterns such as rapid resubmissions, bot-like step timing, burst activity, and multi-IP abuse. Flagged sessions are routed to manual review.

**Response shape (`velocity_analysis` object):**

```json
{
  "velocity_analysis": {
    "ip_address_hash": "a1b2c3...",
    "ip_reuse_count_1h": 3,
    "ip_reuse_count_24h": 8,
    "avg_step_duration_ms": 1200,
    "flags": ["RAPID_IP_REUSE", "BOT_LIKE_TIMING"],
    "score": 75
  }
}
```

**Velocity flags:**

| Flag | Trigger |
|------|---------|
| `RAPID_IP_REUSE` | 3+ verifications from the same IP in 1 hour |
| `BOT_LIKE_TIMING` | Steps completed faster than humanly possible |
| `BURST_ACTIVITY` | Sudden spike in verification volume |
| `MULTI_IP_ABUSE` | Same user_id from many different IPs |

Score range: 0–100. Highest flag wins. Score contributes to the composite risk score. Sessions with velocity flags are routed to `manual_review`.

---

## IP Geolocation Risk

Detects geographic risk signals by comparing the client IP location against the document's issuing country. Identifies Tor exit nodes, datacenter/VPN IPs, and high-risk jurisdictions.

**Response shape (`ip_geolocation` object):**

```json
{
  "ip_geolocation": {
    "ip_country": "RO",
    "document_country": "US",
    "country_match": false,
    "is_tor": false,
    "is_datacenter": true,
    "geo_risk_flags": ["COUNTRY_MISMATCH", "DATACENTER_IP"],
    "geo_risk_score": 60
  }
}
```

**Geo risk flags:**

| Flag | Trigger |
|------|---------|
| `COUNTRY_MISMATCH` | Client IP country differs from document nationality |
| `TOR_EXIT_NODE` | Client IP is a known Tor exit node |
| `DATACENTER_IP` | Client IP belongs to a datacenter/VPN provider |
| `HIGH_RISK_JURISDICTION` | Client IP is in a high-risk jurisdiction |

Score contributes to the composite risk score.

---

## Voice Authentication (Optional)

Optional speaker verification step after face matching. Users speak randomly generated digits; the system verifies both the spoken content (ASR transcription) and the speaker identity (512-dimensional embedding comparison). Enable per-developer via the settings API.

### Enable Voice Auth

```
PUT /api/developer/settings/voice-auth
Content-Type: application/json

{ "enabled": true }
```

Response: `{ "success": true, "voice_auth_enabled": true }`

When enabled, the verification pipeline adds an `AWAITING_VOICE` state after face matching. When disabled, verification completes at face match as usual.

### Request Voice Challenge

```
POST /api/v2/verify/:id/voice-challenge
```

Generates 6 random digits for the user to speak aloud. Challenge expires after 120 seconds. Session must be in `AWAITING_VOICE` state.

**Response (200):**

```json
{
  "success": true,
  "challenge_digits": "3 7 1 9 0 5",
  "expires_in": 120
}
```

### Submit Voice Capture

```
POST /api/v2/verify/:id/voice-capture
Content-Type: multipart/form-data
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| audio | File | Yes | Audio recording (WebM, WAV, or OGG). User speaking the challenge digits |

**Response (200):**

```json
{
  "success": true,
  "verification_id": "v_abc123",
  "status": "COMPLETE",
  "voice_match_results": {
    "similarity_score": 0.82,
    "passed": true,
    "threshold_used": 0.55,
    "challenge_verified": true,
    "challenge_digits": "3 7 1 9 0 5"
  }
}
```

**Technical details:**

- Speaker embeddings: 512-dimensional vectors from CAM++ model
- Similarity: cosine similarity between live capture voice and enrollment
- Thresholds: 0.55 (production), 0.50 (sandbox)
- All decisions are deterministic — no LLM in the gate path
- Rejection reasons: `VOICE_MATCH_FAILED` (speaker mismatch), `VOICE_CHALLENGE_FAILED` (wrong digits spoken)

---

## PEP Screening (Politically Exposed Persons)

Screens verified identities against PEP databases (heads of state, senior officials, political associates). PEP screening is a separate provider that can run alongside or instead of AML/sanctions screening.

**Configuration:** Set `AML_PROVIDER=pep` or combine with sanctions: `AML_PROVIDER=opensanctions,pep`. Per-developer opt-out via `aml_enabled: false`.

PEP results appear in the same `aml_screening` field as sanctions results. When multiple providers run, matches are merged and deduplicated. The `list_source` field in each match indicates whether the hit came from a sanctions list or a PEP database.

---

## Compliance Orchestration Engine

Developer-configurable rule engine. Define rules that automatically adjust verification requirements based on country, document type, user age, risk score, and custom metadata. Rules auto-apply at `POST /api/v2/verify/initialize` — zero code changes needed to your existing integration.

All compliance endpoints accept **either** `X-API-Key` (developer automation/SDK) **or** an organization-admin reviewer session cookie (Admin Dashboard UI).

### Endpoints

```
POST   /api/v2/compliance/rulesets           → Create ruleset
GET    /api/v2/compliance/rulesets           → List rulesets (includes rule_count)
GET    /api/v2/compliance/rulesets/:id       → Get ruleset with all rules
PUT    /api/v2/compliance/rulesets/:id       → Update ruleset metadata
DELETE /api/v2/compliance/rulesets/:id       → Delete ruleset + all rules (CASCADE)
POST   /api/v2/compliance/rulesets/:id/rules → Add rule to ruleset
PUT    /api/v2/compliance/rules/:id          → Update rule condition/action
DELETE /api/v2/compliance/rules/:id          → Delete rule
POST   /api/v2/compliance/evaluate           → Dry-run evaluation
```

### Condition Fields

| Field | Description |
|-------|-------------|
| `country` | ISO country code |
| `document_type` | passport, drivers_license, national_id |
| `user_age` | Calculated from DOB |
| `risk_score` | Composite score (0.0–1.0) |
| `aml_risk_level` | clear, potential_match, confirmed_match |
| `metadata.*` | Developer-supplied key-value data |

### Action Fields

| Field | Description |
|-------|-------------|
| `set_mode` | Override to age_only / document_only / identity / full |
| `require_address` | Enable address verification step |
| `require_liveness` | Set to passive or head_turn |
| `require_aml` | Force AML/sanctions screening |
| `force_manual_review` | Route to manual review |
| `set_flag` | Attach a custom flag (e.g. high_risk) |

### Condition Operators

| Operator | Description |
|----------|-------------|
| `eq` / `neq` | Equals / not equals |
| `in` / `not_in` | Value in / not in array |
| `gt` / `gte` / `lt` / `lte` | Numeric comparisons |
| `exists` | Field present (true) or absent (false) |
| `contains` | String contains substring |
| `all` / `any` / `not` | Combinators — nest conditions |

### Rule Structure

Conditions use leaf nodes (`{ "field": "country", "op": "in", "value": ["DE", "FR"] }`) or combinators (`{ "all": [...] }`, `{ "any": [...] }`, `{ "not": {...} }`).

**Example — EU minor rule:**

```json
{
  "condition": {
    "all": [
      { "field": "country", "op": "in", "value": ["DE", "FR", "IT", "ES"] },
      { "field": "user_age", "op": "lt", "value": 18 }
    ]
  },
  "action": {
    "set_mode": "full",
    "require_aml": true,
    "force_manual_review": true,
    "set_flag": "eu_minor"
  }
}
```

**Merging behavior:** When multiple rules match, actions merge additively. Verification mode escalates (never downgrades): `age_only < document_only < identity < full`. Boolean flags like `force_manual_review` stick once set. Custom flags from all matching rules are collected and deduplicated.

### Dry-Run Evaluate

```
POST /api/v2/compliance/evaluate
Content-Type: application/json

{
  "context": {
    "country": "DE",
    "user_age": 16,
    "document_type": "passport"
  }
}
```

**Response:**

```json
{
  "success": true,
  "matched_rules": 1,
  "matches": [
    {
      "ruleset": "EU Compliance",
      "rule": "Full verification + manual review for EU minors",
      "action": { "set_mode": "full", "require_aml": true, "force_manual_review": true, "set_flag": "eu_minor" }
    }
  ],
  "resolved_action": {
    "set_mode": "full",
    "require_aml": true,
    "force_manual_review": true,
    "flags": ["eu_minor"]
  }
}
```

Response includes `compliance_applied` when rules match during `POST /api/v2/verify/initialize`.

---

## Verifiable Credentials (W3C JWT-VC)

Issue W3C Verifiable Credentials for completed verifications. Credentials are signed with Ed25519 (EdDSA) and can be independently verified by any relying party using the issuer's DID document — no API key needed for verification.

```
GET  /api/v2/verify/:id/credential         → Issue JWT-VC (requires API key, vc_enabled)
POST /api/v2/verify/:id/credential/send    → Email credential with QR code (requires API key, vc_enabled)
GET  /api/v2/credentials/:jti/status       → Check revocation status (public, no auth)
POST /api/v2/credentials/:jti/revoke       → Revoke credential (requires API key)
GET  /.well-known/did.json                 → Issuer DID document (public)
```

**Client-side verification:** Resolve `did:web:api.idswyft.app` → fetch `/.well-known/did.json` → extract Ed25519 public key → verify JWT signature. ~15KB with `@noble/ed25519`.

---

## Identity Vault

Tokenized identity storage. Store verified identity data encrypted at rest (AES-256-GCM), reference it via opaque tokens.

```
POST   /api/v2/vault/store                    → Store verified identity, get vault token (requires API key, vault_enabled)
GET    /api/v2/vault/:token                   → Retrieve full identity data (requires API key)
GET    /api/v2/vault/:token/attributes/:attr  → Get single attribute e.g. age_over_21 (requires API key)
DELETE /api/v2/vault/:token                   → GDPR hard delete (requires API key)
GET    /api/v2/vault                          → List vault tokens, paginated (requires API key)
POST   /api/v2/vault/:token/share             → Create time-limited share link (requires API key)
GET    /api/v2/vault/share/:shareToken        → Access shared attributes (public, no auth)
```

---

## Batch Verification

Process hundreds of verifications at once. Batch mode runs the full pipeline (OCR, barcode/MRZ extraction, quality gates, cross-validation) but skips live capture — verifications end at `manual_review` status for human decision.

```
POST /api/v2/batch/upload          — Create batch job (multipart CSV + document URLs)
GET  /api/v2/batch/:id/status      — Get batch progress (items completed/failed/pending)
GET  /api/v2/batch/:id/results     — Get batch results (per-item status and extracted data)
POST /api/v2/batch/:id/cancel      — Cancel batch job
```

Items that fail quality gates are marked as `failed` with a rejection reason. Passed items are set to `manual_review` for human decision via the Review Dashboard.

---

## Address Verification

Verify proof-of-address documents (utility bills, bank statements). Requires a completed identity verification session.

```
POST /api/v2/verify/:id/address-document    — Upload address document
GET  /api/v2/verify/:id/address-status      — Get address verification status
```

---

## AML / Sanctions Screening

AML screening runs automatically on all non-sandbox verifications when the `AML_PROVIDER` environment variable is set (e.g., `opensanctions`, `offline`, or comma-separated for multiple providers). Screening happens after face matching (Gate 6) and results are persisted to the `aml_screenings` table.

**Configuration:**
- `AML_PROVIDER=opensanctions` — OpenSanctions API
- `AML_PROVIDER=offline` — local offline list
- `AML_PROVIDER=opensanctions,offline` — both run in parallel, results merged
- Developers can opt out via `aml_enabled: false` on their developer record
- Per-session override: `addons.aml_screening: false` disables for that session

| Risk Level | Risk Score | Outcome |
|-----------|-----------|---------|
| clear | 0 | Verification proceeds normally |
| potential_match | 60 | Routed to `manual_review` for human decision |
| confirmed_match | 100 | Hard reject (`failed`) |

The `aml_screening` field in the status response includes: `risk_level`, `match_found`, `match_count`, `matches` (array with `listed_name`, `list_source`, `score`, `match_type`), `lists_checked`, `screened_name`, `screened_dob`, and `screened_at`.

AML contributes 10% weight to the composite risk score (factor: `aml_screening`).

Lists checked: OFAC SDN, EU Sanctions, UN Sanctions (depends on configured provider).

---

## Monitoring & Re-verification

Schedule automatic re-verification for expiring documents.

```
POST /api/v2/monitoring/schedules              — Create re-verification schedule
GET  /api/v2/monitoring/expiring-documents      — List expiring documents
```

Webhook events: `document.expiry_warning`, `verification.reverification_due`.

---

## Review Dashboard

A web-based admin interface at `/admin/verifications` for reviewing, approving, and rejecting identity verifications. No code required — integrate the API, then use the dashboard for manual review decisions.

### Access & Roles

Developers invite team members from the Developer Portal (Settings → Team Management). Team members access the dashboard at `/admin/login` via passwordless OTP. Developers themselves do not access the Review Dashboard — they manage API keys in the Developer Portal and delegate verification review to their team.

Two roles exist for the Review Dashboard:

| Role | Capabilities |
|------|-------------|
| **Organization Admin** | Approve, reject, **override** verifications. Access analytics, manage GDPR erasure, manage team. |
| **Reviewer** | Approve or reject verifications only. Cannot override, access analytics, or manage GDPR. |

Both roles are scoped to the developer's verifications and sign in via OTP. A role badge in the dashboard header shows the user's access level.

### Team Management

Developers manage their team from the Developer Portal:

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/developer/reviewers/invite` | Invite a team member by email (body: `email`, `name?`, `role?`) |
| GET | `/api/developer/reviewers` | List all team members |
| DELETE | `/api/developer/reviewers/:id` | Revoke a team member's access |

The `role` parameter accepts `'reviewer'` (default) or `'admin'`.

Team member OTP authentication:

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/auth/reviewer/otp/send` | Send OTP to team member email (rate limited: 5/15min per IP) |
| POST | `/api/auth/reviewer/otp/verify` | Verify OTP, receive developer-scoped JWT with role claim (24h expiry) |

Team members can only see verifications belonging to the developer who invited them. They cannot access API keys, billing, or other developers' data.

### Dashboard Features

- **Stats bar** — real-time counts: total, review, pending, verified, failed (5 cards)
- **Role badge** — shows Organization Admin, Reviewer, or Platform Admin in the header
- **Filterable table** — columns: Preview, Verification ID, User ID, Status, Doc Type, Created, Actions
- **Status filter tabs** — All, Manual Review, Pending, Verified, Failed
- **Expandable detail panel** — document images, OCR data, cross-validation results, face match score, gate analysis with score bars, risk assessment with AML screening
- **Search** — search by verification ID or user ID

### Review Actions

| Action | Description | Webhook Event |
|--------|-------------|---------------|
| **Approve** | Sets status to `verified`. Decision is final. | `verification.verified` |
| **Reject** | Sets status to `failed`. Optional reason included in webhook. | `verification.failed` |
| **Override** | Sets any status (**Organization Admin only**). Reason required. | `verification.status_changed` |

All actions require a confirmation dialog. All actions are logged in the audit trail. Webhook notifications fire immediately after a decision.

### Admin Verification Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/admin/verifications?status=&page=&limit=` | List verifications with pagination and status filter |
| GET | `/api/admin/verification/:id` | Detail view with documents array, selfie URL, OCR data |
| PUT | `/api/admin/verification/:id/review` | Submit review decision (body: `decision`, `reason?`, `new_status?`) |
| GET | `/api/admin/dashboard` | Stats: total, pending, verified, failed, manual_review counts |

---

## Webhooks

Register webhook URLs to receive real-time notifications when verification events occur.

### Webhook Events

| Event | Trigger |
|-------|---------|
| `verification.verified` | Verification approved (automated or manual) |
| `verification.failed` | Verification failed or rejected |
| `verification.status_changed` | Status override from Review Dashboard |
| `document.expiry_warning` | Document approaching or past expiry (alert_type: 90_day, 60_day, 30_day, expired) |
| `verification.reverification_due` | Scheduled re-verification is due |
| `verification.age_check` | Age-only verification completed (includes `age_verification` in payload data) |

### Webhook Headers

Every delivery includes these headers:

| Header | Description |
|--------|-------------|
| `Content-Type` | Always `application/json` |
| `User-Agent` | `Idswyft-Webhooks/1.0` |
| `X-Idswyft-Webhook-Id` | Unique delivery ID — use to dedupe retries |
| `X-Idswyft-Delivery-Attempt` | Attempt number (1, 2, or 3) |
| `X-Idswyft-Sandbox` | `true` for sandbox webhooks, `false` for production. Use to route or alert without parsing the payload |
| `X-Idswyft-Verification-Mode` | `sandbox` or `production` (string form of the same signal) |
| `X-Idswyft-Signature` | HMAC-SHA256 signature when a signing secret is configured |

### Webhook Security

All webhooks are signed with HMAC-SHA256 using your webhook secret. Verify the `X-Idswyft-Signature` header to confirm authenticity. The signature is over the raw JSON body — verify before parsing to avoid replay/tamper attacks.

### Webhook Management

| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/api/developer/webhooks` | Register a webhook URL |
| GET | `/api/developer/webhooks` | List registered webhooks |
| DELETE | `/api/developer/webhooks/:id` | Delete a webhook |
| GET | `/api/developer/webhooks/:id/deliveries` | View delivery logs |
| POST | `/api/developer/webhooks/:id/deliveries/:did/resend` | Resend a failed delivery |

Webhooks retry up to 3 times on failure with exponential backoff.

---

## Idempotency

The verification endpoints accept an `Idempotency-Key` header (or the legacy `X-Idempotency-Key`) to make POST requests safely retryable. If a request times out or your network blips, retrying with the same key returns the stored response instead of creating a duplicate verification or processing the document twice.

### How it works

- Send any unique string per request — typically a UUIDv4 generated client-side.
- We cache the (key, developer) → response mapping for 24 hours.
- Retries within the window get the original response back, with header `Idempotent-Replayed: true` so observability can distinguish replays from fresh requests.
- Keys are scoped per-developer; the same key from a different account doesn't collide.

### Supported endpoints

| Endpoint | Notes |
|----------|-------|
| `POST /api/v2/verify/initialize` | Prevents duplicate sessions on retry |
| `POST /api/v2/verify/:id/front-document` | Prevents duplicate document rows |
| `POST /api/v2/verify/:id/back-document` | Same |
| `POST /api/v2/verify/:id/live-capture` | Same |

### Example

```bash
curl -X POST https://api.idswyft.app/api/v2/verify/initialize \
  -H "X-API-Key: ik_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"user_id": "...", "document_type": "auto"}'
```

### Notes

- The cache covers the response body and status code only — it does not validate that the request body matches the original. Reusing a key with a different body silently replays the original response. Generate a fresh key per logical operation.
- If you don't send the header, the endpoint behaves normally (no caching, no idempotency guarantee) — opt-in only.

---

## Verification Statuses

| Status | Description | Terminal |
|--------|-------------|----------|
| AWAITING_FRONT | Waiting for front document upload | No |
| AWAITING_BACK | Front processed, waiting for back document | No |
| CROSS_VALIDATING | Running cross-validation checks | No |
| AWAITING_LIVE | Cross-validation passed, waiting for live capture | No |
| FACE_MATCHING | Running liveness detection + face match | No |
| COMPLETE | All gates passed — verification successful | Yes |
| HARD_REJECTED | Rejected by a gate — verification failed | Yes |

**Final result values** (returned in `final_result` field):

| Value | Description |
|-------|-------------|
| `verified` | Identity confirmed by automated pipeline or manual approval |
| `failed` | Verification failed — document unreadable, face mismatch, or manually rejected |
| `manual_review` | Automated checks flagged something — requires human review via the Review Dashboard |

---

## Rate Limits

| Scope | Cloud Edition | Self-Hosted |
|-------|--------------|-------------|
| Per developer key | 1,000 req/hour | None by default (configurable) |
| Per user | 5 verifications/hour | None by default (configurable) |
| Monthly verification quota | 50/month (Starter) | Unlimited |

## HTTP Status Codes

| Code | Meaning |
|------|---------|
| 200/201 | Success |
| 400 | Bad request — validation error |
| 401 | Unauthorized — invalid or missing API key |
| 404 | Verification not found |
| 409 | Conflict — session hard-rejected, cannot proceed |
| 410 | Gone — handoff session expired |
| 429 | Rate limit exceeded |
| 500 | Internal server error |

---

## Self-Hosted / Community Edition

Idswyft is open source and can be self-hosted using Docker Compose.

### Quick Start

```bash
git clone https://github.com/team-idswyft/idswyft-community.git
cd idswyft-community
./install.sh     # Interactive setup — generates .env and starts containers
```

### Architecture

Four Docker containers:

| Container | Purpose | Port |
|-----------|---------|------|
| postgres | PostgreSQL database | 5432 |
| engine | ML verification engine (OCR, face detection, liveness, deepfake) | 3002 |
| api | Express API server — orchestrates verifications | 3001 |
| frontend | Nginx serving the React app | 80 |

The engine worker handles all ML-heavy processing (TensorFlow, ONNX, PaddleOCR) in isolation. The API server communicates with the engine via HTTP (`ENGINE_URL` env var).

### First-Run Setup

On first boot, navigate to the frontend. If no developer account exists, a setup wizard guides you through creating the first account and API key — no OTP required for the initial setup.

---

## Changelog

### v1.8.52 (2026-04-24)

**Fixed:**
- `redirect_url` now works on mobile verification pages (previously dropped during mobile auto-redirect)
- Open redirect vulnerability: `redirect_url` validated to only allow `http:` / `https:` protocols

**Added:**
- Face age estimation — cross-checks apparent face age against declared DOB as fraud signal
- PEP screening — screen against politically exposed persons databases
- Velocity / fraud detection — flags rapid resubmissions, bot timing, burst activity, multi-IP abuse
- IP geolocation risk — detects country mismatch, Tor, datacenter IPs, high-risk jurisdictions
- Voice authentication — optional speaker verification with 512-d embeddings (CAM++ model)
- Compliance orchestration engine — rule-based verification policy automation
- Verifiable Credentials (W3C JWT-VC) — issue and verify Ed25519-signed credentials
- Identity Vault — tokenized identity storage with AES-256-GCM encryption

### v1.8.2 (2026-04-02)

**Added:**
- Verification page branding — white-label with custom logo, accent color, and company name
- Branding settings endpoints: `GET/PUT /api/developer/settings/branding`, `POST /api/developer/branding/logo`
- Public page-config endpoint: `GET /api/v2/verify/page-config?api_key=...`
- Live preview in Developer Portal Settings
- Branding applied to desktop, mobile, and embedded verification flows

### v1.7.3 (2026-04-01)

**Added:**
- Age verification mode (`verification_mode: 'age_only'`) — lightweight 18+/21+ age check using front document OCR only
- `age_threshold` parameter (1-99) on initialize endpoint
- `age_verification` response object with `is_of_age` boolean (DOB never exposed)
- `verification.age_check` webhook event
- New rejection reasons: `DOB_NOT_FOUND`, `UNDERAGE`

### v1.7.2 (2026-03-30)

**Added:**
- AML screening auto-trigger — runs automatically on all non-sandbox verifications when `AML_PROVIDER` is set
- Multi-provider AML support — comma-separated `AML_PROVIDER` runs providers in parallel with match deduplication
- AML result persistence to `aml_screenings` DB table with full match details
- AML risk scoring factor (weight 0.10) integrated into composite risk score
- Address cross-validation — supplementary comparison between front OCR address and back barcode address
- Developer-level AML toggle: `aml_enabled` column (default true)

**Changed:**
- Risk scoring weights rebalanced to accommodate AML factor
- `aml_screening` in status response now includes `matches` array, `screened_name`, `screened_dob`
- `cross_validation_results` now includes optional `address_validation` field

### v1.7.0 (2026-03-27)

**Added:**
- Team invitation system with role-based access: Organization Admin (`role: 'admin'`) and Reviewer (`role: 'reviewer'`)
- Organization Admins can override verifications, access analytics, manage GDPR erasure, and manage team
- Reviewers can approve/reject only — no override, analytics, or GDPR access
- Role selector in Developer Portal Settings when inviting team members
- Team setup banner in Developer Portal when no Organization Admin exists
- Role badge in Review Dashboard header showing access level
- OTP auth endpoints: `POST /api/auth/reviewer/otp/send`, `POST /api/auth/reviewer/otp/verify`
- Team management: `POST /api/developer/reviewers/invite` (accepts `role` param), `GET /api/developer/reviewers`, `DELETE /api/developer/reviewers/:id`

**Changed:**
- Developer escalation to admin removed (POST `/api/auth/admin/escalate` returns 410 Gone)
- Admin verification endpoints scope queries by developer_id for org admin/reviewer tokens
- Analytics endpoints opened to Organization Admins (scoped to their developer)
- GDPR delete endpoint opened to Organization Admins with ownership verification
- Override restricted to Organization Admins and Platform Admins

### v1.6.0 (2026-03-26)

**Added:**
- Batch verification processing — full pipeline (OCR, cross-validation, quality gates) without live capture
- Admin status override with new_status field
- Webhook forwarding on admin review actions (approve, reject, override)
- Verification Management page at /admin/verifications

### v1.5.0 (2026-03-24)

**Added:**
- Community edition first-run setup wizard
- Mobile handoff link endpoint: `PATCH /api/verify/handoff/:token/link`
- Engine Worker microservice — standalone container for ML verification

**Changed:**
- Core API image reduced from ~2GB to ~250MB — ML dependencies isolated in engine container (~1.5GB)
- Docker Compose architecture: postgres + engine + api + frontend (4 containers)

### v1.2.0 (2026-03-20)

**Changed:**
- Liveness system: removed dead MediaPipe code path, renamed MultiFrame to HeadTurn
- Malformed liveness_metadata now returns HTTP 400 (VALIDATION_ERROR) instead of silently falling back to passive mode
- Removed legacy multi_frame_color challenge type alias — only head_turn is accepted

### v1.1.0 (2026-03-19)

**Added:**
- Visual authenticity checks (FFT, color distribution, deepfake detection)
- Webhook resend endpoint and delivery logs
- AML/sanctions screening addon
- Email OTP + GitHub OAuth authentication

### v1.0.0 (2025-12-01)

**Added:**
- Initial release — document OCR, face matching, verification pipeline
- RESTful API, API key management, webhook system, sandbox environment

---

## Support

- Developer Portal: https://idswyft.app/developer
- Review Dashboard: https://idswyft.app/admin/verifications
- Live Demo: https://idswyft.app/demo
- Documentation: https://idswyft.app/docs
- SDK: npm install @idswyft/sdk
- GitHub: https://github.com/team-idswyft/idswyft-community
- Email: support@idswyft.app

---

Generated from Idswyft API v1.8.52 — https://idswyft.app/docs
