# 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 ` 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 ``` > **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 ( 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 ( <> {result &&

Result: {result.finalResult}

} ); } ``` ### 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