Developer ManualVeris Compare
Guide 3 of 4

Veris Compare

Server-side 1:1 face comparison. Send two images from your backend and get a confidence score back. Images are never stored: they are deleted on every request.

01Overview

Veris Compare runs on a dedicated server separate from the main API. It accepts two face images - a captured image and a reference - and returns a match result with a 0–100 confidence score.

Technology:

  • Proprietary face recognition engine optimised for accuracy across diverse skin tones and real-world lighting conditions.
  • Specifically tuned for the West African market — performs well in varied indoor and outdoor lighting typical of Nigerian ID verification flows.

Privacy model:

  • Images are never stored. They are deleted in the finally block of every request handler - even if the request errors.
  • No image data is written to logs at any verbosity level.
  • The Compare server has no direct database connection. It communicates only with api.verisinfra.com for license validation.
◆

Compare is included on every plan: Spark 15 and Build 500 comparisons a month (they stop at the limit), and Launch, Scale and Pro with extras billed per call up to a ceiling.

Typical integration pattern

Veris Compare is most commonly used alongside Veris Capture and Veris Scan in a document-to-selfie verification workflow:

  1. Use Veris Scan to extract the face photo from a user's ID document (faceImageBase64 in the scan result).
  2. Use Veris Capture to take a live selfie with liveness detection.
  3. Send both images to Veris Compare from your backend. A confidence score above 90 is a strong match.

02Authentication

Use your production license key as a Bearer token. There is no separate API key for Compare.

Authorization: Bearer veris_live_regular_xxxxxxxxxxxxxxxxxxxx
i

Sandbox keys work too - but are limited to 10 lifetime Compare calls. Use production keys for real workloads.

⚠

Never embed your license key in client-side code. All Compare calls must originate from your backend server.

03Endpoints

Two endpoints, both perform the same comparison. Pick the one that fits your stack.

POST /v1/compare/base64

Send images as base64-encoded strings in a JSON body. Good for backends that already have images in memory or as strings.

POST https://compare.verisinfra.com/v1/compare/base64
JSON body
{
  "captured_b64":  "<base64-encoded JPEG or PNG>",
  "reference_b64": "<base64-encoded JPEG or PNG>",
  "use_case":      "kyc_onboarding"   // optional — see use cases below
}
FieldRequiredDescription
captured_b64 Yes The live face image — typically from a Veris Capture session. Base64-encoded JPEG or PNG, max 5 MB decoded.
reference_b64 Yes The reference image to compare against — typically the face extracted from an ID document via Veris Scan (faceImageBase64). Same format and size limits.
use_case No Comparison scenario. Sets the match threshold automatically. Defaults to "default". See use cases below.
threshold No Override the match threshold directly, as a similarity percentage (for example 90). Overrides use_case if provided.

POST /v1/compare/files

Send images as multipart form data. Good for backends working with file streams or disk-based uploads.

POST https://compare.verisinfra.com/v1/compare/files
Multipart form fields
captured   <file>   JPEG or PNG, max 5 MB
reference  <file>   JPEG or PNG, max 5 MB
use_case   <string> optional — see use cases below
threshold  <float>  optional — direct threshold override

GET /v1/compare/use-cases

Returns all available use_case values with their thresholds and descriptions. No authentication required.

GET https://compare.verisinfra.com/v1/compare/use-cases

GET /v1/compare/quota

Returns your current quota usage for the active billing cycle.

GET https://compare.verisinfra.com/v1/compare/quota
Response
{
  "plan":              "regular",
  "quota_total":       10000,
  "quota_used":        258,
  "quota_remaining":   9742,
  "overage_count":     0,
  "cycle_resets_at":   "2026-04-01T00:00:00Z"
}

04Response

JSON (HTTP 200)
{
  "match":            true,
  "confidence":       91.4,
  "distance":         0.086,
  "threshold":        85,
  "use_case":         "kyc_onboarding",
  "engine":           "veris-compare-v3",
  "environment":      "production",
  "quota_remaining":  9742,
  "comment":          "Match confirmed. The same person was identified in both images...",
  "captured": {
    "face_detected":    true,
    "detection_score":  0.884,
    "pose": { "pitch": -6.0, "yaw": 5.1, "roll": -1.5 },
    "face_size_px":     480,
    "quality":          "high",
    "quality_score":    88.6
  },
  "reference": {
    "face_detected":    true,
    "detection_score":  0.872,
    "pose": { "pitch": -12.8, "yaw": 4.5, "roll": 2.3 },
    "face_size_px":     143,
    "quality":          "medium",
    "quality_score":    55.9
  },
  "flags":             ["medium_reference_quality"],
  "recommendation":   "manual_review"
}
FieldTypeDescription
matchbooleantrue when the faces are the same person. Determined by confidence ≥ threshold.
confidencenumber0–100 similarity score. Higher = more similar. match is true when this is at or above threshold.
distancenumberLegacy field kept for backward compatibility, equal to 1 − confidence/100. Use confidence instead.
thresholdnumberThe similarity percentage (0–100) used as the accept/reject boundary for this request, derived from use_case or your override.
use_casestringThe comparison scenario used. Echoed back so you can verify which preset was applied.
commentstringPlain-English explanation of the result — what the score means and likely causes for low confidence.
capturedobjectFace metadata for the captured image. See FaceInfo below.
referenceobjectFace metadata for the reference image. See FaceInfo below.
flagsstring[]Array of detected conditions. See Flags below.
recommendationstring"accept", "manual_review", or "reject". See Recommendation below.
enginestringEngine version. Included for debugging.
environmentstring"production" or "sandbox".
quota_remainingnumberMonthly compare calls remaining after this request.

FaceInfo object

Returned for both captured and reference. Returned with every comparison at no extra cost or call.

FieldTypeDescription
face_detectedbooleanWhether a face was found. Always true in a 200 response — if no face is detected the API returns 422.
detection_scorenumberFace detection confidence (0–1). Below 0.70 is unreliable.
pose.pitchnumberUp/down tilt in degrees. Positive = looking up.
pose.yawnumberLeft/right rotation in degrees. Above ±25° degrades accuracy.
pose.rollnumberSide tilt in degrees.
face_size_pxnumberLargest face dimension in pixels. Below 80 px is low quality; above 200 px is comfortable for recognition.
qualitystring"high", "medium", or "low". Derived from face size, detection confidence, and image sharpness.
quality_scorenumber0–100 composite quality score.

Flags

FlagMeaning
low_reference_qualityReference image quality score is low. Common with heavily compressed government ID photos.
medium_reference_qualityReference image quality is usable but limited — typically a small ID photo (140–200 px).
low_captured_qualityCaptured image quality is low. Check lighting and camera resolution.
small_reference_faceFace in the reference image is below 80 px — below reliable recognition threshold.
head_pose_deviationOne or both faces have yaw > 25°. Non-frontal poses reduce accuracy.
possible_minor_detectedEstimated age is under 18 in one or both images (only when age estimation is enabled for your account). Route to manual review.

Recommendation

ValueMeaningSuggested action
"accept"Match with no quality or pose concerns.Proceed automatically.
"manual_review"Match, but one or more flags present — head-pose deviation, possible minor, or a confidence below the accept level for the use case.Route to a human agent or require the user to retake the capture.
"reject"No match.Deny the action. Optionally prompt the user to retry with better lighting.

05Use cases

Pass use_case in every request. It sets the match threshold automatically for your scenario. Choosing the wrong preset is the most common cause of unexpected false rejections or false acceptances.

use_caseThreshold (similarity %)When to use
"kyc_onboarding" 85 Live capture vs a government-issued ID photo — BVN, NIN, Driver's Licence, Passport. Loosest threshold because you have no control over the reference image: it may be 5–15 years old, low resolution (140–300 px), have text watermarks over the face, and was taken under formal ID conditions very different from a mobile selfie.
"liveness_check" 90 Live capture vs the face saved from the user's completed KYC session. Use this for step-up authentication — password reset, large transfer confirmation, high-risk action approval. Both images are mobile selfies under similar conditions, so a stricter threshold is appropriate.
"user_photo" 88 Live capture vs a photo the user uploaded themselves — a profile picture, social media photo, etc. Better quality than a scanned government ID, but you don't control when or how it was taken.
"default" 88 No specific scenario. Use a specific preset wherever possible — "default" is a safe fallback, not a recommended choice.

You can also override the threshold directly with "threshold": 90 (a similarity percentage) if you need custom calibration beyond the presets. Call GET /v1/compare/use-cases to retrieve the current preset values programmatically. Preset values may be tuned over time; the endpoint is the source of truth.

06Error codes

All error responses follow the same shape:

JSON
{
  "error":   "FACE_NOT_DETECTED",
  "message": "No face detected in the captured image."
}
HTTP Error code Meaning Fix
401 INVALID_LICENSE License key not found or malformed. Check your Authorization header. Use the full license key starting with veris_.
402 SUBSCRIPTION_INACTIVE Plan expired or payment failed. Renew subscription in the dashboard. SDK returns SubscriptionInactive in the 48-hour grace period.
403 COMPARE_NOT_IN_PLAN The plan does not include Compare. Upgrade to Launch or higher in the dashboard.
422 FACE_NOT_DETECTED No face found in one or both images. Ensure both images contain a clear, front-facing face with adequate lighting.
422 QUOTA_EXCEEDED Monthly quota and overage cap both reached, and the wallet could not pay. The response includes wallet_reason: WALLET_OFF, WALLET_EMPTY, WALLET_TOPPING_UP, WALLET_LIMIT_REACHED or WALLET_UNAVAILABLE. Add credit to your wallet or turn it on in the dashboard, wait for the quota reset at your billing cycle start, or upgrade to a higher plan.
422 SANDBOX_COMPARE_LIMIT_REACHED Used all 10 lifetime sandbox Compare calls. Activate a paid subscription to continue using Compare.
422 IMAGE_TOO_LARGE One or both images exceed the 5 MB limit. Compress or resize the image before sending.
422 INVALID_IMAGE_FORMAT Image is not a valid JPEG or PNG. Convert to JPEG or PNG and retry.
429 RATE_LIMITED Per-minute request limit exceeded. Slow down. See rate limits below.
503 COMPARE_DISABLED Service temporarily paused by Veris admin. Retry after a few minutes. No action required on your end.
503 COMPARE_ENGINE_BUSY The comparison engine is handling a burst of requests. Quota is not charged. Retry after the number of seconds in the Retry-After header.
503 COMPARE_ENGINE_UNAVAILABLE The comparison engine could not be reached. Quota is not charged. Retry with backoff. If it persists, contact support.

07Quotas

Plan Monthly quota Overage rate Overage cap
Spark 15 / month — (stops at the limit) —
Build 500 / month — (stops at the limit) —
Launch 1,500 / month $0.03 / call $30 / month
Scale 10,000 / month $0.02 / call $50 / month
Pro 30,000 / month $0.01 / call $80 / month

Quota reset: Resets on the first day of your billing cycle - not on the 1st of the calendar month. Check your dashboard for the exact reset date.

Overage cap: Once you exceed both the included quota and the overage cap, the Compare API returns QUOTA_EXCEEDED until the next cycle. The cap is a hard ceiling: calls stop at it and you are never billed past it. Spark and Build stop at their included amount. Your base subscription is not suspended; only Compare calls are blocked.

Wallet (optional prepaid credit): Turn the wallet on in your dashboard and Compare keeps working after your included comparisons, and any overage up to the cap, are used up. Each further comparison is paid from your credit at one flat price per comparison, shown in your dashboard. The wallet is off by default, only comparisons that succeed are charged, and you can set a spending limit for each billing cycle. Your balance stays in your wallet and carries over from month to month until you use it. A comparison paid from the wallet does not count towards your plan quota.

Automatic top-up: When you turn the wallet on, automatic top-up is on too. If a card is saved on your subscription, Veris adds credit to your wallet through Paystack when the balance runs low; you choose the amount and when it happens, and you can switch it off at any time. If the wallet runs out before the top-up lands, the call returns QUOTA_EXCEEDED with wallet_reason WALLET_TOPPING_UP; retry in a few seconds. GET /v1/compare/quota includes can_consume (whether the next comparison will be served) and a wallet object with your balance, the comparisons left and topping_up.

Overage billing: Calculated at midnight on the last day of your billing cycle and charged as a one-time payment. If the charge fails, you have a 72-hour window to resolve it. After that, Compare calls are throttled to zero until payment succeeds.

◆

The quota_remaining field in every Compare response shows your current remaining quota. Subscribe to the veris.compare.quota_80_percent webhook to receive a notification when you hit 80% usage.

08Rate limits

Plan Requests per minute Burst
Launch / Scale 10 20
Pro 30 60

Rate limits are per license key, not per account. If you run multiple apps, each has its own limit. When a limit is hit, the response is 429 RATE_LIMITED with a Retry-After header indicating when you can retry.

09Code examples

Complete working examples for the most common backend environments.

JavaScript (node-fetch + form-data)
const fs       = require('fs');
const FormData = require('form-data');
const fetch    = require('node-fetch');

async function compareFaces(capturedPath, referencePath) {
    const form = new FormData();
    form.append('captured',  fs.createReadStream(capturedPath));
    form.append('reference', fs.createReadStream(referencePath));

    const res = await fetch('https://compare.verisinfra.com/v1/compare/files', {
        method: 'POST',
        headers: {
            Authorization: 'Bearer veris_live_regular_xxxx',
            ...form.getHeaders(),
        },
        body: form,
    });

    const data = await res.json();
    if (!res.ok) throw new Error(data.message || data.error);

    console.log(`Match: ${data.match} - Confidence: ${data.confidence}%`);
    return data;
}

Base64 variant (no file system required)

JavaScript
async function compareFacesBase64(capturedBuffer, referenceBuffer) {
    const res = await fetch('https://compare.verisinfra.com/v1/compare/base64', {
        method:  'POST',
        headers: {
            'Authorization':  'Bearer veris_live_regular_xxxx',
            'Content-Type':   'application/json',
        },
        body: JSON.stringify({
            captured_b64:  capturedBuffer.toString('base64'),
            reference_b64: referenceBuffer.toString('base64'),
        }),
    });

    if (!res.ok) {
        const err = await res.json();
        throw new Error(err.error);
    }
    return res.json();
}
Python (requests)
import requests
import base64

def compare_faces_base64(captured_path: str, reference_path: str) -> dict:
    with open(captured_path, "rb") as f:
        captured_b64 = base64.b64encode(f.read()).decode()
    with open(reference_path, "rb") as f:
        reference_b64 = base64.b64encode(f.read()).decode()

    response = requests.post(
        "https://compare.verisinfra.com/v1/compare/base64",
        headers={"Authorization": "Bearer veris_live_regular_xxxx"},
        json={"captured_b64": captured_b64, "reference_b64": reference_b64},
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()

    print(f"Match: {data['match']} - Confidence: {data['confidence']:.1f}%")
    return data

def compare_faces_files(captured_path: str, reference_path: str) -> dict:
    with open(captured_path, "rb") as cap, open(reference_path, "rb") as ref:
        response = requests.post(
            "https://compare.verisinfra.com/v1/compare/files",
            headers={"Authorization": "Bearer veris_live_regular_xxxx"},
            files={
                "captured":  ("captured.jpg",  cap,  "image/jpeg"),
                "reference": ("reference.jpg", ref,  "image/jpeg"),
            },
            timeout=30,
        )
    response.raise_for_status()
    return response.json()
Shell
# Files endpoint
curl -X POST https://compare.verisinfra.com/v1/compare/files \
  -H "Authorization: Bearer veris_live_regular_xxxx" \
  -F "captured=@/path/to/face.jpg" \
  -F "reference=@/path/to/id_photo.jpg"
Shell (base64)
# Base64 endpoint
curl -X POST https://compare.verisinfra.com/v1/compare/base64 \
  -H "Authorization: Bearer veris_live_regular_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "captured_b64":  "'$(base64 -i face.jpg)'",
    "reference_b64": "'$(base64 -i id_photo.jpg)'"
  }'

# Check quota
curl https://compare.verisinfra.com/v1/compare/quota \
  -H "Authorization: Bearer veris_live_regular_xxxx"

10Image guidelines

Image quality directly affects match accuracy. Follow these guidelines for best results.

  • Format: JPEG or PNG. JPEG is preferred for photos - smaller file sizes and equal accuracy.
  • Size: Maximum 5 MB per image. For typical photos, 200 KB–2 MB is the normal range.
  • Resolution: The face should be at least 80×80 pixels after cropping. Higher is better, but diminishing returns above 400×400.
  • Lighting: Avoid harsh shadows or backlighting. The Veris Compare engine handles low-light reasonably well, but extreme underexposure will reduce accuracy.
  • Occlusion: No glasses, hats, or masks covering the face. The model needs clear sight of the full face region.
  • Angle: Front-facing is ideal. Up to ~30° yaw is tolerated. Profile shots will likely return FACE_NOT_DETECTED.
  • Document photos: The face photo extracted from ID documents via Veris Scan is pre-cropped and formatted for Compare. Pass it directly to reference_b64.

Further reading.

Guides on the Veris blog that go with this page.

Stuck, or something here does not match what you see?

Write to us and a person will answer.

Email the team