API reference
The same detection pipeline that powers the analyst console, exposed over a small JSON API. Base URL: https://api.verifai.example. All responses are JSON. All requests must be HTTPS.
Authentication
Every request needs an Authorization: Bearer sk_live_... header. Create keys under Settings → API keys in the console. The full key is shown once at creation - only its SHA-256 hash is stored server-side.
Keys are scoped to a single organization. Revoking a key takes effect on the next request.
Submit a scan
/api/public/v1/scansPre-sample the video on your side into 1-12 evenly spaced JPEG frames (recommended 6-8) and submit them as base64 alongside a millisecond timestamp. Frames are analyzed synchronously; the response contains the final verdict, per-signal scores, and rate-limit headers.
Request body
| filename | string (optional) | ≤ 300 chars | Original filename. Surfaced in the analyst console for triage. |
| external_ref | string (optional) | ≤ 200 chars | Your reference (KYC session id, case number). Idempotency is NOT enforced - resubmitting the same ref creates a second scan. |
| frames | array (required) | 1-12 items | Ordered JPEG frames. |
| frames[].ts_ms | integer (required) | ≥ 0 | Frame timestamp within the source video, in milliseconds. |
| frames[].image_base64 | string (required) | 100 - 3.5M chars | Base64-encoded JPEG. No data: prefix. Recommended 512-1024 px longest side. |
Example request
curl -X POST https://api.verifai.example/api/public/v1/scans \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"filename": "kyc-selfie-8123.mp4",
"external_ref": "kyc-session-8123",
"frames": [
{ "ts_ms": 0, "image_base64": "/9j/4AAQSkZJRgAB..." },
{ "ts_ms": 500, "image_base64": "/9j/4AAQSkZJRgAB..." },
{ "ts_ms": 1000, "image_base64": "/9j/4AAQSkZJRgAB..." },
{ "ts_ms": 1500, "image_base64": "/9j/4AAQSkZJRgAB..." },
{ "ts_ms": 2000, "image_base64": "/9j/4AAQSkZJRgAB..." },
{ "ts_ms": 2500, "image_base64": "/9j/4AAQSkZJRgAB..." }
]
}'Response - 200 OK
HTTP/1.1 200 OK
content-type: application/json
x-ratelimit-limit-minute: 30
x-ratelimit-remaining-minute: 29
x-ratelimit-limit-month: 10000
x-ratelimit-remaining-month: 9873
{
"scan_id": "b41a9c8f-2d1e-4c3b-9a77-1e0f2f4d0e01",
"verdict": "likely_synthetic",
"score": 0.82,
"confidence": 0.91,
"signals": [
{ "signal": "frame_artifacts", "score": 0.88, "confidence": 0.92, "weight": 0.22 },
{ "signal": "temporal_consistency", "score": 0.74, "confidence": 0.80, "weight": 0.14 },
{ "signal": "cardiac_liveness", "score": 0.90, "confidence": 0.78, "weight": 0.17 },
{ "signal": "physics_consistency", "score": 0.71, "confidence": 0.75, "weight": 0.17 },
{ "signal": "provenance", "score": 0.95, "confidence": 0.99, "weight": 0.10 }
]
}Response fields
| scan_id | uuid | - | Stable id. Use for GET, PDF report, and signed receipt. |
| verdict | enum | "authentic" | "suspicious" | "likely_synthetic" | "inconclusive" | Final calibrated verdict. See below. |
| score | number | 0 - 1 | Weighted ensemble suspicion score. |
| confidence | number | 0 - 1 | Aggregate confidence across signals. Below 0.35 forces verdict to inconclusive. |
| signals[] | array | - | Per-signal breakdown used to compute the verdict. |
| signals[].signal | string | - | Signal id. See Signal glossary. |
| signals[].score | number | 0 - 1 | Suspicion contribution of this signal (higher = more likely synthetic). |
| signals[].confidence | number | 0 - 1 | How much weight this signal deserves for this specific clip. |
| signals[].weight | number | 0 - 1 | Ensemble weight for this signal. |
Special case - known-synthetic short-circuit
If the video's perceptual fingerprint matches an entry in the cross-org threat-intel registry, the full pipeline is skipped and a single known_synthetic signal is returned. The verdict is locked at likely_synthetic.
HTTP/1.1 200 OK
{
"scan_id": "b41a9c8f-...",
"verdict": "likely_synthetic",
"score": 1,
"confidence": 0.99,
"signals": [
{
"signal": "known_synthetic",
"score": 1,
"confidence": 0.99,
"weight": 1
}
]
}Fetch an existing scan
/api/public/v1/scans/:idRead a previously submitted scan, including any analyst override applied in the console.
curl https://api.verifai.example/api/public/v1/scans/b41a9c8f-... \ -H "Authorization: Bearer sk_live_..."
Returns the same shape as the POST response, plus a reviewed boolean and (if applicable) a reviewed_verdict. When present, reviewed_verdict is authoritative - the automated verdict is preserved for auditability under automated_verdict.
Signed verdict receipt
/api/public/v1/scans/:id/receiptReturns a compact JWS signed with the VerifAI Ed25519 key. Anyone can verify it offline against the public key at /api/public/v1/pubkey - no network call to VerifAI is required to trust a receipt. The payload includes the current head of your organization's audit chain, binding the verdict to the tamper-evident log.
curl https://api.verifai.example/api/public/v1/scans/b41a9c8f-.../receipt \ -H "Authorization: Bearer sk_live_..."
{
"receipt": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ2ZXJpZmFpIiw...sig",
"payload": {
"iss": "verifai",
"typ": "verdict-receipt",
"scan_id": "b41a9c8f-...",
"org_id": "3fa9-...",
"verdict": "likely_synthetic",
"automated_verdict": "likely_synthetic",
"reviewed": false,
"score": 0.82,
"confidence": 0.91,
"completed_at": "2026-07-25T14:03:11.842Z",
"external_ref": "kyc-session-8123",
"audit_chain_head": "9f2c4b71a3e8...",
"audit_chain_entries": 1284,
"issued_at": "2026-07-25T14:03:12.001Z"
},
"verify": "https://api.verifai.example/api/public/v1/pubkey"
}Verdict values
| Verdict | Trigger | Meaning | |
|---|---|---|---|
authentic | score < 0.35 | - | Cleared. Safe to proceed. |
suspicious | 0.35 - 0.65 | - | Review recommended before high-value action. |
likely_synthetic | score ≥ 0.65 | - | Mandatory analyst review before acting. |
inconclusive | confidence < 0.35 | - | Too little signal (very short clip, extreme compression, no face). Resubmit with better frames. |
Signal glossary
| Signal | What it measures |
|---|---|
| frame_artifacts | Per-frame GAN/diffusion artifact classifier. |
| temporal_consistency | Frame-to-frame identity, lighting, and structure drift. |
| lip_sync | Audio-visual phoneme alignment (when audio available). |
| audio_synthesis | Prosody and spectral fingerprints of synthetic voice. |
| provenance | C2PA manifest presence, integrity, and issuer trust. |
| compression_forensics | Double-compression and re-encode markers. |
| cardiac_liveness | rPPG - skin-chroma oscillations in the 0.7-3 Hz cardiac band. |
| physics_consistency | Catchlight geometry, shadow direction agreement, saccade micro-timing. |
| known_synthetic | Perceptual-hash match against the cross-org threat-intel registry. |
Errors
All errors are JSON with an error string and, where useful, a details field.
| Status | When |
|---|---|
| 400 | Body failed schema validation. details contains the field path. |
| 401 | Missing, malformed, or revoked API key. |
| 404 | Scan id not found or not owned by your org. |
| 409 | Scan has no verdict yet (receipt endpoint only). |
| 429 | Per-minute or per-month limit exceeded. See Rate limits. |
| 500 | Detection backend failure. scan_id is returned when available so you can retry the same case. |
HTTP/1.1 401 Unauthorized
{ "error": "Invalid or revoked API key" }HTTP/1.1 400 Bad Request
{ "error": "Invalid body", "details": "frames: Array must contain at most 12 element(s)" }Rate limits
Each API key is capped at 30 requests per minute and 10,000 requests per month. Every response includes:
| Header | Meaning |
|---|---|
| x-ratelimit-limit-minute | Per-minute cap (30). |
| x-ratelimit-remaining-minute | Requests left in the current UTC minute. |
| x-ratelimit-limit-month | Per-month cap (10000). |
| x-ratelimit-remaining-month | Requests left in the current UTC month. |
| retry-after | Seconds to wait before retrying (429 only). |
Every 429 is written to the tamper-evident audit log. Contact us for higher limits or streaming ingestion.
HTTP/1.1 429 Too Many Requests
retry-after: 42
x-ratelimit-remaining-minute: 0
{ "error": "Rate limit exceeded (per minute)", "retry_after_seconds": 42 }Evidence report (PDF)
Every scan can be downloaded as a signed PDF from the analyst console (PDF report on the scan detail page). The PDF includes the verdict, per-signal scores and confidences, frame timestamps, analyst notes, override history, and the current head hash of your organization's audit chain - so the report itself is verifiable against the append-only log.