Verify AI API
Submit photos for AI-powered compliance verification. Get structured results with confidence scores, violation details, and user feedback.
https://verify.switchlabs.dev/api/v1Quick Start
Get up and running in under 5 minutes. Choose your integration path:
1. Install the React Native SDK
npx expo install @switchlabs/verify-ai-react-native expo-camera @react-native-async-storage/async-storage2. Get Your API Key
Sign up for a plan, then find your key in Dashboard → Settings.
3. Verify a Photo (Minimal Example)
import { useVerifyAI } from '@switchlabs/verify-ai-react-native';
function ParkingScreen() {
const { verify, loading, lastResult } = useVerifyAI({
apiKey: 'vai_your_api_key',
});
const handlePhoto = async (base64Image: string) => {
const result = await verify({
image: base64Image,
policy: 'scooter_parking',
});
if (result?.is_compliant) {
console.log('Parking approved!');
}
};
return <>{/* your camera UI */}</>;
}4. Full Camera Integration (5 lines)
import { useVerifyAI } from '@switchlabs/verify-ai-react-native';
import { VerifyAIScanner } from '@switchlabs/verify-ai-react-native/scanner';
function ScannerScreen() {
const { verify } = useVerifyAI({ apiKey: 'vai_your_api_key' });
return (
<VerifyAIScanner
onCapture={(base64) => verify({ image: base64, policy: 'scooter_parking' })}
onResult={(result) => console.log(result.is_compliant ? 'PASS' : 'FAIL')}
overlay={{ title: 'Parking Check', instructions: 'Center the scooter', showGuideFrame: true }}
/>
);
}5. REST API (No SDK)
Not using React Native? Call the API directly from any language:
curl -X POST https://verify.switchlabs.dev/api/v1/verify \
-H "X-API-Key: vai_your_api_key" \
-F "image=@photo.jpg" \
-F "policy=scooter_parking"
# Response:
# { "is_compliant": false, "confidence": 0.98,
# "violation_reasons": ["blocking_sidewalk"],
# "feedback": "Please move away from the walkway." }Authentication
All API requests require an API key passed via the X-API-Key header.
curl https://verify.switchlabs.dev/api/v1/verifications \
-H "X-API-Key: vai_your_api_key_here"Get your API key from the sign-up page or your dashboard settings.
Submit Verification
Submit a photo for AI analysis. Accepts multipart form-data or JSON with base64 image.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| image | file | base64 | Yes | The image to verify (JPEG, PNG, WebP, max 10MB) |
| policy | string | Yes | Policy ID (e.g., scooter_parking, damage_detection, delivery_pod) |
| metadata | object | No | Arbitrary metadata to attach (device_id, user_id, gps, etc.) |
| provider | string | No | AI provider override: "openai", "anthropic", or "gemini" |
Example: Multipart Form-Data
curl -X POST https://verify.switchlabs.dev/api/v1/verify \
-H "X-API-Key: vai_your_api_key" \
-F "image=@photo.jpg" \
-F "policy=scooter_parking" \
-F 'metadata={"device_id":"dev_123","gps":{"lat":37.77,"lng":-122.42}}'Example: JSON with Base64
curl -X POST https://verify.switchlabs.dev/api/v1/verify \
-H "X-API-Key: vai_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"image": "data:image/jpeg;base64,/9j/4AAQ...",
"policy": "scooter_parking",
"metadata": {"device_id": "dev_123"}
}'Response
{
"id": "ver_8x92m4k9",
"created_at": "2026-01-10T14:30:00Z",
"status": "success",
"is_compliant": false,
"confidence": 0.98,
"policy": "scooter_parking",
"violation_reasons": ["blocking_sidewalk", "kickstand_up"],
"feedback": "Please deploy the kickstand and move away from the walkway.",
"metadata": {
"device_id": "dev_123",
"gps": {"lat": 37.77, "lng": -122.42}
},
"image_url": "https://...signed-url..."
}List Verifications
Retrieve a paginated list of past verifications. Results are scoped to your API key.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| limit | integer | No | Results per page (default 20, max 100) |
| cursor | string | No | Pagination cursor (created_at of last item) |
| policy | string | No | Filter by policy ID |
| status | string | No | Filter by status (success, error) |
| is_compliant | boolean | No | Filter by compliance result |
| start_date | ISO date | No | Filter: created after this date |
| end_date | ISO date | No | Filter: created before this date |
Response
{
"data": [
{
"id": "ver_8x92m4k9",
"created_at": "2026-01-10T14:30:00Z",
"status": "success",
"is_compliant": false,
"confidence": 0.98,
"policy": "scooter_parking",
"violation_reasons": ["blocking_sidewalk"],
"feedback": "...",
"metadata": {},
"image_url": "https://...signed-url..."
}
],
"has_more": true,
"next_cursor": "2026-01-10T14:30:00Z"
}Get Verification
Retrieve full details of a specific verification, including a fresh signed image URL.
Example
curl https://verify.switchlabs.dev/api/v1/verifications/ver_8x92m4k9 \
-H "X-API-Key: vai_your_api_key"Webhooks
Configure webhooks to receive queued notifications when verifications complete. Deliveries are processed every 5 minutes from your dashboard settings.
Payload Format
{
"event": "verification.completed",
"data": {
"id": "ver_8x92m4k9",
"created_at": "2026-01-10T14:30:00Z",
"status": "success",
"is_compliant": false,
"confidence": 0.98,
"policy": "scooter_parking",
"violation_reasons": ["blocking_sidewalk"],
"feedback": "Please move away from the walkway.",
"metadata": {},
"image_url": "https://...signed-url..."
}
}Signature Verification
Each delivery includes an X-VerifyAI-Signature header with format t=timestamp,v1=signature.
// Verify webhook signature (Node.js)
const crypto = require('crypto');
function verifySignature(payload, header, secret) {
const [tPart, sigPart] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = sigPart.split('=')[1];
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Retry Behavior
Failed deliveries are retried up to 3 times: after 1 minute, 5 minutes, and 30 minutes. A delivery is considered successful if your endpoint returns a 2xx status code within 5 seconds.
Policies
Available verification policies:
scooter_parkingScooter Parking ComplianceVerifies electric scooter/bike parking meets city regulations. Configurable detection parameters: vehicle in frame, upright, kickstand deployed, sidewalk clearance, entrance clearance, designated area, roadway detection, and bike rack attachment.
no_vehicle_visiblevehicle_fallenkickstand_upblocking_sidewalkblocking_entrancenot_designated_areain_roadwaynot_secured_to_rackimage_uncleardamage_detectionVehicle Damage DetectionDetects and documents damage to vehicles or equipment. Identifies type and severity of damage.
scratchesdentscracksbroken_partsflat_tiremissing_componentsstructural_damageelectrical_damagedelivery_podDelivery Proof of DeliveryVerifies package delivery placement and condition. Checks: package visible, at door, protected, upright, accessible.
no_package_visiblenot_at_doorexposed_to_weatherpackage_damagedblocking_pathimage_unclearReact Native SDK
The official SDK for React Native / Expo apps. Install with:
npm install @switchlabs/verify-ai-react-nativeVerifyAIClient
Low-level client for direct API access. Use this if you don't need React hooks.
Constructor Config
| Parameter | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Yes | Your Verify AI API key (vai_...) |
| baseUrl | string | No | API base URL (default: https://verify.switchlabs.dev/api/v1) |
| timeout | number | No | Request timeout in ms (default: 30000) |
| offlineMode | boolean | No | Enable offline queue (requires AsyncStorage) |
Methods
| Method | Returns | Description |
|---|---|---|
| verify(request) | Promise<VerificationResult> | Submit a photo for verification |
| listVerifications(params?) | Promise<VerificationListResponse> | List past verifications with filters |
| getVerification(id) | Promise<VerificationResult> | Get a single verification by ID |
VerifyAIRequestError
Thrown on non-2xx responses. Includes helper getters:
try {
const result = await client.verify({ image, policy: 'scooter_parking' });
} catch (err) {
if (err instanceof VerifyAIRequestError) {
err.status; // HTTP status code
err.body; // { error, status, current_usage?, limit?, upgrade_url? }
err.isRateLimited; // true if 429
err.isUnauthorized; // true if 401
err.upgradeUrl; // URL to upgrade plan (if 403)
}
}useVerifyAI Hook
React hook that wraps the client with loading/error state and optional offline queue.
Config
| Parameter | Type | Required | Description |
|---|---|---|---|
| apiKey | string | Yes | Your Verify AI API key |
| baseUrl | string | No | API base URL override |
| timeout | number | No | Request timeout in ms |
| offlineMode | boolean | No | Enable offline queue — queues failed requests and processes on app foreground |
Return Values
| Parameter | Type | Required | Description |
|---|---|---|---|
| verify | (request) => Promise<Result | null> | No | Submit a verification. Returns null if queued offline. |
| listVerifications | (params?) => Promise<ListResponse> | No | List past verifications |
| getVerification | (id) => Promise<Result> | No | Get a single verification |
| loading | boolean | No | True while a verify() call is in progress |
| error | Error | null | No | Most recent error (null on success) |
| lastResult | VerificationResult | null | No | Most recent successful result |
| queueSize | number | No | Number of items in offline queue |
| processQueue | () => Promise<void> | No | Manually trigger offline queue processing |
| client | VerifyAIClient | No | The underlying client instance |
| offlineQueue | OfflineQueue | null | No | Queue instance (null if offlineMode is off) |
const {
verify, loading, error, lastResult, queueSize
} = useVerifyAI({ apiKey: 'vai_...', offlineMode: true });
// verify() auto-queues on network failure when offlineMode is on
const result = await verify({ image: base64, policy: 'scooter_parking' });
// result is null if queued, VerificationResult if successfulVerifyAIScanner Component
Drop-in camera component with capture button, processing overlay, and pass/fail display.
Props
| Parameter | Type | Required | Description |
|---|---|---|---|
| onCapture | (base64) => Promise<Result | null> | Yes | Called with base64 image data when user captures a photo |
| onResult | (result) => void | No | Called when verification completes successfully |
| onError | (error) => void | No | Called when an error occurs |
| overlay | ScannerOverlayConfig | No | Overlay config: title, instructions, showGuideFrame, guideFrameAspectRatio |
| style | ViewStyle | No | Custom container style |
| showCaptureButton | boolean | No | Show default capture button (default: true) |
| captureRef | MutableRefObject | No | Ref to trigger capture programmatically from parent |
Scanner Status Flow
Overlay Config
| Parameter | Type | Required | Description |
|---|---|---|---|
| title | string | No | Title text shown at top of camera view |
| instructions | string | No | Instruction text shown when idle |
| showGuideFrame | boolean | No | Show dashed guide rectangle |
| guideFrameAspectRatio | number | No | Guide frame aspect ratio (default: 4/3) |
Offline Queue
When offlineMode: true is set, failed verification requests are automatically saved to AsyncStorage and retried when the app returns to the foreground.
| Method | Description |
|---|---|
| enqueue(request) | Manually add a request to the queue |
| getQueue() | Get all queued items |
| getQueueSize() | Get number of queued items |
| remove(id) | Remove a specific item |
| clear() | Clear all queued items |
| processQueue(onResult?, maxRetries?) | Process all items, returns { processed, failed, remaining } |
Items are stored individually in AsyncStorage to avoid Android's per-key size limit. The queue auto-processes on app foreground via the useVerifyAI hook.
TypeScript Types
import type {
VerifyAIConfig, // { apiKey, baseUrl?, timeout?, offlineMode? }
VerificationRequest, // { image: string, policy: string, metadata?, provider? }
VerificationResult, // { id, created_at, status, is_compliant, confidence, ... }
VerificationListResponse, // { data: Result[], has_more, next_cursor }
VerificationListParams, // { limit?, cursor?, policy?, status?, is_compliant?, ... }
QueueItem, // { id, request, createdAt, retryCount }
VerifyAIError, // { error, status, current_usage?, limit?, upgrade_url? }
ScannerStatus, // 'idle' | 'capturing' | 'processing' | 'success' | 'error'
ScannerOverlayConfig, // { title?, instructions?, showGuideFrame?, guideFrameAspectRatio? }
} from '@switchlabs/verify-ai-react-native';Expo Configuration
Add the camera plugin to your app.json:
{
"expo": {
"plugins": [
[
"expo-camera",
{
"cameraPermission": "Allow $(PRODUCT_NAME) to access the camera for photo verification."
}
]
]
}
}Code Examples
cURL
curl -X POST https://verify.switchlabs.dev/api/v1/verify \
-H "X-API-Key: vai_your_api_key" \
-F "image=@photo.jpg" \
-F "policy=scooter_parking"JavaScript (fetch)
const formData = new FormData();
formData.append('image', fileInput.files[0]);
formData.append('policy', 'scooter_parking');
formData.append('metadata', JSON.stringify({
device_id: 'dev_123',
user_id: 'usr_456',
}));
const response = await fetch(
'https://verify.switchlabs.dev/api/v1/verify',
{
method: 'POST',
headers: { 'X-API-Key': 'vai_your_api_key' },
body: formData,
}
);
const result = await response.json();
console.log(result.is_compliant); // true or false
console.log(result.feedback); // User-friendly messagePython (requests)
import requests
import json
response = requests.post(
'https://verify.switchlabs.dev/api/v1/verify',
headers={'X-API-Key': 'vai_your_api_key'},
files={'image': open('photo.jpg', 'rb')},
data={
'policy': 'scooter_parking',
'metadata': json.dumps({
'device_id': 'dev_123',
'user_id': 'usr_456',
}),
},
)
result = response.json()
print(f"Compliant: {result['is_compliant']}")
print(f"Feedback: {result['feedback']}")React Native (SDK)
import { VerifyAIClient } from '@switchlabs/verify-ai-react-native';
const client = new VerifyAIClient({ apiKey: 'vai_your_api_key' });
// Verify a photo
const result = await client.verify({
image: base64ImageData,
policy: 'scooter_parking',
metadata: { device_id: 'dev_123' },
});
// List recent verifications
const { data, has_more } = await client.listVerifications({
limit: 10,
policy: 'scooter_parking',
});
// Get a specific verification
const detail = await client.getVerification('ver_8x92m4k9');Error Handling (SDK)
import { VerifyAIRequestError } from '@switchlabs/verify-ai-react-native';
try {
const result = await client.verify({ image, policy: 'scooter_parking' });
} catch (err) {
if (err instanceof VerifyAIRequestError) {
if (err.isRateLimited) {
// Back off and retry
} else if (err.isUnauthorized) {
// Invalid API key
} else if (err.upgradeUrl) {
// Plan limit reached — show upgrade prompt
Linking.openURL(err.upgradeUrl);
}
}
}Offline Mode (SDK)
const { verify, queueSize, processQueue } = useVerifyAI({
apiKey: 'vai_your_api_key',
offlineMode: true, // queues failed requests automatically
});
// verify() returns null when queued offline
const result = await verify({ image: base64, policy: 'scooter_parking' });
// Queue processes automatically on app foreground, or manually:
await processQueue();
// Show pending count to user
<Text>{queueSize} photos waiting to upload</Text>Errors & Rate Limits
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad request — missing required fields or invalid image format |
| 401 | Unauthorized — invalid or missing API key |
| 403 | Forbidden — no active subscription or wrong product |
| 404 | Verification not found |
| 429 | Rate limit exceeded |
| 500 | Server error — AI processing failure |
Rate Limits
| Limit | Value |
|---|---|
| Requests per minute | 60 |
| Requests per hour | 1,000 |
| Max image size | 10 MB |
When rate limited, the response includes a Retry-After header with the number of seconds to wait.