Python

There's no separate Python package to install — VerifyAI is a plain HTTP/JSON API, so any HTTP client works. This page uses requests for the synchronous path and httpx when you need async. Read your API key from the environment, POST a photo to /v1/verify, and branch on the structured response. This is backend-only code; for an in-app camera scanner, use the React Native or Flutter SDK instead.

Keep your key on the server

The X-API-Key header authenticates against your subscription, so anyone holding the key can spend your quota. Run these calls from a backend service and read the key from the environment. See Authentication for key format and rotation.

Install

bash
pip install requests   # synchronous
pip install httpx      # async (optional)

The essentials

| | | | --------------- | ------------------------------------------------------ | | Base URL | https://verify.switchlabs.dev/api | | Auth | X-API-Key: vai_… header on every request | | Content | multipart/form-data or application/json | | Image limit | 10 MB, JPEG / PNG / WebP |

Initialize

There's no client object to construct — just centralize the base URL and key. Read the key from the environment at startup so it never lives in source control:

python
import os
 
VERIFY_AI_KEY = os.environ["VERIFY_AI_KEY"]
VERIFY_AI_BASE = "https://verify.switchlabs.dev/api"

Verify a photo

Multipart is the most direct way to send a file. Two fields are required: image and policy. Pass metadata as a JSON string to persist context like a trip or user ID alongside the verification.

import os, json, requests

VERIFY_AI_KEY = os.environ["VERIFY_AI_KEY"]
VERIFY_AI_BASE = "https://verify.switchlabs.dev/api"

with open("scooter.jpg", "rb") as f:
  res = requests.post(
      f"{VERIFY_AI_BASE}/v1/verify",
      headers={"X-API-Key": VERIFY_AI_KEY},
      files={"image": f},
      data={
          "policy": "scooter_parking",
          "metadata": json.dumps({"user_id": "usr_456", "trip_id": "trip_123"}),
      },
      timeout=30,
  )

res.raise_for_status()
result = res.json()
print(result["is_compliant"], result["category"], result["violation_reasons"])

JSON + base64

When you already hold the image as a base64 string, POST application/json instead of a file. The image field accepts a raw base64 string or a full data URL; the data:image/...;base64, prefix is stripped automatically.

python
import os, base64, requests
 
with open("scooter.jpg", "rb") as f:
    b64 = base64.b64encode(f.read()).decode()
 
res = requests.post(
    f"{VERIFY_AI_BASE}/v1/verify",
    headers={
        "X-API-Key": VERIFY_AI_KEY,
        "Content-Type": "application/json",
    },
    json={
        "image": b64,
        "policy": "scooter_parking",
        "metadata": {"device_id": "dev_123"},
    },
    timeout=30,
)
result = res.json()

The response

A 200 OK is a Verification object. The fields you'll branch on most:

json
{
  "id": "ver_8x92m4k9",
  "created_at": "2026-06-01T14:30:00Z",
  "status": "success",
  "is_compliant": false,
  "confidence": 0.94,
  "policy": "scooter_parking",
  "category": "unsafe",
  "violation_reasons": ["blocking_sidewalk", "kickstand_up"],
  "feedback": "Please deploy the kickstand and move away from the walkway.",
  "image_url": "https://...signed-url...",
  "evaluation_source": "cloud_vlm"
}

is_compliant is the top-level pass/fail; category is the outcome bucket the policy resolved to; violation_reasons lists the failed criterion IDs; and feedback is a human-readable string safe to surface to an end user:

python
if result["is_compliant"]:
    end_ride()
else:
    notify_rider(result["feedback"], result["violation_reasons"])

See the Verify endpoint reference for every field.

Handling errors and rate limits

Non-200 responses carry a JSON {"error": "..."} body. raise_for_status() turns any 4xx/5xx into an HTTPError, but you'll usually want to branch on the status code so you only retry transient failures — a 429 tells you exactly how long to wait via the Retry-After header:

| Status | Action | | ------ | ---------------------------------------------------------------------------- | | 400 | Fix the request — missing field, image too large, or unknown policy ID. | | 401 | Check the X-API-Key header. | | 403 | Key isn't valid for VerifyAI, or no active subscription. | | 429 | Back off — read Retry-After (seconds) and retry after it. | | 5xx | Transient — retry with backoff, reusing your Idempotency-Key. |

python
if res.status_code != 200:
    body = res.json() if res.headers.get("content-type", "").startswith("application/json") else {}
    request_id = res.headers.get("X-Request-Id")
    raise RuntimeError(f"verify failed {res.status_code}: {body.get('error')} (req {request_id})")

Every response carries an X-Request-Id header. Log it on failures — it's the fastest way for support to trace a specific call.

Retries with idempotency

Send an Idempotency-Key header so a retried request returns the original cached response instead of re-running (and re-billing) the verification. Keep the same key across one request's retries, and only retry 429 and 5xx:

python
import os, time, uuid, requests
 
def verify_with_retry(path: str, policy: str, max_attempts: int = 4) -> dict:
    idempotency_key = str(uuid.uuid4())  # stable across this request's retries
 
    for attempt in range(1, max_attempts + 1):
        with open(path, "rb") as f:
            res = requests.post(
                f"{VERIFY_AI_BASE}/v1/verify",
                headers={
                    "X-API-Key": VERIFY_AI_KEY,
                    "Idempotency-Key": idempotency_key,
                },
                files={"image": f},
                data={"policy": policy},
                timeout=30,
            )
 
        if res.status_code == 200:
            return res.json()
 
        # Only retry transient failures.
        if res.status_code == 429 or res.status_code >= 500:
            retry_after = int(res.headers.get("Retry-After") or 2 ** attempt)
            time.sleep(retry_after)
            continue
 
        # 4xx (other than 429) won't succeed on retry — fail fast.
        res.raise_for_status()
 
    raise RuntimeError("verify failed after retries")

Keys are scoped per route. Reusing a key with a different request body returns 422 Unprocessable Entity, so generate a fresh key per logical request and reuse it only across that request's retries.

Verify webhook signatures

If you subscribe to verification events, every delivery is signed. Verify the HMAC over the exact raw bytes of the request body before parsing JSON — re-serializing the parsed body changes whitespace and key order, so the signature won't match. Each delivery carries two headers: X-VerifyAI-Signature (hex HMAC-SHA256 of the raw body) and X-VerifyAI-Timestamp (UNIX seconds, for replay protection). Use hmac.compare_digest so the comparison is constant-time.

python
import os, time, hmac, hashlib, json
from flask import Flask, request, abort
 
app = Flask(__name__)
SIGNING_SECRET = os.environ["VERIFY_AI_WEBHOOK_SECRET"]  # whsec_...
MAX_SKEW_SECONDS = 5 * 60
 
@app.post("/webhooks/verify-ai")
def verify_ai_webhook():
    raw = request.get_data()  # exact raw bytes — do NOT use request.json here
    signature = request.headers.get("X-VerifyAI-Signature", "")
    timestamp = request.headers.get("X-VerifyAI-Timestamp", "")
 
    # 1. Reject stale deliveries (replay protection).
    if not timestamp or abs(time.time() - int(timestamp)) > MAX_SKEW_SECONDS:
        abort(400, "stale or missing timestamp")
 
    # 2. Verify the HMAC over the raw body, constant-time.
    expected = hmac.new(SIGNING_SECRET.encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature):
        abort(401, "bad signature")
 
    # 3. Safe to parse. Dedupe on event["id"], ack fast, work off the request path.
    event = json.loads(raw)
    if event["type"] == "verification.completed":
        enqueue(event["data"])  # your queue
 
    return "", 200
Verify before parse, always

Read request.get_data() for the raw bytes — never request.json/request.get_json(), which consumes and re-parses the body so the HMAC will never match. Use hmac.compare_digest rather than ==, and reject timestamps more than a few minutes old as replays.

The Setting up webhooks guide covers endpoint registration, the full payload envelope, and idempotent processing of at-least-once retries.

What's next

Get in Touch

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