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:
Before you start
- An active Verify AI subscription with an API key
- At least one active policy ID from Dashboard → Policies
- For React Native SDK: React Native >= 0.72 and React >= 18
- For Flutter SDK: Flutter >= 3.16.0 and Dart >= 3.1.0
1. Install the React Native SDK
Core SDK (client, hooks, types):
npm install @switchlabs/verify-ai-react-nativeWith built-in camera scanner:
npm install @switchlabs/verify-ai-react-native expo-camera expo-image-manipulatorFor offline queue support, also install @react-native-async-storage/async-storage.
2. Get Your API Key
Sign up for a plan, then find your key in Dashboard → Settings.
3. Choose a Policy ID
Use a built-in policy ID like scooter_parking or create your own in Dashboard → Policies.
4. 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 */}</>;
}5. 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 }}
/>
);
}6. 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." }Security note: use API keys on trusted backend infrastructure whenever possible. If you must use them in mobile apps, rotate keys regularly and monitor usage from Dashboard.
Authentication
All API requests require an API key passed via the X-API-Key header.
Keys currently use the vai_... prefix. Invalid, inactive, expired, or wrong-product keys are rejected.
curl https://verify.switchlabs.dev/api/v1/verifications \
-H "X-API-Key: vai_your_api_key_here"Common Auth Errors
{ "error": "Missing API key" } // 401
{ "error": "Invalid API key format" } // 401
{ "error": "Invalid API key" } // 401
{ "error": "Rate limit exceeded" } // 429 + Retry-AfterGet 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.
Returns 400 for invalid JSON, missing fields, unsupported image types/sizes, or unknown policy IDs.
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",
"category": "unsafe",
"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",
"category": "unsafe",
"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"Response
{
"id": "ver_8x92m4k9",
"created_at": "2026-01-10T14:30:00Z",
"status": "success",
"is_compliant": false,
"confidence": 0.98,
"policy": "scooter_parking",
"category": "unsafe",
"violation_reasons": ["blocking_sidewalk"],
"feedback": "Please move away from the walkway.",
"metadata": {},
"image_url": "https://...signed-url...",
"processing_time_ms": 1187,
"error_message": null
}Webhooks
Configure webhooks in Dashboard → Settings to receive verification.completed events.
Webhook URLs must be public HTTPS endpoints (no localhost/private network hosts). A signing secret is shown once when the webhook is created.
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",
"category": "unsafe",
"violation_reasons": ["blocking_sidewalk"],
"feedback": "Please move away from the walkway.",
"metadata": {},
"image_url": "https://...signed-url..."
}
}Delivery Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
| X-VerifyAI-Signature | string | Yes | HMAC signature in format t=timestamp,v1=hex_signature |
| X-VerifyAI-Event | string | Yes | Event type (currently verification.completed) |
| X-VerifyAI-Delivery | string | Yes | Unique delivery ID for idempotency/deduping |
Signature Verification
Verify signatures against the raw request body (before JSON parsing). Reject stale timestamps to prevent replay attacks.
// Verify webhook signature (Node.js)
const crypto = require('crypto');
const MAX_SKEW_SECONDS = 300; // 5 minutes
function verifySignature(rawBody, signatureHeader, secret) {
if (!signatureHeader) return false;
const parts = Object.fromEntries(
signatureHeader.split(',').map(part => part.split('='))
);
const timestamp = Number(parts.t);
const signature = parts.v1;
if (!timestamp || !signature) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > MAX_SKEW_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + rawBody)
.digest('hex');
const providedBuf = Buffer.from(signature, 'hex');
const expectedBuf = Buffer.from(expected, 'hex');
if (providedBuf.length !== expectedBuf.length) return false;
return crypto.timingSafeEqual(providedBuf, expectedBuf);
}Retry Behavior
Verify AI attempts delivery immediately after each verification. Failed deliveries are retried up to 3 times (after 1 minute, 5 minutes, and 30 minutes). Retry jobs are polled every 5 minutes. A delivery is successful when your endpoint returns any 2xx status within 5 seconds.
Policies
Available verification policies. You can also create custom policies in Dashboard → Policies and use those IDs with the same API endpoints.
For structured configuration (categories, criteria, attempt limits, and custom UI copy), see Policy Configuration below.
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_unclearPolicy Configuration
Policies support two configuration modes: Structured (visual builder) and Advanced (raw prompt). Structured mode is recommended for new policies — it gives you granular control over outcome categories, verification criteria, attempt limits, and custom UI messages.
Configure policies in Dashboard → Policies. The configuration is returned by the GET /policies/:id/config endpoint so your app can adapt at runtime without a new build.
Categories
Define outcome categories that the AI assigns to each verification. Each category has a compliance flag that determines the pass/fail result.
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Unique identifier (e.g., "compliant", "unsafe") |
| label | string | Yes | Display label shown in dashboard and results |
| color | string | Yes | Hex color for UI display (e.g., "#22c55e") |
| isCompliant | boolean | Yes | Whether this category counts as a passing result |
| description | string | No | Longer description of what this category means |
Default Categories
[
{ "id": "compliant", "label": "Compliant", "color": "#22c55e", "isCompliant": true,
"description": "Meets all requirements" },
{ "id": "improvable", "label": "Improvable", "color": "#eab308", "isCompliant": false,
"description": "Minor issues that could be improved" },
{ "id": "unsafe", "label": "Unsafe", "color": "#ef4444", "isCompliant": false,
"description": "Fails critical safety requirements" },
{ "id": "lacks_info", "label": "Lacks Information", "color": "#6b7280", "isCompliant": false,
"description": "Not enough information to determine compliance" }
]Criteria
Define specific verification criteria the AI evaluates. Each criterion has a severity level and can be marked as required.
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Unique identifier (e.g., "kickstand_deployed") |
| label | string | Yes | Display label for this criterion |
| description | string | No | Detailed description of what to check |
| severity | string | Yes | "critical", "warning", or "info" |
| required | boolean | Yes | Whether failing this criterion forces a non-compliant result |
[
{ "id": "vehicle_visible", "label": "Vehicle Visible", "severity": "critical",
"required": true, "description": "A scooter or bike must be clearly visible" },
{ "id": "kickstand_deployed", "label": "Kickstand Deployed", "severity": "critical",
"required": true, "description": "Vehicle kickstand must be down" },
{ "id": "clear_of_sidewalk", "label": "Clear of Sidewalk", "severity": "warning",
"required": false, "description": "Vehicle should not block pedestrian path" }
]Attempt Limits
Control how many times a user can retry photo verification before the scanner stops accepting attempts.
| Parameter | Type | Required | Description |
|---|---|---|---|
| maxAttempts | number | No | Maximum photo attempts (1–10, default: 3). After exhaustion, the scanner shows the exhausted message. |
| autoApproveOnExhaust | boolean | No | If true, automatically approve when attempts are exhausted (default: false). Useful for non-critical verifications where blocking the user is worse than accepting a marginal photo. |
Custom UI Copy
Override the default scanner overlay messages. All fields are optional — omitted fields use SDK defaults.
| Parameter | Type | Required | Description |
|---|---|---|---|
| scannerTitle | string | No | Title shown at top of scanner view |
| scannerInstructions | string | No | Instruction text shown when scanner is idle |
| processingMessage | string | No | Shown while AI is analyzing the photo |
| successMessage | string | No | Shown when verification passes |
| failureMessage | string | No | Shown when verification fails |
| retryMessage | string | No | Shown when user can retry. Use {remaining} for attempt count (e.g., "Try again — {remaining} attempts left") |
| exhaustedMessage | string | No | Shown when all attempts are used up |
Config API Endpoint
Returns the structured configuration for a policy. Use this to drive scanner settings from the server so you can update behavior without shipping a new app build.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Policy ID (e.g., "scooter_parking" or a custom policy ID) |
Authentication
Requires X-API-Key header, same as all other endpoints.
Example
curl https://verify.switchlabs.dev/api/v1/policies/scooter_parking/config \
-H "X-API-Key: vai_your_api_key"Response
{
"maxAttempts": 3,
"autoApproveOnExhaust": false,
"uiCopy": {
"scannerTitle": "Parking Verification",
"scannerInstructions": "Center your scooter in the frame",
"processingMessage": "Checking parking compliance...",
"successMessage": "Parking approved!",
"failureMessage": "Parking issue detected",
"retryMessage": "Try again — {remaining} attempts left",
"exhaustedMessage": "Maximum attempts reached"
},
"categories": [
{ "id": "compliant", "label": "Compliant", "color": "#22c55e",
"isCompliant": true, "description": "Meets all requirements" },
{ "id": "improvable", "label": "Improvable", "color": "#eab308",
"isCompliant": false, "description": "Minor issues" },
{ "id": "unsafe", "label": "Unsafe", "color": "#ef4444",
"isCompliant": false, "description": "Fails critical safety requirements" },
{ "id": "lacks_info", "label": "Lacks Information", "color": "#6b7280",
"isCompliant": false, "description": "Not enough info to determine compliance" }
]
}Server-Driven Scanner Config
Fetch the policy config at app startup and pass it to the scanner overlay. This lets you update categories, attempt limits, and UI copy from the dashboard without releasing a new app version.
import { VerifyAIClient } from '@switchlabs/verify-ai-react-native';
import { VerifyAIScanner } from '@switchlabs/verify-ai-react-native/scanner';
import { useEffect, useState } from 'react';
const client = new VerifyAIClient({ apiKey: 'vai_your_api_key' });
function ScannerScreen() {
const [config, setConfig] = useState(null);
useEffect(() => {
client.fetchPolicyConfig('scooter_parking').then(setConfig);
}, []);
if (!config) return null; // or a loading spinner
return (
<VerifyAIScanner
onCapture={(base64) =>
client.verify({ image: base64, policy: 'scooter_parking' })
}
onResult={(result) => console.log(result.category, result.is_compliant)}
overlay={{
title: config.uiCopy.scannerTitle,
instructions: config.uiCopy.scannerInstructions,
processingMessage: config.uiCopy.processingMessage,
successMessage: config.uiCopy.successMessage,
failureMessage: config.uiCopy.failureMessage,
retryMessage: config.uiCopy.retryMessage,
exhaustedMessage: config.uiCopy.exhaustedMessage,
maxAttempts: config.maxAttempts,
autoApproveOnExhaust: config.autoApproveOnExhaust,
showGuideFrame: true,
}}
/>
);
}React Native SDK
The official SDK for React Native / Expo apps.
Core SDK (client, hooks, types):
npm install @switchlabs/verify-ai-react-nativeWith built-in camera scanner:
npm install @switchlabs/verify-ai-react-native expo-camera expo-image-manipulatorFor offline queue support, also install @react-native-async-storage/async-storage.
Imports
// Core SDK — no native dependencies beyond react-native
import { VerifyAIClient, useVerifyAI } from '@switchlabs/verify-ai-react-native';
// Scanner component — requires expo-camera
import { VerifyAIScanner } from '@switchlabs/verify-ai-react-native/scanner';VerifyAIClient
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 |
| fetchPolicyConfig(policyId) | Promise<PolicyConfigResponse> | Fetch structured policy configuration (categories, criteria, limits, UI copy) |
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.isRetryable; // true for 408, 429, 5xx
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 transient failures 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) |
| processingMessage | string | No | Shown while AI is analyzing the photo |
| successMessage | string | No | Shown when verification passes |
| failureMessage | string | No | Shown when verification fails |
| retryMessage | string | No | Shown when user can retry. Supports {remaining} placeholder. |
| exhaustedMessage | string | No | Shown when all attempts are used up |
| maxAttempts | number | No | Maximum photo attempts (1–10). Overrides policy config if set. |
| autoApproveOnExhaust | boolean | No | Auto-approve when attempts exhausted. Overrides policy config if set. |
Offline Queue
When offlineMode: true is set, transient failures (network errors, timeouts, 429, and 5xx responses) 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?, processingMessage?, ... }
PolicyConfigResponse, // { maxAttempts, autoApproveOnExhaust, uiCopy, categories }
} from '@switchlabs/verify-ai-react-native';Server-Driven Scanner Config
Use fetchPolicyConfig() to load scanner settings from the server. This enables over-the-air updates to categories, attempt limits, and UI copy without a new app release.
import { useVerifyAI } from '@switchlabs/verify-ai-react-native';
import { VerifyAIScanner } from '@switchlabs/verify-ai-react-native/scanner';
import { useEffect, useState } from 'react';
function ScannerScreen() {
const { verify, client } = useVerifyAI({ apiKey: 'vai_your_api_key' });
const [config, setConfig] = useState(null);
useEffect(() => {
client.fetchPolicyConfig('scooter_parking').then(setConfig);
}, []);
if (!config) return null;
return (
<VerifyAIScanner
onCapture={(base64) => verify({ image: base64, policy: 'scooter_parking' })}
onResult={(result) => console.log(result.category)}
overlay={{
title: config.uiCopy.scannerTitle,
instructions: config.uiCopy.scannerInstructions,
processingMessage: config.uiCopy.processingMessage,
successMessage: config.uiCopy.successMessage,
failureMessage: config.uiCopy.failureMessage,
retryMessage: config.uiCopy.retryMessage,
exhaustedMessage: config.uiCopy.exhaustedMessage,
maxAttempts: config.maxAttempts,
autoApproveOnExhaust: config.autoApproveOnExhaust,
showGuideFrame: true,
}}
/>
);
}Image Handling & Optional Dependencies
The scanner outputs base64 by default — use verify() in the onCapture callback. If expo-image-manipulator is installed, the scanner automatically resizes images to 1600px (longest side), normalizes EXIF orientation, and encodes JPEG at quality 0.8. If not installed, the scanner falls back to JPEG compression at quality 0.65 to keep payloads smaller. For best results on high-megapixel devices (e.g. Samsung Galaxy S24), install expo-image-manipulator as an optional dependency.
verifyMultipart() is also available for advanced use — it streams a file URI directly without base64 overhead. Use it when you have a file URI from a gallery pick or custom camera implementation.
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."
}
]
]
}
}Flutter SDK
The official SDK for Flutter apps (iOS & Android).
flutter pub add verify_ai_flutterPlatform Configuration
iOS — Info.plist
Add camera permission to ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera is used for photo verification</string>Android — build.gradle
The camera plugin requires minSdkVersion 21 or higher in android/app/build.gradle.
Imports
import 'package:verify_ai_flutter/verify_ai_flutter.dart';VerifyAIClient
Low-level HTTP client for direct API access. Use this if you don't need state management.
Constructor Config (VerifyAIConfig)
| 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 | Duration | No | Request timeout (default: 30 seconds) |
| offlineMode | bool | No | Enable offline queue (requires shared_preferences) |
final client = VerifyAIClient(
const VerifyAIConfig(apiKey: 'vai_your_api_key'),
);Methods
| Method | Returns | Description |
|---|---|---|
| verify(request) | Future<VerificationResult> | Submit a photo for verification |
| verifyMultipart({ imageBytes, policy, metadata?, aiProvider?, includeImageData?, idempotencyKey? }) | Future<VerificationResult> | Submit raw image bytes with multipart/form-data |
| listVerifications([params]) | Future<VerificationListResponse> | List past verifications with filters |
| getVerification(id) | Future<VerificationResult> | Get a single verification by ID |
| fetchPolicyConfig(policyId) | Future<Map<String, dynamic>> | Fetch structured policy configuration (categories, criteria, limits, UI copy) |
| dispose() | void | Close the underlying HTTP client |
VerifyAIRequestError
Thrown on non-2xx responses. Includes helper getters:
try {
final result = await client.verify(
const VerificationRequest(image: base64, policy: 'scooter_parking'),
);
} on VerifyAIRequestError catch (e) {
e.status; // HTTP status code
e.body; // VerifyAIError with error, upgradeUrl, etc.
e.isRateLimited; // true if 429
e.isUnauthorized; // true if 401
e.isRetryable; // true for 0, 408, 429, 5xx
e.upgradeUrl; // URL to upgrade plan (if 403)
}VerifyAIProvider (ChangeNotifier)
State management wrapper using Flutter's ChangeNotifier. Equivalent to the React Native useVerifyAI hook.
Constructor
final provider = VerifyAIProvider(
const VerifyAIConfig(apiKey: 'vai_...', offlineMode: true),
);Properties
| Parameter | Type | Required | Description |
|---|---|---|---|
| loading | bool | No | True while a verify() call is in progress |
| error | Object? | No | Most recent error (null on success) |
| lastResult | VerificationResult? | No | Most recent successful result |
| queueSize | int | No | Number of items in offline queue |
| client | VerifyAIClient | No | The underlying client instance |
| offlineQueue | OfflineQueue? | No | Queue instance (null if offlineMode is off) |
Methods
| Method | Returns | Description |
|---|---|---|
| verify(request) | Future<VerificationResult?> | Submit verification; null if queued offline |
| verifyMultipart({ imageBytes, policy, metadata?, provider?, includeImageData? }) | Future<VerificationResult?> | Submit raw image bytes without base64 encoding |
| listVerifications([params]) | Future<VerificationListResponse> | List past verifications |
| getVerification(id) | Future<VerificationResult> | Get a single verification |
| processQueue() | Future<void> | Manually trigger offline queue processing |
| onAppResumed() | Future<void> | Call from WidgetsBindingObserver on resume |
final result = await provider.verifyMultipart(
imageBytes: bytes,
policy: 'scooter_parking',
);// Use with ListenableBuilder
ListenableBuilder(
listenable: provider,
builder: (context, child) {
if (provider.loading) return CircularProgressIndicator();
if (provider.lastResult != null) {
return Text('Compliant: ${provider.lastResult!.isCompliant}');
}
return ElevatedButton(
onPressed: () => provider.verify(
const VerificationRequest(image: base64, policy: 'scooter_parking'),
),
child: Text('Verify'),
);
},
)VerifyAIScanner Widget
Drop-in camera widget with capture button, processing overlay, and pass/fail display.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| onCapture | Future<VerificationResult?> Function(Uint8List) | Yes | Called with raw image bytes when user captures a photo |
| onResult | void Function(VerificationResult)? | No | Called when verification completes successfully |
| onError | void Function(Object)? | No | Called when an error occurs |
| overlay | ScannerOverlayConfig? | No | Overlay config: title, instructions, showGuideFrame, guideFrameAspectRatio |
| showCaptureButton | bool | No | Show default capture button (default: true) |
| controller | VerifyAIScannerController? | No | Controller to trigger capture programmatically |
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 | bool? | No | Show dashed guide rectangle |
| guideFrameAspectRatio | double? | No | Guide frame aspect ratio (default: 4/3) |
| processingMessage | String? | No | Shown while AI is analyzing the photo |
| successMessage | String? | No | Shown when verification passes |
| failureMessage | String? | No | Shown when verification fails |
| retryMessage | String? | No | Shown when user can retry. Supports {remaining} placeholder. |
| exhaustedMessage | String? | No | Shown when all attempts are used up |
| maxAttempts | int? | No | Maximum photo attempts (1–10). Overrides policy config if set. |
| autoApproveOnExhaust | bool | No | Auto-approve when attempts exhausted (default: false). Overrides policy config if set. |
| showTechnicalErrorDetails | bool | No | Show HTTP status code and request ID in error cards for debugging. |
| theme | ScannerTheme? | No | Customize scanner colors and text styles. |
Offline Queue
When offlineMode: true is set, transient failures (network errors, timeouts, 429, and 5xx responses) are automatically saved to SharedPreferences and retried when the app resumes.
| Method | Description |
|---|---|
| enqueue(request) | Add a request to the queue; returns a temporary ID |
| 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 ({int processed, int failed, int remaining}) |
Items are stored individually in SharedPreferences with a manifest key. The queue auto-processes on app foreground via the VerifyAIProvider.onAppResumed() method.
Dart Types
import 'package:verify_ai_flutter/verify_ai_flutter.dart';
// All exported types:
// VerifyAIConfig — { apiKey, baseUrl?, timeout?, offlineMode? }
// VerificationRequest — { image, policy, metadata?, provider? }
// VerificationResult — { id, createdAt, status, isCompliant, confidence, ... }
// VerificationListResponse — { data, hasMore, nextCursor }
// VerificationListParams — { limit?, cursor?, policy?, status?, isCompliant?, ... }
// QueueItem — { id, idempotencyKey, request, createdAt, retryCount }
// VerifyAIError — { error, status?, currentUsage?, limit?, upgradeUrl? }
// ScannerStatus — idle | capturing | processing | success | error
// ScannerOptions — convenience config for VerifyAI.presentScanner()
// ScannerOverlayConfig — { title?, instructions?, showGuideFrame?, guideFrameAspectRatio?, processingMessage?, ... }
// VerifyAIRequestError — Exception with status, body, isRetryable, etc.
// PolicyConfigResponse — fetchPolicyConfig() returns Map<String, dynamic>
// VerifyAI — high-level wrapper with presentScanner()
// VerifyAIClient — HTTP client
// VerifyAIProvider — ChangeNotifier state manager
// VerifyAIScanner — Camera widget
// VerifyAIScannerController — Programmatic capture controller
// OfflineQueue — Offline request storageServer-Driven Scanner Config
Use fetchPolicyConfig() to load scanner settings from the server for over-the-air updates.
import 'package:verify_ai_flutter/verify_ai_flutter.dart';
class ScannerScreen extends StatefulWidget {
@override
State<ScannerScreen> createState() => _ScannerScreenState();
}
class _ScannerScreenState extends State<ScannerScreen> {
final client = VerifyAIClient(
const VerifyAIConfig(apiKey: 'vai_your_api_key'),
);
Map<String, dynamic>? config;
@override
void initState() {
super.initState();
client.fetchPolicyConfig('scooter_parking').then((c) {
setState(() => config = c);
});
}
@override
Widget build(BuildContext context) {
if (config == null) return const CircularProgressIndicator();
final uiCopy = config!['uiCopy'] as Map<String, dynamic>? ?? {};
return VerifyAIScanner(
onCapture: (bytes) => client.verifyMultipart(
imageBytes: bytes,
policy: 'scooter_parking',
),
onResult: (result) => print(result.isCompliant),
overlay: ScannerOverlayConfig(
title: uiCopy['scannerTitle'],
instructions: uiCopy['scannerInstructions'],
processingMessage: uiCopy['processingMessage'],
successMessage: uiCopy['successMessage'],
failureMessage: uiCopy['failureMessage'],
retryMessage: uiCopy['retryMessage'],
exhaustedMessage: uiCopy['exhaustedMessage'],
maxAttempts: config!['maxAttempts'],
autoApproveOnExhaust: config!['autoApproveOnExhaust'] ?? false,
showGuideFrame: true,
),
);
}
}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');Flutter (SDK)
import 'package:verify_ai_flutter/verify_ai_flutter.dart';
final client = VerifyAIClient(
const VerifyAIConfig(apiKey: 'vai_your_api_key'),
);
// Verify a photo
final result = await client.verify(
const VerificationRequest(
image: base64ImageData,
policy: 'scooter_parking',
metadata: {'device_id': 'dev_123'},
),
);
// List recent verifications
final list = await client.listVerifications(
const VerificationListParams(limit: 10, policy: 'scooter_parking'),
);
// Get a specific verification
final 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 transient failures 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>Go-Live Checklist
- Store API keys securely and rotate them from Dashboard → Settings if exposed.
- Use active policy IDs from Dashboard → Policies.
- Enforce client-side checks for file type (JPEG/PNG/WebP) and max image size (10MB).
- Implement retry with exponential backoff for 429 and 5xx responses.
- If using webhooks, verify
X-VerifyAI-Signatureand respond with 2xx in under 5 seconds. - Use Dashboard → Sandbox to test policies before production rollout.
- Configure policy categories and criteria in Dashboard → Policies for structured AI analysis.
- Use
fetchPolicyConfig()to drive scanner settings server-side so you can update UI copy and attempt limits without an app release. - Monitor usage and errors in Dashboard during launch week.
Errors & Rate Limits
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad request — invalid JSON, missing fields, invalid policy ID, or invalid image |
| 401 | Unauthorized — invalid or missing API key |
| 403 | Forbidden — no active subscription or wrong product |
| 404 | Verification not found |
| 429 | Rate limit exceeded or monthly plan limit reached |
| 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.
Error Response Format
{ "error": "Missing API key" }
{ "error": "Invalid policy: your_policy_id" }
{ "error": "Rate limit exceeded" }Retry Guidance
- Do not retry 400/401/403/404 until request data or credentials are fixed.
- Retry 429 after waiting the
Retry-Afterduration. - Retry 500 with exponential backoff and jitter.