← Verify AIAPI Documentation
Dashboard

Verify AI API

Submit photos for AI-powered compliance verification. Get structured results with confidence scores, violation details, and user feedback.

Base URL: https://verify.switchlabs.dev/api/v1

Quick 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):

bash
npm install @switchlabs/verify-ai-react-native

With built-in camera scanner:

bash
npm install @switchlabs/verify-ai-react-native expo-camera expo-image-manipulator

For 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)

tsx
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)

tsx
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:

bash
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.

bash
curl https://verify.switchlabs.dev/api/v1/verifications \
  -H "X-API-Key: vai_your_api_key_here"

Common Auth Errors

json
{ "error": "Missing API key" }      // 401
{ "error": "Invalid API key format" } // 401
{ "error": "Invalid API key" }        // 401
{ "error": "Rate limit exceeded" }    // 429 + Retry-After

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.

Returns 400 for invalid JSON, missing fields, unsupported image types/sizes, or unknown policy IDs.

Request Parameters

ParameterTypeRequiredDescription
imagefile | base64YesThe image to verify (JPEG, PNG, WebP, max 10MB)
policystringYesPolicy ID (e.g., scooter_parking, damage_detection, delivery_pod)
metadataobjectNoArbitrary metadata to attach (device_id, user_id, gps, etc.)
providerstringNoAI provider override: "openai", "anthropic", or "gemini"

Example: Multipart Form-Data

bash
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

bash
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

json
{
  "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

ParameterTypeRequiredDescription
limitintegerNoResults per page (default 20, max 100)
cursorstringNoPagination cursor (created_at of last item)
policystringNoFilter by policy ID
statusstringNoFilter by status (success, error)
is_compliantbooleanNoFilter by compliance result
start_dateISO dateNoFilter: created after this date
end_dateISO dateNoFilter: created before this date

Response

json
{
  "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

bash
curl https://verify.switchlabs.dev/api/v1/verifications/ver_8x92m4k9 \
  -H "X-API-Key: vai_your_api_key"

Response

json
{
  "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

json
{
  "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

ParameterTypeRequiredDescription
X-VerifyAI-SignaturestringYesHMAC signature in format t=timestamp,v1=hex_signature
X-VerifyAI-EventstringYesEvent type (currently verification.completed)
X-VerifyAI-DeliverystringYesUnique delivery ID for idempotency/deduping

Signature Verification

Verify signatures against the raw request body (before JSON parsing). Reject stale timestamps to prevent replay attacks.

javascript
// 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 Compliance

Verifies 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_unclear
damage_detectionVehicle Damage Detection

Detects and documents damage to vehicles or equipment. Identifies type and severity of damage.

scratchesdentscracksbroken_partsflat_tiremissing_componentsstructural_damageelectrical_damage
delivery_podDelivery Proof of Delivery

Verifies package delivery placement and condition. Checks: package visible, at door, protected, upright, accessible.

no_package_visiblenot_at_doorexposed_to_weatherpackage_damagedblocking_pathimage_unclear

Policy 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.

ParameterTypeRequiredDescription
idstringYesUnique identifier (e.g., "compliant", "unsafe")
labelstringYesDisplay label shown in dashboard and results
colorstringYesHex color for UI display (e.g., "#22c55e")
isCompliantbooleanYesWhether this category counts as a passing result
descriptionstringNoLonger description of what this category means

Default Categories

json
[
  { "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.

ParameterTypeRequiredDescription
idstringYesUnique identifier (e.g., "kickstand_deployed")
labelstringYesDisplay label for this criterion
descriptionstringNoDetailed description of what to check
severitystringYes"critical", "warning", or "info"
requiredbooleanYesWhether failing this criterion forces a non-compliant result
json
[
  { "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.

ParameterTypeRequiredDescription
maxAttemptsnumberNoMaximum photo attempts (1–10, default: 3). After exhaustion, the scanner shows the exhausted message.
autoApproveOnExhaustbooleanNoIf 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.

ParameterTypeRequiredDescription
scannerTitlestringNoTitle shown at top of scanner view
scannerInstructionsstringNoInstruction text shown when scanner is idle
processingMessagestringNoShown while AI is analyzing the photo
successMessagestringNoShown when verification passes
failureMessagestringNoShown when verification fails
retryMessagestringNoShown when user can retry. Use {remaining} for attempt count (e.g., "Try again — {remaining} attempts left")
exhaustedMessagestringNoShown 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

ParameterTypeRequiredDescription
idstringYesPolicy ID (e.g., "scooter_parking" or a custom policy ID)

Authentication

Requires X-API-Key header, same as all other endpoints.

Example

bash
curl https://verify.switchlabs.dev/api/v1/policies/scooter_parking/config \
  -H "X-API-Key: vai_your_api_key"

Response

json
{
  "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.

tsx
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):

bash
npm install @switchlabs/verify-ai-react-native

With built-in camera scanner:

bash
npm install @switchlabs/verify-ai-react-native expo-camera expo-image-manipulator

For offline queue support, also install @react-native-async-storage/async-storage.

Imports

typescript
// 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

ParameterTypeRequiredDescription
apiKeystringYesYour Verify AI API key (vai_...)
baseUrlstringNoAPI base URL (default: https://verify.switchlabs.dev/api/v1)
timeoutnumberNoRequest timeout in ms (default: 30000)
offlineModebooleanNoEnable offline queue (requires AsyncStorage)

Methods

MethodReturnsDescription
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:

typescript
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

ParameterTypeRequiredDescription
apiKeystringYesYour Verify AI API key
baseUrlstringNoAPI base URL override
timeoutnumberNoRequest timeout in ms
offlineModebooleanNoEnable offline queue — queues failed requests and processes on app foreground

Return Values

ParameterTypeRequiredDescription
verify(request) => Promise<Result | null>NoSubmit a verification. Returns null if queued offline.
listVerifications(params?) => Promise<ListResponse>NoList past verifications
getVerification(id) => Promise<Result>NoGet a single verification
loadingbooleanNoTrue while a verify() call is in progress
errorError | nullNoMost recent error (null on success)
lastResultVerificationResult | nullNoMost recent successful result
queueSizenumberNoNumber of items in offline queue
processQueue() => Promise<void>NoManually trigger offline queue processing
clientVerifyAIClientNoThe underlying client instance
offlineQueueOfflineQueue | nullNoQueue instance (null if offlineMode is off)
tsx
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 successful

VerifyAIScanner Component

Drop-in camera component with capture button, processing overlay, and pass/fail display.

Props

ParameterTypeRequiredDescription
onCapture(base64) => Promise<Result | null>YesCalled with base64 image data when user captures a photo
onResult(result) => voidNoCalled when verification completes successfully
onError(error) => voidNoCalled when an error occurs
overlayScannerOverlayConfigNoOverlay config: title, instructions, showGuideFrame, guideFrameAspectRatio
styleViewStyleNoCustom container style
showCaptureButtonbooleanNoShow default capture button (default: true)
captureRefMutableRefObjectNoRef to trigger capture programmatically from parent

Scanner Status Flow

idle → capturing → processing → success | error → idle

Overlay Config

ParameterTypeRequiredDescription
titlestringNoTitle text shown at top of camera view
instructionsstringNoInstruction text shown when idle
showGuideFramebooleanNoShow dashed guide rectangle
guideFrameAspectRationumberNoGuide frame aspect ratio (default: 4/3)
processingMessagestringNoShown while AI is analyzing the photo
successMessagestringNoShown when verification passes
failureMessagestringNoShown when verification fails
retryMessagestringNoShown when user can retry. Supports {remaining} placeholder.
exhaustedMessagestringNoShown when all attempts are used up
maxAttemptsnumberNoMaximum photo attempts (1–10). Overrides policy config if set.
autoApproveOnExhaustbooleanNoAuto-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.

MethodDescription
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

typescript
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.

tsx
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:

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).

bash
flutter pub add verify_ai_flutter

Platform Configuration

iOS — Info.plist

Add camera permission to ios/Runner/Info.plist:

xml
<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

dart
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)

ParameterTypeRequiredDescription
apiKeyStringYesYour Verify AI API key (vai_...)
baseUrlStringNoAPI base URL (default: https://verify.switchlabs.dev/api/v1)
timeoutDurationNoRequest timeout (default: 30 seconds)
offlineModeboolNoEnable offline queue (requires shared_preferences)
dart
final client = VerifyAIClient(
  const VerifyAIConfig(apiKey: 'vai_your_api_key'),
);

Methods

MethodReturnsDescription
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()voidClose the underlying HTTP client

VerifyAIRequestError

Thrown on non-2xx responses. Includes helper getters:

dart
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

dart
final provider = VerifyAIProvider(
  const VerifyAIConfig(apiKey: 'vai_...', offlineMode: true),
);

Properties

ParameterTypeRequiredDescription
loadingboolNoTrue while a verify() call is in progress
errorObject?NoMost recent error (null on success)
lastResultVerificationResult?NoMost recent successful result
queueSizeintNoNumber of items in offline queue
clientVerifyAIClientNoThe underlying client instance
offlineQueueOfflineQueue?NoQueue instance (null if offlineMode is off)

Methods

MethodReturnsDescription
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
dart
final result = await provider.verifyMultipart(
  imageBytes: bytes,
  policy: 'scooter_parking',
);
dart
// 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

ParameterTypeRequiredDescription
onCaptureFuture<VerificationResult?> Function(Uint8List)YesCalled with raw image bytes when user captures a photo
onResultvoid Function(VerificationResult)?NoCalled when verification completes successfully
onErrorvoid Function(Object)?NoCalled when an error occurs
overlayScannerOverlayConfig?NoOverlay config: title, instructions, showGuideFrame, guideFrameAspectRatio
showCaptureButtonboolNoShow default capture button (default: true)
controllerVerifyAIScannerController?NoController to trigger capture programmatically

Scanner Status Flow

idle → capturing → processing → success | error → idle

Overlay Config

ParameterTypeRequiredDescription
titleString?NoTitle text shown at top of camera view
instructionsString?NoInstruction text shown when idle
showGuideFramebool?NoShow dashed guide rectangle
guideFrameAspectRatiodouble?NoGuide frame aspect ratio (default: 4/3)
processingMessageString?NoShown while AI is analyzing the photo
successMessageString?NoShown when verification passes
failureMessageString?NoShown when verification fails
retryMessageString?NoShown when user can retry. Supports {remaining} placeholder.
exhaustedMessageString?NoShown when all attempts are used up
maxAttemptsint?NoMaximum photo attempts (1–10). Overrides policy config if set.
autoApproveOnExhaustboolNoAuto-approve when attempts exhausted (default: false). Overrides policy config if set.
showTechnicalErrorDetailsboolNoShow HTTP status code and request ID in error cards for debugging.
themeScannerTheme?NoCustomize 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.

MethodDescription
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

dart
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 storage

Server-Driven Scanner Config

Use fetchPolicyConfig() to load scanner settings from the server for over-the-air updates.

dart
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

bash
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)

javascript
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 message

Python (requests)

python
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)

typescript
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)

dart
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)

typescript
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)

tsx
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-Signature and 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

CodeMeaning
200Success
400Bad request — invalid JSON, missing fields, invalid policy ID, or invalid image
401Unauthorized — invalid or missing API key
403Forbidden — no active subscription or wrong product
404Verification not found
429Rate limit exceeded or monthly plan limit reached
500Server error — AI processing failure

Rate Limits

LimitValue
Requests per minute60
Requests per hour1,000
Max image size10 MB

When rate limited, the response includes a Retry-After header with the number of seconds to wait.

Error Response Format

json
{ "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-After duration.
  • Retry 500 with exponential backoff and jitter.

Get in Touch

Questions about pricing, integrations, or custom deployments? We'd love to hear from you.