YeboVerify — Product Requirements Document
B2B Identity Verification API for Africa Version 1.0 | March 2026
Table of Contents
- Executive Summary
- Vision & Mission
- Problem Statement
- Solution
- Features
- User Journeys
- Data Models
- API Reference
- Service Architecture
- SDKs
- Authentication
- Billing & Plans
- Technical Stack
- Security
- Gaps & Missing Features
1. Executive Summary
YeboVerify is a B2B identity verification API designed for African businesses. It provides automated KYC (Know Your Customer) verification by comparing a user's selfie against their government-issued ID document and extracting personal information via OCR.
Key Capabilities
- Face Comparison: AWS Rekognition-powered face matching between ID document photo and live selfie
- Document OCR: Google Gemini AI extraction of personal data from ID documents
- Multi-Platform SDKs: Node.js, React, React Native, Flutter, Kotlin (Android), Swift (iOS)
- Async Processing: Non-blocking verification with webhook notifications
- Multi-Tenant: API key authentication for business accounts
Primary Use Cases
- Customer onboarding for fintech/banking apps
- Age/identity verification for e-commerce
- Employee identity verification for HR platforms
- Compliance with KYC regulations
2. Vision & Mission
Vision
Become the leading identity verification infrastructure for Africa, enabling businesses to onboard customers securely and compliantly with world-class technology.
Mission
Provide simple, fast, and accurate identity verification that works with African ID documents, at a price point accessible to startups and enterprises alike.
Guiding Principles
- Security First: Documents encrypted at rest and in transit; never stored publicly
- Developer Experience: Simple integration via RESTful API and native SDKs
- African Focus: Optimized for African ID document formats (National IDs, passports)
- Speed: Sub-5-second verification times with async webhooks
3. Problem Statement
The Challenge
African businesses face significant hurdles implementing KYC:
- Global providers are expensive — per-verification pricing often exceeds $1-2 USD, prohibitive for high-volume African businesses
- Poor support for African documents — OCR models trained on Western documents fail on African ID formats
- Complex integration — most providers require weeks of integration work
- No local data residency — compliance concerns with data leaving the continent
Market Gap
Existing solutions (Jumio, Onfido, Veriff) are priced for Western markets and lack optimization for African ID documents. Local alternatives are fragmented and lack modern developer tooling.
Target Customers
| Segment | Use Case | Volume |
|---|---|---|
| Fintech startups | User onboarding | 100-10,000/month |
| Banks | Account opening | 10,000-100,000/month |
| E-commerce | Age verification | 1,000-50,000/month |
| HR platforms | Employee verification | 100-5,000/month |
| Government | Citizen services | 50,000+/month |
4. Solution
Overview
YeboVerify provides a single API endpoint that accepts:
- ID Front Image — photo of the front of government-issued ID
- Selfie — live photo of the person
- ID Back Image (optional) — back of ID for additional data
Within seconds, YeboVerify:
- Compares the face on the ID to the selfie using AWS Rekognition
- Extracts personal data (name, DOB, ID number) via Gemini OCR
- Returns a decision (APPROVED / REJECTED / NEEDS_REVIEW)
- Sends results to configured webhook
Architecture Overview
┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Client │────▶│ YeboVerify API │────▶│ Cloudflare R2 │
│ (SDK) │ │ (Cloud Run) │ │ (Storage) │
└─────────────┘ └────────┬─────────┘ └─────────────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌────────────────┐ ┌───────────────┐
│ AWS Rekognition│ │ Google Gemini │ │ PostgreSQL │
│ (Face Match) │ │ (OCR) │ │ (Neon) │
└───────────────┘ └────────────────┘ └───────────────┘5. Features
5.1 Core Features (Implemented)
| Feature | Description | Status |
|---|---|---|
| Face Comparison | AWS Rekognition CompareFaces API | ✅ Live |
| Document OCR | Gemini 2.5 Flash multimodal extraction | ✅ Live |
| Async Processing | Background processing with setImmediate | ✅ Live |
| Webhook Notifications | HMAC-signed POST to business webhook | ✅ Live |
| Multi-tenancy | API key per business account | ✅ Live |
| Business Dashboard | Stats endpoint with verification counts | ✅ Live |
| Verification History | Paginated list with status filtering | ✅ Live |
| Image Storage | Cloudflare R2 (private, presigned URLs) | ✅ Live |
5.2 Decision Logic
The verification decision is computed based on face match score and OCR confidence:
| Face Score | OCR Confidence | Decision | Confidence Level |
|---|---|---|---|
| ≥ 85% | ≥ 70% | APPROVED | High |
| ≥ 80% | ≥ 60% | APPROVED | Medium |
| ≥ 70% | ≥ 50% | NEEDS_REVIEW | Medium |
| < 60% | (skipped) | REJECTED | Low |
| < 70% | < 50% | REJECTED | Low |
5.3 OCR Extracted Fields
| Field | Description | Example |
|---|---|---|
surname | Family/last name | DLAMINI |
names | Given names | SIBUSISO JOHN |
dateOfBirth | DOB in YYYY-MM-DD | 1990-05-15 |
idNumber | National ID number | 9005155555083 |
sex | Gender | Male / Female |
chiefCode | Chief/regional code | 12345 |
issueDate | Document issue date | 2020-01-15 |
expiryDate | Document expiry date | 2030-01-15 |
documentType | Type of document | National ID |
5.4 Supported File Types
image/jpegimage/pngimage/webpimage/heicimage/heif
Max file size: 10MB per image
6. User Journeys
6.1 Business Integrator Journey
Persona: Developer at a fintech company implementing KYC
Steps
Registration
- Admin creates business account via admin API
- Receives API key (
yv_live_xxx...)
Integration
- Installs SDK for their platform (Node/React/Flutter/etc.)
- Configures webhook URL for async results
Testing
- Submits test verifications
- Verifies webhook signatures
Production
- Deploys SDK widget to production app
- Monitors verification stats via
/v1/account
// Node.js Integration Example
import { YeboVerify } from '@yeboverify/node';
const client = new YeboVerify({ apiKey: 'yv_live_xxx' });
// Configure webhook
await client.setWebhook('https://api.myapp.com/webhooks/kyc');
// Submit verification
const { verificationId } = await client.verify({
idFront: '/path/to/id.jpg',
selfie: '/path/to/selfie.jpg',
externalRef: 'user_123',
});
// Results arrive via webhook...6.2 End User Journey
Persona: Customer signing up for a fintech app
Steps
Prompt
- App displays "Verify your identity" screen
- YeboVerify widget appears
ID Front
- User photographs front of their National ID
- Widget validates image quality
ID Back (optional)
- User photographs back of ID
- Can skip if not required
Selfie
- User takes a selfie facing camera
- Instructions: good lighting, no glasses
Review
- User reviews all captured photos
- Can retake any photo
Submit
- Widget uploads to YeboVerify API
- User sees "Processing..." state
Result
- App receives webhook notification
- User sees approval or rejection
6.3 Admin Journey
Persona: YeboVerify platform administrator
Steps
Create Business
bashPOST /v1/businesses/register Authorization: Bearer ADMIN_SECRETMonitor
- View business verification stats
- Review flagged verifications (NEEDS_REVIEW)
Support
- Lookup verification by ID
- Investigate failed verifications
7. Data Models
7.1 Business Model
Represents a registered business customer.
model Business {
id String @id @default(cuid())
name String // Company name
email String @unique // Contact email
apiKey String @unique @default(cuid()) // API key (yv_live_xxx)
webhookUrl String? // Webhook endpoint URL
webhookSecret String? // HMAC signing secret (whsec_xxx)
active Boolean @default(true) // Account active/disabled
plan String @default("free") // Subscription plan
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
verifications Verification[]
}| Field | Type | Description |
|---|---|---|
id | cuid | Primary key |
name | String | Business display name |
email | String | Unique contact email |
apiKey | String | API key (format: yv_live_<32chars>) |
webhookUrl | String? | HTTPS URL for webhook delivery |
webhookSecret | String? | HMAC secret (format: whsec_<32chars>) |
active | Boolean | Account status |
plan | String | Plan tier: free, basic, pro, enterprise |
createdAt | DateTime | Account creation timestamp |
updatedAt | DateTime | Last update timestamp |
7.2 Verification Model
Represents a single identity verification request.
model Verification {
id String @id @default(cuid())
verificationId String @unique // Public ID (vrf_xxx)
businessId String // FK to Business
externalRef String? // Client's reference ID
status VerificationStatus @default(PENDING)
idFrontImage String // R2 storage key
idBackImage String? // R2 storage key (optional)
selfieImage String // R2 storage key
extractedData Json? // OCR results
ocrConfidence Float? // 0-100 OCR confidence
faceScore Float? // 0-100 face similarity
faceDecision String? // approved/rejected/needs_review
decision Decision? // Final decision
decisionReason String? // Human-readable reason
confidence String? // high/medium/low
processingMs Int? // Processing time in ms
webhookSent Boolean @default(false)
webhookSentAt DateTime?
webhookError String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?
business Business @relation(...)
@@index([businessId])
@@index([status])
}| Field | Type | Description |
|---|---|---|
id | cuid | Internal database ID |
verificationId | String | Public ID (format: vrf_<16chars>) |
businessId | String | Foreign key to Business |
externalRef | String? | Client's user/order ID |
status | VerificationStatus | Processing status |
idFrontImage | String | R2 object key for ID front |
idBackImage | String? | R2 object key for ID back |
selfieImage | String | R2 object key for selfie |
extractedData | Json? | Structured OCR output |
ocrConfidence | Float? | OCR confidence percentage (0-100) |
faceScore | Float? | Face similarity percentage (0-100) |
faceDecision | String? | Face comparison decision |
decision | Decision? | Final verification decision |
decisionReason | String? | Explanation of decision |
confidence | String? | Confidence level: high, medium, low |
processingMs | Int? | Total processing time |
webhookSent | Boolean | Webhook delivery status |
webhookSentAt | DateTime? | Webhook delivery timestamp |
webhookError | String? | Webhook error message if failed |
createdAt | DateTime | Verification creation time |
updatedAt | DateTime | Last update time |
completedAt | DateTime? | Processing completion time |
7.3 Enums
VerificationStatus
enum VerificationStatus {
PENDING // Just submitted, not yet processing
PROCESSING // Currently being verified
COMPLETED // Successfully processed
FAILED // Processing failed (error)
NEEDS_REVIEW // Requires manual review
}Decision
enum Decision {
APPROVED // Identity verified successfully
REJECTED // Identity verification failed
NEEDS_REVIEW // Manual review required
}8. API Reference
Base URL
https://api.yeboverify.com8.1 Health Check
GET /health
Returns API health status.
Response
{
"status": "ok",
"timestamp": "2026-03-19T10:00:00.000Z",
"service": "yeboverify-api",
"version": "1.0.0"
}8.2 Admin Endpoints
POST /v1/businesses/register
Create a new business account.
Authentication: Bearer token (ADMIN_SECRET)
Request Body
{
"name": "Acme Corp",
"email": "admin@acme.com",
"plan": "pro"
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Business name (1-255 chars) |
email | string | ✅ | Valid email address |
plan | string | ❌ | free, basic, pro, enterprise (default: free) |
Response (201 Created)
{
"success": true,
"data": {
"businessId": "clx...",
"apiKey": "yv_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456",
"name": "Acme Corp",
"email": "admin@acme.com",
"plan": "pro"
}
}8.3 Business Account Endpoints
All endpoints require X-API-Key header.
GET /v1/account
Get current business account info and stats.
Response
{
"success": true,
"data": {
"id": "clx...",
"name": "Acme Corp",
"email": "admin@acme.com",
"webhookUrl": "https://api.acme.com/webhooks/kyc",
"plan": "pro",
"createdAt": "2026-01-01T00:00:00.000Z",
"stats": {
"total": 1234,
"pending": 5,
"completed": 1200,
"approved": 1100,
"rejected": 80,
"needsReview": 20
}
}
}PUT /v1/account/webhook
Configure webhook URL for receiving verification results.
Request Body
{
"webhookUrl": "https://api.acme.com/webhooks/kyc",
"webhookSecret": "my-custom-secret"
}| Field | Type | Required | Description |
|---|---|---|---|
webhookUrl | string | ✅ | Valid HTTPS URL |
webhookSecret | string | ❌ | Custom secret (auto-generated if omitted) |
Response
{
"success": true,
"data": {
"webhookUrl": "https://api.acme.com/webhooks/kyc",
"webhookSecret": "whsec_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456",
"message": "Webhook configured successfully"
}
}8.4 Verification Endpoints
POST /v1/verify
Submit a new identity verification.
Content-Type: multipart/form-data
Form Fields
| Field | Type | Required | Description |
|---|---|---|---|
idFront | file | ✅ | Front of ID document (JPEG, PNG, WebP, HEIC) |
selfie | file | ✅ | User's selfie |
idBack | file | ❌ | Back of ID document |
externalRef | string | ❌ | Your reference ID (e.g., user ID) |
documentType | string | ❌ | Document type hint |
Response (202 Accepted)
{
"success": true,
"data": {
"verificationId": "vrf_abc123xyz456789",
"status": "pending",
"message": "Verification submitted successfully. Results will be sent via webhook."
}
}GET /v1/verifications/:verificationId
Get a specific verification by ID.
Response
{
"success": true,
"data": {
"verificationId": "vrf_abc123xyz456789",
"externalRef": "user_123",
"status": "COMPLETED",
"decision": "APPROVED",
"decisionReason": "High face match and OCR confidence",
"confidence": "high",
"faceScore": 92.5,
"ocrConfidence": 85.0,
"extractedData": {
"surname": "DLAMINI",
"names": "SIBUSISO JOHN",
"dateOfBirth": "1990-05-15",
"idNumber": "9005155555083",
"documentType": "National ID"
},
"processingMs": 3200,
"createdAt": "2026-03-19T10:00:00.000Z",
"completedAt": "2026-03-19T10:00:03.200Z"
}
}GET /v1/verifications
List verifications with pagination and filtering.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
limit | int | 20 | Items per page (max 100) |
status | string | - | Filter by status |
Response
{
"success": true,
"data": {
"verifications": [
{
"verificationId": "vrf_abc123xyz456789",
"externalRef": "user_123",
"status": "COMPLETED",
"decision": "APPROVED",
"confidence": "high",
"createdAt": "2026-03-19T10:00:00.000Z",
"completedAt": "2026-03-19T10:00:03.200Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1234,
"totalPages": 62
}
}
}8.5 Webhook Payload
When verification completes, YeboVerify POSTs to your webhook URL:
{
"event": "verification.completed",
"verificationId": "vrf_abc123xyz456789",
"externalRef": "user_123",
"status": "completed",
"decision": "approved",
"confidence": "high",
"faceScore": 92.5,
"ocrConfidence": 85.0,
"extractedData": {
"surname": "DLAMINI",
"names": "SIBUSISO JOHN",
"dateOfBirth": "1990-05-15",
"idNumber": "9005155555083",
"documentType": "National ID"
},
"timestamp": "2026-03-19T10:00:03.200Z"
}Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-YeboVerify-Signature | HMAC-SHA256 signature |
Signature Verification
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}8.6 Error Responses
All errors follow this format:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human readable error message"
}
}| Code | HTTP Status | Description |
|---|---|---|
MISSING_API_KEY | 401 | X-API-Key header not provided |
INVALID_API_KEY | 401 | API key not found or invalid |
ACCOUNT_DISABLED | 403 | Business account is disabled |
MISSING_AUTH | 401 | Admin Bearer token required |
INVALID_TOKEN | 401 | Invalid admin token |
MISSING_FILES | 400 | No files uploaded |
MISSING_ID_FRONT | 400 | ID front image required |
MISSING_SELFIE | 400 | Selfie image required |
INVALID_FILE_TYPE | 400 | Unsupported file type |
FILE_TOO_LARGE | 400 | File exceeds 10MB limit |
NOT_FOUND | 404 | Resource not found |
VALIDATION_ERROR | 400 | Request validation failed |
EMAIL_EXISTS | 409 | Email already registered |
VERIFICATION_ERROR | 500 | Verification processing error |
INTERNAL_ERROR | 500 | Unexpected server error |
9. Service Architecture
9.1 Face Comparison Service (AWS Rekognition)
File: src/services/aws-rekognition.service.ts
Uses AWS Rekognition CompareFaces API to match the face on an ID document with a selfie.
Configuration
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=us-east-1Key Methods
| Method | Description |
|---|---|
compareFaces(sourceBuffer, targetBuffer) | Compare two face images |
getVerificationDecision(result) | Convert score to decision |
isEnabled() | Check if AWS credentials are configured |
Face Comparison Result
interface FaceComparisonResult {
similarity: number; // 0-100 match score
confidence: number; // Source face detection confidence
sourceImageFace: {
boundingBox: { width, height, left, top };
confidence: number;
};
faceMatches: Array<{
similarity: number;
face: { boundingBox, confidence };
}>;
unmatchedFaces: unknown[];
}Decision Thresholds
| Similarity | Decision | Confidence |
|---|---|---|
| ≥ 90% | approved | high |
| ≥ 80% | needs_review | medium |
| ≥ 70% | needs_review | low |
| < 70% | rejected | low |
9.2 OCR Service (Google Gemini)
File: src/services/gemini-ocr.service.ts
Uses Google Gemini 2.5 Flash multimodal model to extract personal information from ID document images.
Configuration
GEMINI_API_KEY=AIzaSy...Key Methods
| Method | Description |
|---|---|
extractPersonalInfo(imageBuffer) | Extract data from ID image |
validatePersonalInfo(info) | Validate extracted data completeness |
isEnabled() | Check if API key is configured |
OCR Result
interface OCRResult {
personalInfo: {
surname?: string;
names?: string;
dateOfBirth?: string;
sex?: string;
idNumber?: string;
chiefCode?: string;
issueDate?: string;
expiryDate?: string;
documentType?: string;
confidence: number;
};
extractedText: string[];
confidence: number;
success: boolean;
error?: string;
}Prompt Engineering
The service uses a structured prompt requesting JSON output:
Analyze this ID document image and extract the following personal information in JSON format.
Be very precise and only extract information that is clearly visible and readable.
...
Return ONLY a JSON object with the fields above.9.3 Storage Service (Cloudflare R2)
File: src/services/storage.service.ts
Uses Cloudflare R2 (S3-compatible) for storing document images. Images are stored privately — never exposed publicly.
Configuration
R2_ACCOUNT_ID=9f15f12d867a59b212ed2ae3cf4615ca
R2_BUCKET=yeboverify-documents
R2_ACCESS_KEY_ID=...
R2_SECRET_ACCESS_KEY=...Key Methods
| Method | Description |
|---|---|
uploadFile(buffer, key, mimeType) | Upload file to R2 |
downloadBuffer(key) | Download file as Buffer |
getSignedUrl(key, expiresIn) | Generate presigned URL (15 min default) |
deleteFile(key) | Delete file from R2 |
Storage Structure
yeboverify-documents/
├── verifications/
│ ├── vrf_abc123xyz/
│ │ ├── id-front-1710844800000
│ │ ├── id-back-1710844800001
│ │ └── selfie-1710844800002
│ └── vrf_def456uvw/
│ └── ...9.4 Webhook Service
File: src/services/webhook.service.ts
Handles webhook delivery with retry logic and HMAC signing.
Key Features
- Retry: 3 attempts with exponential backoff (1s, 2s, 3s)
- Signing: HMAC-SHA256 signature in
X-YeboVerify-Signatureheader - Tracking: Records delivery status and errors in database
Webhook Payload
interface WebhookPayload {
event: 'verification.completed';
verificationId: string;
externalRef: string | null;
status: 'completed' | 'failed' | 'needs_review';
decision: 'approved' | 'rejected' | 'needs_review';
confidence: 'high' | 'medium' | 'low';
faceScore: number;
ocrConfidence: number;
extractedData: {
surname?: string;
names?: string;
dateOfBirth?: string;
idNumber?: string;
documentType?: string;
};
timestamp: string;
}9.5 Verification Flow
1. Client submits POST /v1/verify with images
│
▼
2. Upload images to R2 (private bucket)
│
▼
3. Create Verification record (status: PENDING)
│
▼
4. Return 202 Accepted with verificationId
│
▼
5. Background: setImmediate → processVerification()
│
├── Update status to PROCESSING
│
├── Download images from R2
│
├── AWS Rekognition: compareFaces()
│ │
│ ├── If faceScore < 60%: REJECT (skip OCR)
│ │
│ └── Continue...
│
├── Gemini OCR: extractPersonalInfo()
│
├── Calculate final decision
│
├── Update Verification record
│
└── Send webhook (if configured)10. SDKs
YeboVerify provides official SDKs for all major platforms.
10.1 Node.js SDK
Package: @yeboverify/node
Installation
npm install @yeboverify/nodeUsage
import { YeboVerify } from '@yeboverify/node';
const client = new YeboVerify({ apiKey: 'yv_live_xxx' });
// Submit verification
const { verificationId } = await client.verify({
idFront: '/path/to/id.jpg', // File path, Buffer, or base64
selfie: '/path/to/selfie.jpg',
externalRef: 'user_123',
});
// Get verification
const verification = await client.getVerification(verificationId);
// Poll until complete (for testing)
const result = await client.pollVerification(verificationId, {
intervalMs: 2000,
timeoutMs: 60000,
});
// Verify webhook signature
const isValid = client.verifyWebhook(rawBody, signature, webhookSecret);Types
interface YeboVerifyConfig {
apiKey: string;
baseUrl?: string; // defaults to https://api.yeboverify.com
}
interface VerifyOptions {
idFront: Buffer | string;
selfie: Buffer | string;
idBack?: Buffer | string;
externalRef?: string;
documentType?: string;
}
interface Verification {
verificationId: string;
externalRef?: string | null;
status: VerificationStatus;
decision?: VerificationDecision | null;
decisionReason?: string | null;
confidence?: VerificationConfidence | null;
faceScore?: number | null;
ocrConfidence?: number | null;
extractedData?: ExtractedData | null;
processingMs?: number | null;
createdAt: string;
completedAt?: string | null;
}10.2 React SDK
Package: @yeboverify/react
Installation
npm install @yeboverify/reactUsage
import { YeboVerifyWidget } from '@yeboverify/react';
function KYCPage() {
return (
<YeboVerifyWidget
apiKey="yv_live_xxx"
externalRef={currentUser.id}
documentType="National ID"
requireIdBack={false}
primaryColor="#16a34a"
onSubmit={(verificationId) => {
console.log('Submitted:', verificationId);
}}
onError={(error) => {
console.error('Error:', error);
}}
/>
);
}Props
| Prop | Type | Required | Description |
|---|---|---|---|
apiKey | string | ✅ | Your YeboVerify API key |
baseUrl | string | ❌ | API base URL |
externalRef | string | ❌ | Your reference ID |
documentType | string | ❌ | Document type hint |
requireIdBack | boolean | ❌ | Require back of ID |
primaryColor | string | ❌ | Brand color (default: #16a34a) |
onSubmit | function | ❌ | Called on successful submission |
onResult | function | ❌ | Called when result received |
onError | function | ❌ | Called on error |
className | string | ❌ | Custom CSS class |
10.3 React Native SDK
Package: @yeboverify/react-native
Installation
npm install @yeboverify/react-native
npx expo install expo-image-pickerUsage
import { YeboVerifyScreen } from '@yeboverify/react-native';
function KYCScreen() {
return (
<YeboVerifyScreen
apiKey="yv_live_xxx"
externalRef={user.id}
documentType="National ID"
requireIdBack={false}
primaryColor="#16a34a"
onSubmit={(verificationId) => {
navigation.navigate('KYCComplete', { verificationId });
}}
onError={(error) => {
Alert.alert('Error', error.message);
}}
/>
);
}10.4 Flutter SDK
Package: yeboverify
Installation
# pubspec.yaml
dependencies:
yeboverify: ^1.0.0
image_picker: ^1.0.0Usage
import 'package:yeboverify/yeboverify.dart';
// Programmatic API
final client = YeboVerify(apiKey: 'yv_live_xxx');
final verificationId = await client.verify(
idFront: File('/path/to/id.jpg'),
selfie: File('/path/to/selfie.jpg'),
externalRef: user.id,
);
// Widget
YeboVerifyWidget(
apiKey: 'yv_live_xxx',
externalRef: user.id,
requireIdBack: false,
primaryColor: Color(0xFF16a34a),
onSubmit: (verificationId) {
Navigator.pushNamed(context, '/kyc-complete');
},
onError: (error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(error.message)),
);
},
)Models
enum VerificationStatus { pending, processing, completed, failed, needsReview }
enum VerificationDecision { approved, rejected, needsReview }
enum VerificationConfidence { high, medium, low }
class Verification {
final String verificationId;
final String? externalRef;
final VerificationStatus status;
final VerificationDecision? decision;
final String? decisionReason;
final VerificationConfidence? confidence;
final double? faceScore;
final double? ocrConfidence;
final ExtractedData? extractedData;
final int? processingMs;
final DateTime createdAt;
final DateTime? completedAt;
bool get isApproved => decision == VerificationDecision.approved;
bool get isPending => status == VerificationStatus.pending || status == VerificationStatus.processing;
}10.5 Kotlin SDK (Android)
Package: com.yeboverify
Installation
// build.gradle
dependencies {
implementation 'com.yeboverify:yeboverify:1.0.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}Usage
// Programmatic API
val client = YeboVerify(apiKey = "yv_live_xxx")
val verificationId = client.verify(
idFront = File("/path/to/id.jpg"),
selfie = File("/path/to/selfie.jpg"),
externalRef = user.id,
)
// Launch built-in UI
val intent = client.launchVerificationFlow(
context = this,
externalRef = user.id,
documentType = "National ID",
requireIdBack = false,
)
startActivityForResult(intent, REQUEST_CODE_VERIFY)
// Handle result
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_VERIFY && resultCode == RESULT_OK) {
val verificationId = data?.getStringExtra(YeboVerifyActivity.EXTRA_VERIFICATION_ID)
// Handle success
}
}10.6 Swift SDK (iOS)
Package: YeboVerify (Swift Package)
Installation
// Package.swift
dependencies: [
.package(url: "https://github.com/yeboverify/swift-sdk.git", from: "1.0.0")
]Usage
// Programmatic API
let client = YeboVerify(apiKey: "yv_live_xxx")
let verificationId = try await client.verify(
idFront: idFrontImageData,
selfie: selfieImageData,
externalRef: user.id
)
// SwiftUI Widget
YeboVerifyView(
apiKey: "yv_live_xxx",
externalRef: user.id,
documentType: "National ID",
requireIdBack: false,
primaryColor: Color.green
) { verificationId in
print("Submitted:", verificationId)
} onError: { error in
print("Error:", error.message)
}11. Authentication
11.1 Business API Key Authentication
All verification and account endpoints use API key authentication:
X-API-Key: yv_live_aBcDeFgHiJkLmNoPqRsTuVwXyZ123456API Key Format: yv_live_<32 alphanumeric characters>
Generated by: customAlphabet('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 32)
11.2 Admin Bearer Token
Admin endpoints (business registration) use Bearer token:
Authorization: Bearer YOUR_ADMIN_SECRETConfigured via: ADMIN_SECRET environment variable
11.3 Webhook Signature
Webhooks are signed with HMAC-SHA256:
X-YeboVerify-Signature: sha256=abc123...Secret Format: whsec_<32 alphanumeric characters>
12. Billing & Plans
12.1 Current Plan Structure
| Plan | Description | Default |
|---|---|---|
free | Trial tier | ✅ |
basic | Starter businesses | |
pro | Growing businesses | |
enterprise | High-volume |
Note: Billing/payments are not yet implemented. Plans are stored but not enforced.
12.2 Planned Pricing Model
| Plan | Verifications/month | Price/verification | Features |
|---|---|---|---|
| Free | 100 | $0 | Basic OCR |
| Basic | 1,000 | $0.30 | + Webhooks |
| Pro | 10,000 | $0.20 | + Priority processing |
| Enterprise | Unlimited | Custom | + SLA, support |
13. Technical Stack
13.1 Backend
| Component | Technology |
|---|---|
| Runtime | Node.js 22+ |
| Language | TypeScript 5.7 |
| Framework | Express.js 4.21 |
| Database | PostgreSQL (Neon serverless) |
| ORM | Prisma 6.3 |
| Validation | Zod 3.24 |
| Logging | Winston 3.17 |
13.2 External Services
| Service | Provider | Purpose |
|---|---|---|
| Face Recognition | AWS Rekognition | Face comparison |
| OCR | Google Gemini 2.5 Flash | Document data extraction |
| Storage | Cloudflare R2 | Image storage |
| Database | Neon | PostgreSQL |
| Hosting | Google Cloud Run | Container hosting |
| CI/CD | Google Cloud Build | Build & deploy |
13.3 Dependencies
{
"dependencies": {
"@aws-sdk/client-rekognition": "^3.637.0",
"@aws-sdk/client-s3": "^3.1006.0",
"@aws-sdk/s3-request-presigner": "^3.1006.0",
"@google/generative-ai": "^0.21.0",
"@prisma/client": "^6.3.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"multer": "^1.4.5-lts.1",
"nanoid": "^3.3.7",
"winston": "^3.17.0",
"zod": "^3.24.1"
}
}13.4 Deployment
- Region: europe-west1 (Belgium)
- Platform: Google Cloud Run
- Build: Cloud Build with Docker
- Deploy Script:
deploy.sh(required for secrets)
14. Security
14.1 Data Protection
| Measure | Implementation |
|---|---|
| Encryption at Rest | R2 server-side encryption |
| Encryption in Transit | TLS 1.3 |
| No Public URLs | Images only accessible via presigned URLs (15 min expiry) |
| API Key Hashing | Keys stored as plain text (TODO: hash) |
| Webhook Signing | HMAC-SHA256 with constant-time comparison |
14.2 Access Control
| Layer | Control |
|---|---|
| Admin APIs | Bearer token (ADMIN_SECRET) |
| Business APIs | API key (X-API-Key) |
| Webhooks | HMAC signature verification |
| Storage | Private bucket + presigned URLs |
14.3 Input Validation
- File types: Allowlist (JPEG, PNG, WebP, HEIC, HEIF)
- File size: Max 10MB per image
- Request body: Zod schema validation
- ID format: Regex validation
15. Gaps & Missing Features
15.1 Critical Gaps
| Gap | Priority | Description |
|---|---|---|
| No rate limiting | 🔴 High | API has no rate limits — vulnerable to abuse |
| No billing/payments | 🔴 High | Plans exist but not enforced |
| No Stripe integration | 🔴 High | No payment processing |
| No API key rotation | 🔴 High | Cannot rotate compromised keys |
| API keys in plaintext | 🔴 High | Should be hashed |
15.2 Important Missing Features
| Feature | Priority | Description |
|---|---|---|
| Admin dashboard | 🟡 Medium | Web UI for managing businesses |
| Manual review UI | 🟡 Medium | Interface for reviewing NEEDS_REVIEW cases |
| Usage analytics | 🟡 Medium | Detailed verification metrics |
| Document type detection | 🟡 Medium | Auto-detect ID type (passport vs national ID) |
| Liveness detection | 🟡 Medium | Ensure selfie is live, not a photo |
| Email notifications | 🟡 Medium | Notify business on verification completion |
| Retry webhook endpoint | 🟡 Medium | API to retry failed webhooks |
15.3 Nice-to-Have
| Feature | Priority | Description |
|---|---|---|
| Batch verification | 🟢 Low | Submit multiple verifications at once |
| Image quality scoring | 🟢 Low | Reject blurry/dark images upfront |
| Document expiry check | 🟢 Low | Flag expired IDs |
| Multi-language support | 🟢 Low | SDK widgets in local languages |
| Sandbox mode | 🟢 Low | Test mode with mock responses |
| GraphQL API | 🟢 Low | Alternative to REST |
15.4 SDK Gaps
| SDK | Gap |
|---|---|
| Node.js | No retry logic, no streaming upload |
| React | No camera capture (gallery only) |
| React Native | Requires expo-image-picker (not bare RN) |
| Flutter | No liveness detection integration |
| Kotlin | No Jetpack Compose UI (Activity only) |
| Swift | iOS 16+ only, no UIKit support |
15.5 Technical Debt
| Item | Description |
|---|---|
setImmediate for background jobs | Should use proper job queue (Bull/BullMQ) |
| Single-file uploads | Should support streaming for large files |
| No retry mechanism | Failed processing doesn't retry automatically |
| No dead letter queue | Failed webhooks lost after 3 attempts |
| No idempotency keys | Duplicate submissions not prevented |
Appendix A: Environment Variables
# Server
PORT=3010
NODE_ENV=production
# Database (Neon PostgreSQL)
DATABASE_URL=postgresql://user:pass@host:5432/yeboverify?sslmode=require
# Cloudflare R2 Storage
R2_ACCOUNT_ID=9f15f12d867a59b212ed2ae3cf4615ca
R2_BUCKET=yeboverify-documents
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
# AWS Rekognition
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=us-east-1
# Google Gemini OCR
GEMINI_API_KEY=
# Admin API
ADMIN_SECRET=change-this-secretAppendix B: API Response Codes
| HTTP Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created |
| 202 | Accepted (async processing) |
| 400 | Bad Request (validation error) |
| 401 | Unauthorized (missing/invalid auth) |
| 403 | Forbidden (account disabled) |
| 404 | Not Found |
| 409 | Conflict (duplicate) |
| 500 | Internal Server Error |
Appendix C: Glossary
| Term | Definition |
|---|---|
| KYC | Know Your Customer — identity verification requirements |
| OCR | Optical Character Recognition — extracting text from images |
| Face Score | 0-100 percentage indicating face similarity |
| Webhook | HTTP POST callback when verification completes |
| HMAC | Hash-based Message Authentication Code |
| R2 | Cloudflare's S3-compatible object storage |
| Presigned URL | Temporary, signed URL for private object access |
Document generated: March 2026Last updated: March 19, 2026