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.
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
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:
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.
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:
{
"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:
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. |
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:
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.
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 "", 200Read 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
- API reference: Verify — every request parameter, response field, and error code.
- REST (curl) — the same calls from a shell and other languages.
- Setting up webhooks — register an endpoint and process events end to end.
- Authentication — key format, header, and rotation.