Troubleshooting Knowledge Base
Comprehensive troubleshooting guide for VerifyHuman platform integration, covering all products, integration methods, and common issues.
Table of Contents
- Quick Diagnostics
- API Integration Issues
- Widget Integration Issues
- Hosted Flows Issues
- Product-Specific Troubleshooting
- SDK-Specific Troubleshooting
- Mobile Integration Issues
- Authentication & API Keys
- Token Validation Issues
- Credit & Billing Issues
- Performance Issues
- Security Issues
- Environment & Configuration
- Testing & Debugging
- Error Code Reference
- Getting Additional Help
Quick Diagnostics
Diagnostic Checklist
Before diving into specific issues, run through this quick checklist:
| Check | How to Verify | Common Fix |
|---|---|---|
| API Key Valid | Key starts with vhk- prefix |
Generate new key in dashboard |
| API Key Active | Dashboard shows key as "Active" | Enable or regenerate key |
| Product Scopes | Key has required product scope enabled | Edit key scopes in dashboard |
| Sufficient Credits | Dashboard → Credits shows positive balance | Add credits or upgrade plan |
| Domain Whitelisted | Dashboard → API Keys → Domain Whitelist | Add your domain |
| HTTPS Enabled | Site uses https:// |
Enable SSL certificate |
| Correct Endpoint | Using product-specific endpoint | Check API documentation |
Service Status
Check current API status and any ongoing incidents:
- Status Page: status.verifyhuman.io
Test Your API Key
curl -X POST https://app.verifyhuman.io/api/verify \
-F "clientKey=vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
-F "file=@test-selfie.jpg"
API Integration Issues
Error: "Invalid API Key"
Symptoms:
{
"error": "Invalid API key",
"status": "ERROR"
}
Root Causes:
Wrong API Key Format
- Each product requires its specific key prefix
- VerifyHuman:
vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6orvhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 - VerifyAge:
vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6orvhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 - VerifyIdentity:
vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6orvhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 - VerifyTrust:
vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Key Revoked or Expired
- Check key status in dashboard
- Keys can be revoked manually or due to security policies
Environment Mismatch
- Test keys don't work in production
- Live keys shouldn't be used for testing
Solutions:
# WRONG - missing prefix
headers = {'Authorization': 'Bearer abc123xyz'}
# WRONG - wrong product prefix
api_key = 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6' # Using VerifyHuman key for VerifyAge
response = requests.post('/api/v1/verify-age/init', ...)
# CORRECT - matching product and key
api_key = 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6' # VerifyAge key for VerifyAge endpoint
response = requests.post('/api/v1/verify-age/init',
data={'apiKey': api_key})
Verification Script:
import requests
def verify_api_key(api_key):
"""Test if API key is valid and check its properties"""
# Determine product from prefix
prefix = api_key.split('_')[0] if '_' in api_key else 'unknown'
endpoints = {
'vhk': '/api/verify',
'vage': '/api/v1/verify-age/init',
'kyc': '/api/identity/v1/submit',
'ident': '/api/identity/v1/submit',
'kycp': '/api/kyc/v1/submit_plus'
}
if prefix not in endpoints:
return {'valid': False, 'error': f'Unknown prefix: {prefix}'}
# Perform a minimal validation request
# (actual verification requires proper payload)
return {'valid': True, 'product': prefix, 'endpoint': endpoints[prefix]}
# Usage
result = verify_api_key('vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6')
print(result)
Error: "Insufficient Credits"
Symptoms:
{
"error": "Insufficient credits",
"credits_required": 30,
"credits_available": 15
}
Credit Costs by Product:
| Product | Credits per Verification |
|---|---|
| VerifyHuman | 1 credit |
| VerifyAge | 10 credits |
| VerifyIdentity | 30 credits |
| VerifyTrust | 50 credits |
Solutions:
Check Current Balance:
- Log in to your Dashboard
- View credit balance in the sidebar or Credits page
Set Up Low Credit Alerts:
- Dashboard → Settings → Notifications
- Enable "Low Credit Warning" alerts
- Set threshold (e.g., alert when below 100 credits)
Purchase Options:
- Upgrade subscription plan in dashboard
- Purchase prepaid credit packages
- Enable auto-refill (Pro/Enterprise plans)
Error: "Rate Limit Exceeded"
Symptoms:
{
"error": "Rate limit exceeded",
"retry_after": 60,
"limit": "60 requests/minute"
}
Rate Limits by Plan:
| Plan | Requests/Minute | Verifications/Month |
|---|---|---|
| Free | 10 | 20 |
| Starter | 60 | 1,000 |
| Growth | 300 | 10,000 |
| Pro | 500 | 50,000 |
| Enterprise | 1,000 | Unlimited |
Solutions:
- Implement Exponential Backoff:
async function verifyWithRetry(file, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await verify(file);
} catch (error) {
if (error.status === 429) {
const retryAfter = error.headers?.['Retry-After'] || Math.pow(2, attempt);
console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
await sleep(retryAfter * 1000);
continue;
}
throw error;
}
}
throw new Error('Max retries exceeded');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
- Implement Request Queuing:
import time
from collections import deque
from threading import Lock
class RateLimiter:
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = deque()
self.lock = Lock()
def acquire(self):
with self.lock:
now = time.time()
# Remove old requests outside window
while self.requests and self.requests[0] < now - self.window_seconds:
self.requests.popleft()
if len(self.requests) >= self.max_requests:
wait_time = self.requests[0] + self.window_seconds - now
time.sleep(wait_time)
return self.acquire()
self.requests.append(now)
return True
# Usage
limiter = RateLimiter(max_requests=60, window_seconds=60)
limiter.acquire()
response = verify_api_call()
- Cache Verification Results:
from functools import lru_cache
import hashlib
@lru_cache(maxsize=1000)
def get_cached_verification(image_hash, user_id):
"""Cache verifications for the same user/image combination"""
# Only hits API if not in cache
return perform_verification(image_hash, user_id)
def verify_user(image_data, user_id):
# Create hash of image for caching
image_hash = hashlib.sha256(image_data).hexdigest()
return get_cached_verification(image_hash, user_id)
Error: "File Too Large"
Symptoms:
{
"error": "File size exceeds maximum allowed (10MB)",
"file_size": "15.2MB",
"max_size": "10MB"
}
File Requirements:
| Requirement | Value |
|---|---|
| Max File Size | 10 MB |
| Supported Formats | JPG, JPEG, PNG, WebP |
| Recommended Resolution | 1920x1080 or less |
| Minimum Face Size | 100x100 pixels |
Solutions:
- Client-Side Compression (JavaScript):
async function compressImage(file, options = {}) {
const {
maxSizeMB = 5,
maxWidth = 1920,
maxHeight = 1080,
quality = 0.85
} = options;
// Skip if already small enough
if (file.size <= maxSizeMB * 1024 * 1024) {
return file;
}
return new Promise((resolve, reject) => {
const img = new Image();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
img.onload = () => {
let { width, height } = img;
// Calculate new dimensions
if (width > maxWidth) {
height = (height / width) * maxWidth;
width = maxWidth;
}
if (height > maxHeight) {
width = (width / height) * maxHeight;
height = maxHeight;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
if (blob) {
resolve(new File([blob], file.name, { type: 'image/jpeg' }));
} else {
reject(new Error('Failed to compress image'));
}
},
'image/jpeg',
quality
);
};
img.onerror = () => reject(new Error('Failed to load image'));
img.src = URL.createObjectURL(file);
});
}
// Usage
const compressedFile = await compressImage(originalFile);
formData.append('file', compressedFile);
- Server-Side Compression (Python):
from PIL import Image
import io
def compress_image(image_data, max_size_mb=5, quality=85):
"""Compress image to meet size requirements"""
img = Image.open(io.BytesIO(image_data))
# Convert to RGB if necessary (for PNG with transparency)
if img.mode in ('RGBA', 'P'):
img = img.convert('RGB')
# Resize if too large
max_dimension = 1920
if max(img.size) > max_dimension:
ratio = max_dimension / max(img.size)
new_size = tuple(int(dim * ratio) for dim in img.size)
img = img.resize(new_size, Image.LANCZOS)
# Compress with decreasing quality until under max size
output = io.BytesIO()
current_quality = quality
while current_quality > 10:
output.seek(0)
output.truncate()
img.save(output, format='JPEG', quality=current_quality, optimize=True)
if output.tell() <= max_size_mb * 1024 * 1024:
break
current_quality -= 10
return output.getvalue()
- Node.js Compression:
const sharp = require('sharp');
async function compressImage(buffer) {
return await sharp(buffer)
.resize(1920, 1080, { fit: 'inside', withoutEnlargement: true })
.jpeg({ quality: 85, progressive: true })
.toBuffer();
}
Error: "Domain Not Whitelisted"
Symptoms:
{
"error": "API key restricted to whitelisted domains",
"your_domain": "unauthorized-site.com",
"whitelisted_domains": ["example.com", "app.example.com"]
}
Solutions:
Add Domain in Dashboard:
Dashboard → API Keys → [Your Key] → Domain Whitelist → Add DomainDomain Format Requirements:
- Include protocol:
https://yoursite.com - Add both
wwwand non-wwwversions - Add subdomains separately:
app.yoursite.com - For localhost testing:
http://localhost:3000
- Include protocol:
Wildcard Domains (Enterprise only):
*.yoursite.com // Matches all subdomainsCheck Current Whitelist:
- Dashboard → API Keys → [Your Key] → Domain Whitelist
- View all currently whitelisted domains
Error: "Invalid File Format"
Symptoms:
{
"error": "Unsupported file format",
"received": "image/gif",
"supported": ["image/jpeg", "image/png", "image/webp"]
}
Solutions:
- Validate Before Upload:
const SUPPORTED_FORMATS = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
function validateFile(file) {
if (!SUPPORTED_FORMATS.includes(file.type)) {
throw new Error(`Unsupported format: ${file.type}. Use JPG, PNG, or WebP.`);
}
return true;
}
- Convert to Supported Format:
async function convertToJpeg(file) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = await createImageBitmap(file);
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
return new Promise(resolve => {
canvas.toBlob(blob => {
resolve(new File([blob], 'photo.jpg', { type: 'image/jpeg' }));
}, 'image/jpeg', 0.92);
});
}
Error: "Request Timeout"
Symptoms:
{
"error": "Request timeout",
"timeout": 30000,
"message": "Verification processing exceeded time limit"
}
Solutions:
- Increase Client Timeout:
import requests
response = requests.post(
'https://app.verifyhuman.io/api/verify',
files={'file': image_file},
data={'clientKey': API_KEY},
timeout=(10, 60) # (connect timeout, read timeout)
)
- Use Async Processing for Large Batches:
import asyncio
import aiohttp
async def verify_async(session, image_data, api_key):
async with session.post(
'https://app.verifyhuman.io/api/verify',
data={'clientKey': api_key, 'file': image_data},
timeout=aiohttp.ClientTimeout(total=60)
) as response:
return await response.json()
async def batch_verify(images, api_key):
async with aiohttp.ClientSession() as session:
tasks = [verify_async(session, img, api_key) for img in images]
return await asyncio.gather(*tasks, return_exceptions=True)
- Implement Retry with Status Polling:
async function verifyWithPolling(file, apiKey) {
// Submit verification
const submitResponse = await fetch('/api/verify', {
method: 'POST',
body: createFormData(file, apiKey)
});
const { verification_id } = await submitResponse.json();
// Poll for result
for (let i = 0; i < 30; i++) {
await sleep(1000);
const statusResponse = await fetch(`/api/status/${verification_id}`);
const status = await statusResponse.json();
if (status.status !== 'PROCESSING') {
return status;
}
}
throw new Error('Verification timeout');
}
Widget Integration Issues
Widget Not Loading
Symptoms:
- Widget div remains empty
- No errors in browser console
- Script loads but nothing renders
Diagnostic Steps:
- Check Script Loading:
// Add to browser console
console.log('VerifyHuman object:', typeof VerifyHuman);
// Should output: 'VerifyHuman object: object'
- Verify Container Exists:
console.log('Container:', document.getElementById('verifyhuman-widget'));
// Should output the DOM element, not null
Solutions:
- Correct Script Ordering:
<!-- WRONG - script runs before container exists -->
<script>
VerifyHuman.init({ apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6' });
</script>
<div id="verifyhuman-widget"></div>
<!-- CORRECT - container first, then script -->
<div id="verifyhuman-widget"></div>
<script src="https://app.verifyhuman.io/widget.js"></script>
<script>
VerifyHuman.init({ apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6' });
</script>
<!-- BEST - wait for DOM ready -->
<script src="https://app.verifyhuman.io/widget.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
VerifyHuman.init({ apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6' });
});
</script>
- React Integration:
import { useEffect, useRef } from 'react';
function VerifyHumanWidget({ apiKey, onSuccess, onFailure }) {
const containerRef = useRef(null);
const initialized = useRef(false);
useEffect(() => {
// Only initialize once
if (initialized.current) return;
// Load script dynamically
const script = document.createElement('script');
script.src = 'https://app.verifyhuman.io/widget.js';
script.async = true;
script.onload = () => {
if (window.VerifyHuman && containerRef.current) {
window.VerifyHuman.init({
apiKey,
container: containerRef.current,
onSuccess,
onFailure
});
initialized.current = true;
}
};
document.head.appendChild(script);
return () => {
// Cleanup on unmount
if (window.VerifyHuman?.destroy) {
window.VerifyHuman.destroy();
}
};
}, [apiKey, onSuccess, onFailure]);
return <div ref={containerRef} id="verifyhuman-widget" />;
}
- Vue Integration:
<template>
<div ref="widgetContainer" id="verifyhuman-widget"></div>
</template>
<script>
export default {
props: ['apiKey'],
mounted() {
this.loadWidget();
},
methods: {
loadWidget() {
const script = document.createElement('script');
script.src = 'https://app.verifyhuman.io/widget.js';
script.onload = () => {
window.VerifyHuman.init({
apiKey: this.apiKey,
container: this.$refs.widgetContainer,
onSuccess: this.handleSuccess,
onFailure: this.handleFailure
});
};
document.head.appendChild(script);
},
handleSuccess(result) {
this.$emit('verification-success', result);
},
handleFailure(error) {
this.$emit('verification-failure', error);
}
}
}
</script>
Camera Access Denied
Symptoms:
- "Camera access denied" message in widget
- Browser permission prompt doesn't appear
- Camera works in other apps but not widget
Root Causes:
- HTTP Instead of HTTPS
- Browser Permissions Blocked
- Another App Using Camera
- Privacy Settings
Solutions:
- HTTPS Requirement:
✗ http://example.com (camera will NOT work)
✓ https://example.com (camera works)
✓ http://localhost:3000 (allowed for development)
✓ http://127.0.0.1:8080 (allowed for development)
- Handle Permission Errors:
VerifyHuman.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
onCameraError: function(error) {
switch(error.name) {
case 'NotAllowedError':
showMessage('Please allow camera access in your browser settings.');
break;
case 'NotFoundError':
showMessage('No camera found. Please connect a camera and try again.');
break;
case 'NotReadableError':
showMessage('Camera is in use by another application. Please close other apps using the camera.');
break;
case 'OverconstrainedError':
showMessage('Camera does not meet requirements. Please try a different camera.');
break;
default:
showMessage('Camera error: ' + error.message);
}
}
});
- Browser-Specific Permission Reset:
Chrome:
1. Click lock icon in address bar
2. Click "Site settings"
3. Find "Camera" and set to "Allow"
4. Refresh the page
Firefox:
1. Click lock icon in address bar
2. Click "Connection secure" → "More Information"
3. Go to "Permissions" tab
4. Find "Use the Camera" and select "Allow"
Safari:
1. Safari → Preferences → Websites → Camera
2. Find your site and set to "Allow"
- Provide Fallback for File Upload:
VerifyHuman.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
captureMode: 'auto', // Try camera first, fallback to file
allowFileUpload: true,
onCameraError: function(error) {
console.log('Camera unavailable, using file upload');
}
});
Widget Styling Issues
Symptoms:
- Widget appears broken or misaligned
- Custom styles not applying
- Widget overlaps other page elements
Solutions:
- Fix Z-Index Conflicts:
/* Ensure widget is above other elements */
#verifyhuman-widget {
position: relative;
z-index: 1000;
}
/* If using modal dialogs */
.verifyhuman-modal {
z-index: 10000 !important;
}
- Container Sizing:
#verifyhuman-widget {
width: 100%;
max-width: 500px;
min-height: 400px;
margin: 0 auto;
}
/* Responsive sizing */
@media (max-width: 768px) {
#verifyhuman-widget {
max-width: 100%;
min-height: 350px;
}
}
- Custom Theming:
VerifyHuman.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
theme: 'dark',
customStyles: {
primaryColor: '#4F46E5',
backgroundColor: '#1F2937',
textColor: '#F9FAFB',
borderRadius: '12px',
fontFamily: 'Inter, system-ui, sans-serif'
}
});
- Fix CSS Conflicts:
/* Reset potential conflicts */
#verifyhuman-widget * {
box-sizing: border-box;
}
#verifyhuman-widget button {
all: unset;
/* Apply widget-specific styles */
}
CORS Errors
Symptoms:
Access to fetch at 'https://app.verifyhuman.io/api/verify' from origin
'https://yoursite.com' has been blocked by CORS policy
Understanding CORS:
- CORS errors occur when making direct API calls from the browser
- The widget handles CORS automatically
- Direct API calls should be made from your backend
Solutions:
- Use Widget for Frontend:
// WRONG - Direct API call from browser
fetch('https://app.verifyhuman.io/api/verify', {
method: 'POST',
body: formData
});
// CORRECT - Use the widget
VerifyHuman.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
onSuccess: (result) => {
// Send token to your backend
sendToBackend(result.token);
}
});
- Proxy Through Your Backend:
# Flask backend proxy
@app.route('/api/verify', methods=['POST'])
def verify_proxy():
# Receive file from frontend
file = request.files['file']
# Forward to VerifyHuman API
response = requests.post(
'https://app.verifyhuman.io/api/verify',
files={'file': file},
data={'clientKey': os.environ['VERIFYHUMAN_API_KEY']}
)
return jsonify(response.json())
- Node.js Proxy:
const express = require('express');
const multer = require('multer');
const fetch = require('node-fetch');
const FormData = require('form-data');
const upload = multer();
app.post('/api/verify', upload.single('file'), async (req, res) => {
const formData = new FormData();
formData.append('clientKey', process.env.VERIFYHUMAN_API_KEY);
formData.append('file', req.file.buffer, req.file.originalname);
const response = await fetch('https://app.verifyhuman.io/api/verify', {
method: 'POST',
body: formData
});
const result = await response.json();
res.json(result);
});
Hosted Flows Issues
Flow Link Returns 404
Symptoms:
- "Page Not Found" error
- Valid-looking URL doesn't work
Solutions:
- Verify Complete URL:
✗ https://verify.verifyhuman.io/flow/abc123 (incomplete)
✓ https://verify.verifyhuman.io/flow/vh_abc123xyz789 (complete with prefix)
Check Flow Status:
- Login to Dashboard → Hosted Flows
- Verify flow shows as "Active"
- Check expiration date
Common URL Issues:
- Extra spaces when copying
- Missing or incomplete flow ID
- Typos in flow ID
Test Flow Programmatically:
curl -I "https://verify.verifyhuman.io/flow/vh_abc123xyz789"
# Look for 200 OK, not 404 Not Found
Redirect Not Working
Symptoms:
- User completes verification but stays on VerifyHuman page
- No redirect occurs after success/failure
Solutions:
Check Redirect Configuration:
- Dashboard → Hosted Flows → [Your Flow] → Redirect Settings
- Success URL must include
https:// - Test URL is accessible
URL Parameter Placeholders:
Success URL: https://yoursite.com/verified?id={VERIFICATION_ID}&status={STATUS}
Failure URL: https://yoursite.com/failed?error={ERROR}&id={VERIFICATION_ID}
Available placeholders:
| Placeholder | Description |
|---|---|
{VERIFICATION_ID} |
Unique verification identifier |
{STATUS} |
PASS, FAIL, or ERROR |
{CONFIDENCE} |
Confidence score (0-100) |
{TOKEN} |
JWT verification token |
{ERROR} |
Error message (failure only) |
- Handle Redirect on Your Server:
@app.route('/verified')
def verification_complete():
verification_id = request.args.get('id')
status = request.args.get('status')
if status == 'PASS':
# Store verification in database
save_verification(verification_id)
return render_template('success.html')
else:
return render_template('retry.html')
Custom Branding Not Displaying
Symptoms:
- Uploaded logo doesn't appear
- Custom colors not applied
- Branding reverts to default
Solutions:
Logo Requirements:
Requirement Specification Format PNG or SVG Max Size 500 KB Recommended Dimensions 300x100 pixels Background Transparent recommended Color Mode RGB Color Format:
✗ "blue" (won't work)
✗ "rgb(0,0,255)" (won't work)
✓ "#0000FF" (correct)
✓ "#00f" (correct shorthand)
Cache Issues:
- Wait 24 hours for CDN cache to clear
- Or append cache-buster:
?v=timestamp - Force refresh: Ctrl+Shift+R / Cmd+Shift+R
Check Configuration in Dashboard:
- Dashboard → Hosted Flows → [Your Flow] → Branding Settings
- Verify logo and colors are saved correctly
- Re-upload if settings appear blank
Product-Specific Troubleshooting
VerifyHuman Issues
Low Confidence Scores
Symptoms:
{
"status": "PASS",
"confidence": 62,
"warning": "Low confidence score"
}
Causes and Solutions:
Poor Lighting:
- Front-facing light source
- Avoid backlight/silhouette
- Natural daylight preferred
Face Positioning:
VerifyHuman.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
instructions: [
'Face the camera directly',
'Keep your face centered in the frame',
'Fill at least 1/3 of the frame',
'Remove glasses, hats, or masks'
],
minFaceSize: 0.3 // 30% of frame
});
- Implement Confidence Thresholds:
function handleVerification(result) {
if (result.confidence >= 90) {
// High confidence - auto approve
approveUser();
} else if (result.confidence >= 70) {
// Medium confidence - accept with note
approveUser({ requireReview: true });
} else if (result.confidence >= 50) {
// Low confidence - request retry
showRetryPrompt('Please try again with better lighting');
} else {
// Very low - likely fraud attempt
flagForReview(result.verification_id);
}
}
No Face Detected
Symptoms:
{
"status": "FAIL",
"error": "No face detected in image",
"confidence": 0
}
Solutions:
- User Guidance Component:
<div class="verification-tips">
<h4>For Best Results:</h4>
<ul>
<li>✅ Face camera directly</li>
<li>✅ Good front lighting</li>
<li>✅ Plain background</li>
<li>✅ Remove sunglasses/masks</li>
<li>❌ Avoid backlighting</li>
<li>❌ Don't use side profile</li>
</ul>
</div>
- Pre-Check Before Submit:
async function preCheckImage(imageFile) {
// Use local face detection before API call
const faceDetector = new FaceDetector();
const faces = await faceDetector.detect(imageFile);
if (faces.length === 0) {
throw new Error('No face detected. Please ensure your face is visible and well-lit.');
}
if (faces.length > 1) {
throw new Error('Multiple faces detected. Please ensure only you are in the frame.');
}
return true;
}
Liveness Check Failed
Symptoms:
{
"status": "FAIL",
"error": "Liveness check failed - possible photo of photo",
"liveness_score": 35
}
Causes:
- User showing printed photo
- User showing photo on screen
- Very poor image quality
- Extreme lighting conditions
Solutions:
- Force Camera Capture:
VerifyHuman.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
captureMode: 'camera', // Disable file upload
requireLiveness: true,
minLivenessScore: 70
});
- Add Liveness Instructions:
<div class="liveness-notice">
<p><strong>Real-time verification required</strong></p>
<p>Please use your device's camera for live capture.
Photos of photos or screens will not be accepted.</p>
</div>
VerifyAge Issues
Age Estimation Incorrect
Symptoms:
- AI estimates age significantly different from actual age
- Users near the threshold are incorrectly rejected
Solutions:
- Use Two-Step Verification:
VerifyAge.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
minAge: 21,
useCase: 'alcohol',
fallbackToId: true, // Auto-request ID if face estimate is borderline
borderlineRange: 5 // Years margin for ID fallback
});
- Handle Borderline Cases:
def handle_age_verification(result):
estimated_age = result.get('estimated_age')
confidence = result.get('confidence')
min_age = 21
if estimated_age is None:
return require_id_verification()
# Clear pass
if estimated_age >= min_age + 5 and confidence >= 80:
return approve_user()
# Clear fail
if estimated_age < min_age - 3 and confidence >= 80:
return reject_user()
# Borderline - require ID
return require_id_verification()
State-Specific Requirements
Symptoms:
- Different states have different age requirements
- Need to handle jurisdiction-specific rules
Solutions:
# US State Age Requirements
STATE_REQUIREMENTS = {
'alcohol': {
'default': 21,
# All US states are 21 for alcohol
},
'tobacco': {
'default': 21,
'AL': 19, # Alabama
'AK': 19, # Alaska
# Federal law now 21, but some grandfathering
},
'cannabis': {
'default': 21,
'CO': 21,
'CA': 21,
'WA': 21,
# Only legal in certain states
},
'gambling': {
'default': 21,
'MT': 18,
'OK': 18,
'WY': 18,
}
}
def get_age_requirement(use_case, state):
case_requirements = STATE_REQUIREMENTS.get(use_case, {})
return case_requirements.get(state, case_requirements.get('default', 21))
VerifyIdentity Issues
ID Document Not Recognized
Symptoms:
{
"status": "FAIL",
"error": "Unable to extract data from document",
"document_quality": "poor"
}
Solutions:
- Document Requirements:
<div class="id-requirements">
<h4>ID Document Guidelines:</h4>
<ul>
<li>Valid government-issued photo ID</li>
<li>Not expired</li>
<li>All corners visible</li>
<li>No glare or reflections</li>
<li>High resolution (at least 1000px wide)</li>
<li>Color photo (not black & white)</li>
</ul>
<h4>Supported Documents:</h4>
<ul>
<li>Driver's License</li>
<li>State ID Card</li>
<li>Passport</li>
<li>Passport Card</li>
<li>Military ID</li>
</ul>
</div>
- Pre-Validation:
function validateIdImage(file) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
// Check minimum resolution
if (img.width < 1000 || img.height < 600) {
reject(new Error('Image resolution too low. Please capture a higher quality photo.'));
return;
}
// Check aspect ratio (ID cards are typically ~1.586:1)
const aspectRatio = img.width / img.height;
if (aspectRatio < 1.3 || aspectRatio > 2.0) {
reject(new Error('Please capture the full ID card with all corners visible.'));
return;
}
resolve(true);
};
img.src = URL.createObjectURL(file);
});
}
Face Match Failed
Symptoms:
{
"status": "FAIL",
"error": "Selfie does not match ID photo",
"match_confidence": 45
}
Causes:
- Significant appearance change (beard, glasses, haircut)
- Poor selfie quality
- Old or damaged ID photo
- Different person
Solutions:
- Matching Guidelines:
<div class="matching-tips">
<h4>For Best Match Results:</h4>
<ul>
<li>Take selfie in similar conditions to ID photo</li>
<li>Remove accessories not worn in ID (glasses, hat)</li>
<li>Use good lighting</li>
<li>Face camera directly</li>
</ul>
</div>
- Handle Low Match Scores:
def handle_face_match(result):
match_score = result.get('match_confidence', 0)
if match_score >= 90:
return approve_verification()
elif match_score >= 70:
# Possible match, flag for review
return flag_for_manual_review(result)
elif match_score >= 50:
# Retry with better photo
return request_retry("Please retake your selfie with better lighting")
else:
# Potential fraud
return flag_security_review(result)
VerifyTrust Issues
AML Screening Hit
Symptoms:
{
"status": "REVIEW",
"compliance_screening": {
"total_hits": 2,
"sanctions_hits": 0,
"pep_hits": 1,
"adverse_media_hits": 1
}
}
Solutions:
- Handle Different Hit Types:
def handle_compliance_result(result):
screening = result.get('compliance_screening', {})
# Immediate reject for sanctions
if screening.get('sanctions_hits', 0) > 0:
return reject_with_reason("sanctions_match")
# Review required for PEP
if screening.get('pep_hits', 0) > 0:
return flag_for_enhanced_due_diligence(result)
# Adverse media - depends on severity
if screening.get('adverse_media_hits', 0) > 0:
return flag_for_manual_review(result)
# No hits
if screening.get('total_hits', 0) == 0:
return approve_verification()
- Access Compliance Reports:
- Dashboard → Verifications → [Select Verification]
- Click "Download Report" for AES-256 encrypted PDF
- Use the report password (provided during verification setup) to decrypt
Plan Access Restrictions
Symptoms:
{
"error": "VerifyTrust requires Pro or Enterprise plan",
"current_plan": "Growth"
}
Solution:
- VerifyTrust is only available on Pro and Enterprise plans
- Upgrade in Dashboard → Subscription → Upgrade Plan
- Alternative: Use VerifyIdentity (30 credits) for basic KYC without AML screening
SDK-Specific Troubleshooting
Python SDK Issues
SSL Certificate Errors
Symptoms:
requests.exceptions.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED]
Solutions:
- Update Certificates:
pip install --upgrade certifi
python -c "import certifi; print(certifi.where())"
- Install CA Certificates (Linux):
# Debian/Ubuntu
sudo apt-get update && sudo apt-get install ca-certificates
# CentOS/RHEL
sudo yum install ca-certificates
- Temporary Workaround (NOT for production):
import requests
# ONLY for debugging - never use in production
response = requests.post(url, verify=False)
Async/Await Issues
Symptoms:
RuntimeError: This event loop is already running- Timeouts in async code
Solutions:
import asyncio
import aiohttp
# WRONG - nested event loops
async def verify():
return asyncio.run(do_verification()) # This will fail
# CORRECT - proper async structure
async def verify():
async with aiohttp.ClientSession() as session:
return await do_verification(session)
# For Jupyter notebooks
import nest_asyncio
nest_asyncio.apply()
Memory Issues with Large Files
Solutions:
def stream_upload(file_path, api_key):
"""Stream large files to avoid memory issues"""
with open(file_path, 'rb') as f:
# Use streaming upload
response = requests.post(
'https://app.verifyhuman.io/api/verify',
files={'file': ('image.jpg', f, 'image/jpeg')},
data={'clientKey': api_key},
stream=True
)
return response.json()
Node.js SDK Issues
Module Import Errors
Symptoms:
Error: Cannot find module 'verifyhuman'
SyntaxError: Cannot use import statement outside a module
Solutions:
- CommonJS vs ES Modules:
// CommonJS (default for Node.js < 14)
const VerifyHuman = require('verifyhuman');
// ES Modules (package.json: "type": "module")
import VerifyHuman from 'verifyhuman';
- Package.json Configuration:
{
"type": "module",
"dependencies": {
"verifyhuman": "^2.0.0"
}
}
File Upload Issues
Symptoms:
- Files not being sent correctly
- Empty file errors
Solutions:
const fs = require('fs');
const FormData = require('form-data');
const fetch = require('node-fetch');
async function uploadFile(filePath, apiKey) {
const form = new FormData();
// CORRECT - stream from file
form.append('file', fs.createReadStream(filePath));
form.append('clientKey', apiKey);
const response = await fetch('https://app.verifyhuman.io/api/verify', {
method: 'POST',
body: form,
headers: form.getHeaders() // Important!
});
return response.json();
}
Memory Leaks
Solutions:
const { Readable } = require('stream');
async function processWithCleanup(imageBuffer) {
let response;
try {
response = await verify(imageBuffer);
return response;
} finally {
// Clean up buffer
imageBuffer = null;
if (global.gc) {
global.gc(); // Force garbage collection if available
}
}
}
PHP SDK Issues
cURL Errors
Symptoms:
cURL error 60: SSL certificate problem
cURL error 28: Operation timed out
Solutions:
- SSL Certificate Fix:
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.verifyhuman.io/api/verify');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem');
- Download Latest CA Bundle:
curl -o /etc/ssl/certs/cacert.pem https://curl.se/ca/cacert.pem
- Timeout Configuration:
<?php
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
File Upload Issues
Solutions:
<?php
function verifyImage($filePath, $apiKey) {
$ch = curl_init();
$postFields = [
'clientKey' => $apiKey,
'file' => new CURLFile($filePath, 'image/jpeg', 'photo.jpg')
];
curl_setopt_array($ch, [
CURLOPT_URL => 'https://app.verifyhuman.io/api/verify',
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postFields,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: multipart/form-data']
]);
$response = curl_exec($ch);
if (curl_errno($ch)) {
throw new Exception('cURL error: ' . curl_error($ch));
}
curl_close($ch);
return json_decode($response, true);
}
Mobile Integration Issues
iOS Issues
Camera Permission Not Requested
Solutions:
- Info.plist Configuration:
<key>NSCameraUsageDescription</key>
<string>Camera access is required for identity verification</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photo library access is needed to select verification photos</string>
- Request Permission Properly:
import AVFoundation
func requestCameraPermission(completion: @escaping (Bool) -> Void) {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
completion(true)
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
completion(granted)
}
}
case .denied, .restricted:
completion(false)
@unknown default:
completion(false)
}
}
WKWebView Issues
Solutions:
import WebKit
class VerificationViewController: UIViewController, WKUIDelegate {
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
config.allowsInlineMediaPlayback = true
config.mediaTypesRequiringUserActionForPlayback = []
webView = WKWebView(frame: view.bounds, configuration: config)
webView.uiDelegate = self
// Allow camera access
if #available(iOS 14.0, *) {
webView.configuration.defaultWebpagePreferences.allowsContentJavaScript = true
}
}
}
Android Issues
Permission Handling
Solutions:
- AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
- Runtime Permission Request:
class VerificationActivity : AppCompatActivity() {
private val CAMERA_PERMISSION_CODE = 100
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (checkCameraPermission()) {
startVerification()
} else {
requestCameraPermission()
}
}
private fun checkCameraPermission(): Boolean {
return ContextCompat.checkSelfPermission(
this, Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
}
private fun requestCameraPermission() {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
CAMERA_PERMISSION_CODE
)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == CAMERA_PERMISSION_CODE) {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startVerification()
} else {
showPermissionDeniedMessage()
}
}
}
}
WebView Camera Access
Solutions:
class VerificationWebView : WebView {
init {
settings.apply {
javaScriptEnabled = true
mediaPlaybackRequiresUserGesture = false
domStorageEnabled = true
}
webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
request.grant(request.resources)
}
}
}
}
React Native Issues
Camera Module Not Found
Solutions:
// Install required packages
// npm install react-native-camera react-native-permissions
import { Camera } from 'react-native-camera';
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
async function checkAndRequestCamera() {
const permission = Platform.OS === 'ios'
? PERMISSIONS.IOS.CAMERA
: PERMISSIONS.ANDROID.CAMERA;
const result = await check(permission);
if (result === RESULTS.DENIED) {
const requestResult = await request(permission);
return requestResult === RESULTS.GRANTED;
}
return result === RESULTS.GRANTED;
}
Authentication & API Keys
API Key Scope Errors
Symptoms:
{
"error": "API key does not have permission for this product",
"required_scope": "verifyage",
"key_scopes": ["verifyhuman"]
}
Solutions:
Create Product-Specific Keys:
- Dashboard → API Keys → Create New Key
- Select required scopes during creation
Update Existing Key Scopes:
- Dashboard → API Keys → [Your Key] → Scopes
- Enable required products
Unified Key System: All API keys now use the unified format:
vhk-{32chars}Product Required Scope Example Key VerifyHuman verifyhuman vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6VerifyAge verifyage vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6VerifyIdentity verifyidentity vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6VerifyTrust verifycompliance vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6A single key can have multiple scopes for accessing different products.
Two-Factor Authentication Issues
Symptoms:
- Cannot generate API keys
- Dashboard access denied after password entry
Solutions:
TOTP App Not Synced:
- Check device time is accurate
- Resync authenticator app
- Use time-based sync in app settings
Lost 2FA Access:
- Use backup codes (generated during 2FA setup)
- Contact support@verifyhuman.io with account verification
Email OTP Not Received:
- Check spam/junk folders
- Whitelist noreply@verifyhuman.io
- Wait 60 seconds for retry
Token Validation Issues
JWT Token Expired
Symptoms:
{
"valid": false,
"error": "Token expired"
}
Token Lifetime: 24 hours from creation
Solutions:
- Check Token Age:
import jwt
import time
def is_token_valid(token):
try:
# Decode without verification to check expiry
payload = jwt.decode(token, options={"verify_signature": False})
exp = payload.get('exp', 0)
return time.time() < exp
except jwt.exceptions.DecodeError:
return False
- Request New Verification:
async function getValidToken(userId) {
const storedToken = await getStoredToken(userId);
if (storedToken && isTokenValid(storedToken)) {
return storedToken;
}
// Token expired or invalid, require new verification
return await requestNewVerification(userId);
}
Token Validation Failing
Symptoms:
{
"valid": false,
"error": "Invalid token signature"
}
Solutions:
- Always Validate on Backend:
def validate_token(token, client_secret):
"""Server-side token validation"""
response = requests.post(
'https://app.verifyhuman.io/api/validate',
headers={
'Authorization': f'Bearer {client_secret}',
'Content-Type': 'application/json'
},
json={'token': token}
)
result = response.json()
if not result.get('valid'):
raise ValueError(f"Token validation failed: {result.get('error')}")
return result
- Never Trust Client-Side Validation:
// WRONG - trusting client-side result
if (clientResult.token) {
allowAccess();
}
// CORRECT - validate on backend
const backendResult = await fetch('/api/validate-verification', {
method: 'POST',
body: JSON.stringify({ token: clientResult.token })
});
if (backendResult.valid) {
allowAccess();
}
Credit & Billing Issues
Unexpected Credit Deduction
Symptoms:
- Credits depleting faster than expected
- Multiple charges for single verification
Solutions:
- Check for Retry Loops:
// BAD - infinite retry loop
async function verify() {
try {
await verifyHuman();
} catch (error) {
verify(); // Endless loop consuming credits!
}
}
// GOOD - limited retries
async function verify(attempt = 0, maxAttempts = 3) {
try {
return await verifyHuman();
} catch (error) {
if (attempt < maxAttempts && error.retryable) {
await sleep(1000 * Math.pow(2, attempt));
return verify(attempt + 1, maxAttempts);
}
throw error;
}
}
- Implement Verification Caching:
import hashlib
from datetime import datetime, timedelta
# Simple in-memory cache (use Redis in production)
verification_cache = {}
def cached_verify(user_id, image_data, cache_duration_hours=1):
"""Cache verification results to avoid duplicate calls"""
cache_key = f"{user_id}:{hashlib.md5(image_data).hexdigest()}"
if cache_key in verification_cache:
cached = verification_cache[cache_key]
if datetime.now() < cached['expires']:
return cached['result']
# Not in cache, perform verification
result = perform_verification(image_data)
verification_cache[cache_key] = {
'result': result,
'expires': datetime.now() + timedelta(hours=cache_duration_hours)
}
return result
- Monitor Credit Usage:
def verify_with_credit_check(api_key, product):
credits_needed = {
'verifyhuman': 1,
'verifyage': 10,
'verifyidentity': 30,
'verifycompliance': 50
}
# Check credits before verification
credits = get_credit_balance(api_key)
needed = credits_needed[product]
if credits < needed:
send_low_credit_alert(credits, needed)
raise InsufficientCreditsError(credits, needed)
result = perform_verification()
# Log credit usage
log_credit_usage(api_key, product, needed)
return result
Plan Access Restrictions
Symptoms:
{
"error": "VerifyTrust requires Pro or Enterprise plan",
"current_plan": "Growth"
}
Plan Capabilities:
| Product | Free | Starter | Growth | Pro | Enterprise |
|---|---|---|---|---|---|
| VerifyHuman | ✓ | ✓ | ✓ | ✓ | ✓ |
| VerifyAge | ✓ | ✓ | ✓ | ✓ | ✓ |
| VerifyIdentity | ✓ | ✓ | ✓ | ✓ | ✓ |
| VerifyTrust | ✗ | ✗ | ✗ | ✓ | ✓ |
Solutions:
- Upgrade to Pro or Enterprise plan
- Use VerifyIdentity as alternative (basic KYC without AML screening)
Performance Issues
Slow Verification Times
Symptoms:
- Verification takes > 10 seconds
- Frequent timeouts
Expected Performance:
| Metric | Target | Maximum |
|---|---|---|
| Average Response Time | < 3 seconds | 10 seconds |
| P95 Response Time | < 5 seconds | 15 seconds |
| Timeout Threshold | - | 30 seconds |
Solutions:
- Optimize Image Before Upload:
async function optimizeForUpload(file) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = await createImageBitmap(file);
// Target dimensions
const maxWidth = 1920;
const maxHeight = 1080;
let { width, height } = img;
if (width > maxWidth || height > maxHeight) {
const ratio = Math.min(maxWidth / width, maxHeight / height);
width *= ratio;
height *= ratio;
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
return new Promise(resolve => {
canvas.toBlob(resolve, 'image/jpeg', 0.85);
});
}
- Use Appropriate Timeout Settings:
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def create_session_with_retry():
session = requests.Session()
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
return session
session = create_session_with_retry()
response = session.post(
'https://app.verifyhuman.io/api/verify',
files={'file': image_file},
data={'clientKey': api_key},
timeout=(5, 30) # (connect, read) timeouts
)
- Implement Progress Feedback:
async function verifyWithProgress(file, onProgress) {
onProgress({ stage: 'uploading', percent: 0 });
const formData = new FormData();
formData.append('file', file);
formData.append('clientKey', apiKey);
const response = await fetch('/api/verify', {
method: 'POST',
body: formData
});
onProgress({ stage: 'processing', percent: 50 });
const result = await response.json();
onProgress({ stage: 'complete', percent: 100 });
return result;
}
Widget Loading Slowly
Solutions:
- Lazy Load Widget:
function loadVerifyHumanWhenNeeded() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadWidget();
observer.disconnect();
}
});
});
observer.observe(document.getElementById('verifyhuman-widget'));
}
function loadWidget() {
const script = document.createElement('script');
script.src = 'https://app.verifyhuman.io/widget.js';
script.onload = () => {
VerifyHuman.init({ apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6' });
};
document.head.appendChild(script);
}
- Preload Widget Script:
<link rel="preload" href="https://app.verifyhuman.io/widget.js" as="script">
- Use Deferred Loading:
<script src="https://app.verifyhuman.io/widget.js" defer></script>
Security Issues
Suspected Fraudulent Verifications
Indicators:
- Multiple failed attempts from same IP
- Unusual geographic patterns
- Rapid successive attempts
Solutions:
- Implement Additional Checks:
from collections import defaultdict
import time
# Track attempts by IP
ip_attempts = defaultdict(list)
def check_fraud_indicators(ip_address, result):
current_time = time.time()
# Clean old entries (last 5 minutes)
ip_attempts[ip_address] = [
t for t in ip_attempts[ip_address]
if current_time - t < 300
]
# Record this attempt
ip_attempts[ip_address].append(current_time)
# Check for suspicious patterns
if len(ip_attempts[ip_address]) > 10:
flag_for_review(ip_address, "High velocity attempts")
return True
if result.get('confidence', 100) < 50:
flag_for_review(ip_address, "Low confidence score")
return True
return False
- Use Device Fingerprinting:
VerifyHuman.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
collectDeviceInfo: true,
onSuccess: (result) => {
// Device info included in result
console.log(result.device_fingerprint);
}
});
API Key Compromised
Immediate Actions:
Revoke the Compromised Key:
- Dashboard → API Keys → [Compromised Key] → Revoke
Generate New Key:
- Dashboard → API Keys → Create New Key
- Update all applications with new key
Check for Unauthorized Usage:
- Dashboard → Usage Logs
- Look for unusual patterns
Implement Key Rotation:
import os
from datetime import datetime, timedelta
def get_api_key():
"""Get current API key with automatic rotation awareness"""
primary_key = os.environ.get('VERIFYHUMAN_API_KEY')
backup_key = os.environ.get('VERIFYHUMAN_BACKUP_KEY')
# Try primary first
if verify_key_valid(primary_key):
return primary_key
# Fallback to backup
if verify_key_valid(backup_key):
alert_key_rotation_needed()
return backup_key
raise ConfigurationError("No valid API keys available")
Environment & Configuration
Development vs Production
Environment Indicators:
| Setting | Development | Production |
|---|---|---|
| API Key Prefix | _test_ |
_live_ |
| Base URL | Same | Same |
| Credit Consumption | No | Yes |
| Verification Storage | Temporary | Persistent |
Configuration Example:
import os
class Config:
ENV = os.environ.get('FLASK_ENV', 'development')
if ENV == 'production':
VERIFYHUMAN_API_KEY = os.environ.get('VERIFYHUMAN_LIVE_KEY')
VERIFYHUMAN_SECRET = os.environ.get('VERIFYHUMAN_LIVE_SECRET')
else:
VERIFYHUMAN_API_KEY = os.environ.get('VERIFYHUMAN_TEST_KEY')
VERIFYHUMAN_SECRET = os.environ.get('VERIFYHUMAN_TEST_SECRET')
Environment Variables Missing
Required Variables:
# API Keys (use test keys for development)
VERIFYHUMAN_API_KEY=vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
VERIFYHUMAN_SECRET=vhs_xxx
# Optional
VERIFYHUMAN_WEBHOOK_SECRET=whsec_xxx
VERIFYHUMAN_LOG_LEVEL=INFO
Validation Script:
def validate_config():
"""Validate required environment variables"""
required = [
'VERIFYHUMAN_API_KEY',
'VERIFYHUMAN_SECRET'
]
missing = [var for var in required if not os.environ.get(var)]
if missing:
raise ConfigurationError(
f"Missing required environment variables: {', '.join(missing)}"
)
# Validate key format (unified format: vhk-{32chars})
api_key = os.environ.get('VERIFYHUMAN_API_KEY')
if not api_key.startswith('vhk-') or len(api_key) != 36:
raise ConfigurationError(
"Invalid API key format. Expected format: vhk-{32chars}"
)
return True
Testing & Debugging
Test Mode
Using Test Keys:
- Test keys:
vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6,vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6, etc. - No credits consumed
- Results not stored permanently
- Same API behavior as production
Test Images:
# Sample test scenarios
TEST_SCENARIOS = {
'pass_high_confidence': 'test_face_clear.jpg',
'pass_low_confidence': 'test_face_dim.jpg',
'fail_no_face': 'test_no_face.jpg',
'fail_multiple_faces': 'test_multiple.jpg',
'fail_liveness': 'test_photo_of_photo.jpg'
}
Debug Mode
Enable Verbose Logging:
VerifyHuman.init({
apiKey: 'vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6',
debug: true,
onDebug: (log) => {
console.log('[VerifyHuman]', log.timestamp, log.level, log.message);
}
});
Backend Debug Logging:
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('verifyhuman')
def verify_with_logging(image_data, api_key):
logger.debug(f"Starting verification, image size: {len(image_data)} bytes")
try:
result = perform_verification(image_data, api_key)
logger.debug(f"Verification result: {result}")
return result
except Exception as e:
logger.error(f"Verification failed: {e}", exc_info=True)
raise
Common Test Scenarios
import pytest
class TestVerification:
def test_valid_face_passes(self, client, valid_face_image):
result = client.verify(valid_face_image)
assert result['status'] == 'PASS'
assert result['confidence'] >= 70
def test_no_face_fails(self, client, no_face_image):
result = client.verify(no_face_image)
assert result['status'] == 'FAIL'
assert 'No face detected' in result['error']
def test_multiple_faces_fails(self, client, multiple_faces_image):
result = client.verify(multiple_faces_image)
assert result['status'] == 'FAIL'
assert 'Multiple faces' in result['error']
def test_invalid_api_key_rejected(self, invalid_client, valid_face_image):
with pytest.raises(AuthenticationError):
invalid_client.verify(valid_face_image)
def test_rate_limiting(self, client, valid_face_image):
# Make many requests quickly
for _ in range(100):
try:
client.verify(valid_face_image)
except RateLimitError:
return # Expected
pytest.fail("Rate limiting not triggered")
Error Code Reference
Complete Error Code Table
| Code | Error | Description | Solution |
|---|---|---|---|
E001 |
Invalid API Key | API key format incorrect or key doesn't exist | Check key prefix and dashboard status |
E002 |
Insufficient Credits | Account has fewer credits than required | Add credits or upgrade plan |
E003 |
Rate Limit Exceeded | Too many requests in time window | Implement backoff, upgrade plan |
E004 |
File Too Large | File exceeds 10MB limit | Compress image before upload |
E005 |
No Face Detected | AI couldn't find a face in image | Improve lighting, positioning |
E006 |
Multiple Faces | More than one face detected | Ensure single person in frame |
E007 |
Liveness Failed | Possible photo of photo detected | Use live camera capture |
E008 |
Token Invalid | JWT token invalid or expired | Validate on backend, check expiry |
E009 |
Domain Not Whitelisted | Request from unauthorized domain | Add domain in dashboard |
E010 |
Scope Denied | API key lacks required scope | Update key scopes in dashboard |
E011 |
Plan Restricted | Feature not available on plan | Upgrade subscription plan |
E012 |
Invalid File Format | Unsupported image format | Use JPG, PNG, or WebP |
E013 |
File Corrupted | Unable to process image file | Re-capture or re-encode image |
E014 |
Timeout | Processing took too long | Reduce image size, retry |
E015 |
Service Unavailable | API temporarily unavailable | Check status page, retry later |
E016 |
ID Not Recognized | Cannot extract data from ID | Improve ID photo quality |
E017 |
Face Mismatch | Selfie doesn't match ID photo | Retake with similar conditions |
E018 |
ID Expired | Document past expiration date | Use valid, non-expired ID |
E019 |
Age Requirement Not Met | User doesn't meet minimum age | N/A - User rejected |
E020 |
Compliance Hit | AML/PEP/Sanctions match found | Review compliance report |
E021 |
Throttled | Suspicious activity detected | Wait for throttle period |
E022 |
Maintenance | System under maintenance | Check status page |
HTTP Status Code Reference
| Status | Meaning | Action |
|---|---|---|
| 200 | Success | Process response |
| 400 | Bad Request | Check request format |
| 401 | Unauthorized | Verify API key |
| 403 | Forbidden | Check scopes/domain |
| 404 | Not Found | Verify endpoint URL |
| 413 | Payload Too Large | Reduce file size |
| 429 | Rate Limited | Implement backoff |
| 500 | Server Error | Retry, contact support |
| 502 | Bad Gateway | Retry later |
| 503 | Service Unavailable | Check status page |
Getting Additional Help
Support Channels
| Channel | Availability | Best For |
|---|---|---|
| 24/7 | Detailed technical issues | |
| Live Chat | 9 AM - 5 PM EST | Quick questions |
| Documentation | 24/7 | Self-service |
| Status Page | 24/7 | Service status |
Contact Information:
- Email: support@verifyhuman.io
- Live Chat: Available in dashboard
- Documentation: docs.verifyhuman.io
- Status Page: status.verifyhuman.io
When Contacting Support
Include This Information:
Account Details:
- Account email
- API key prefix (e.g.,
vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6...) - Current plan
Issue Details:
- Verification ID (if applicable)
- Timestamp of issue
- Error messages (full text)
- HTTP status codes
Technical Context:
- Integration method (API, Widget, Hosted Flow)
- Programming language/framework
- Browser/OS (for widget issues)
Reproduction Steps:
- Minimal code sample
- Sample image (if applicable)
- Expected vs actual behavior
Example Support Request:
Subject: Liveness check failing for valid users
Account: user@company.com
API Key: vhk-a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6... (first 15 chars)
Plan: Growth
Issue: ~20% of legitimate users are getting liveness failures
Verification IDs (recent failures):
- ver_abc123 (2025-11-25 10:30 UTC)
- ver_def456 (2025-11-25 10:45 UTC)
Integration: JavaScript Widget v2.1.0
Browser: Chrome 119 (mostly), also Safari 17
Steps:
1. User loads verification page
2. Widget initializes successfully
3. User takes selfie with front-facing camera
4. Verification returns FAIL with liveness_score: 35
Expected: These are real users with live camera captures
Actual: Getting liveness failures
No changes to our integration recently. Issue started ~2 days ago.
Last Updated: November 25, 2025
Version: 2.0
Related Documentation: