JavaScript / TypeScript
There's no separate JavaScript package to install — VerifyAI is a plain
HTTP/JSON API, and Node 18+ ships fetch and FormData built in. This
page shows the idiomatic server-side pattern: read your API key from the
environment, POST a photo to /v1/verify, and branch on a typed
response. For the in-app camera scanner with the capture / retry loop,
use the React Native SDK instead — this page
is for backend code.
The X-API-Key header authenticates against your subscription, so
anyone holding the key can spend your quota. Run these calls from a
backend and read the key from process.env — never ship it in
browser-side JavaScript. See Authentication
for key format and rotation.
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 |
| Runtime | Node 18+ (built-in fetch/FormData), or any fetch polyfill |
Initialize
There's no client object to construct — just centralize the base URL and key so every call shares them. Read the key from the environment at startup and fail fast if it's missing:
const VERIFY_AI_KEY = process.env.VERIFY_AI_KEY;
if (!VERIFY_AI_KEY) throw new Error("VERIFY_AI_KEY is not set");
const VERIFY_AI_BASE = "https://verify.switchlabs.dev/api";Verify a photo (multipart)
Multipart is the most direct way to send a file from a Node backend. Two
fields are required: image and policy. Add a metadata field (a
JSON string) to persist context like a trip or user ID alongside the
verification.
import fs from "node:fs";
const VERIFY_AI_KEY = process.env.VERIFY_AI_KEY;
const VERIFY_AI_BASE = "https://verify.switchlabs.dev/api";
const form = new FormData();
form.set("image", new Blob([fs.readFileSync("scooter.jpg")]), "scooter.jpg");
form.set("policy", "scooter_parking");
form.set("metadata", JSON.stringify({ user_id: "usr_456", trip_id: "trip_123" }));
const res = await fetch(`${VERIFY_AI_BASE}/v1/verify`, {
method: "POST",
headers: { "X-API-Key": VERIFY_AI_KEY },
body: form,
});
if (!res.ok) {
const { error } = await res.json().catch(() => ({}));
throw new Error(`verify failed: ${res.status} ${error ?? ""}`);
}
const result = await res.json();
console.log(result.is_compliant, result.category, result.violation_reasons);Verify a photo (JSON + base64)
When you already hold the image as a base64 string — say it arrived in a
JSON request from a mobile client — 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.
const res = await fetch(`${VERIFY_AI_BASE}/v1/verify`, {
method: "POST",
headers: {
"X-API-Key": VERIFY_AI_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
image: base64Image, // raw base64 or a data URL
policy: "scooter_parking",
metadata: { device_id: "dev_123" },
}),
});
const result = await 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. See the Verify endpoint
reference for every field.
Handling errors and rate limits
Non-200 responses carry a JSON { "error": "..." } body. Branch your
retry logic on the status code — only transient failures should be
retried, and 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. |
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
Network drops and client retries happen. Send an Idempotency-Key
header so a retried request returns the original cached response instead
of re-running (and re-billing) the verification. Pair it with backoff
that only retries 429 and 5xx:
import { randomUUID } from "node:crypto";
async function verifyWithRetry(form, { maxAttempts = 4 } = {}) {
const idempotencyKey = randomUUID(); // stable across this request's retries
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const res = await fetch(`${VERIFY_AI_BASE}/v1/verify`, {
method: "POST",
headers: {
"X-API-Key": VERIFY_AI_KEY,
"Idempotency-Key": idempotencyKey,
},
body: form,
});
if (res.ok) return res.json();
// Only retry transient failures.
if (res.status === 429 || res.status >= 500) {
const retryAfter = Number(res.headers.get("Retry-After")) || 2 ** attempt;
await new Promise((r) => setTimeout(r, retryAfter * 1000));
continue;
}
// 4xx (other than 429) won't succeed on retry — fail fast.
const { error } = await res.json().catch(() => ({}));
throw new Error(`verify failed: ${res.status} ${error ?? ""}`);
}
throw new Error("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. Two headers come with each delivery:
X-VerifyAI-Signature (hex HMAC-SHA256 of the raw body) and
X-VerifyAI-Timestamp (UNIX seconds, for replay protection).
const express = require("express");
const crypto = require("node:crypto");
const app = express();
const SIGNING_SECRET = process.env.VERIFY_AI_WEBHOOK_SECRET; // whsec_...
const MAX_SKEW_SECONDS = 5 * 60;
app.post(
"/webhooks/verify-ai",
express.raw({ type: "application/json" }), // capture the RAW bytes
(req, res) => {
const signature = req.header("X-VerifyAI-Signature") || "";
const timestamp = req.header("X-VerifyAI-Timestamp") || "";
// 1. Reject stale deliveries (replay protection).
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!timestamp || age > MAX_SKEW_SECONDS) {
return res.status(400).send("stale or missing timestamp");
}
// 2. Verify the HMAC over the raw body, constant-time.
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.body) // req.body is a Buffer here
.digest("hex");
const sigBuf = Buffer.from(signature, "hex");
const expBuf = Buffer.from(expected, "hex");
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).send("bad signature");
}
// 3. Safe to parse. Dedupe on event.id, ack fast, work off the request path.
const event = JSON.parse(req.body.toString("utf8"));
res.status(200).send("ok");
if (event.type === "verification.completed") {
// enqueue(event.data) ...
}
},
);Always compute the HMAC over the raw bytes you received, before any
JSON parsing. Use crypto.timingSafeEqual rather than === so the
comparison is constant-time, 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.