API reference · v1

Integrate Veriface

Capture an ID document and a selfie in your own flow, send them from your server to /api/v1, and receive a decision. Results also arrive by webhook and appear in your dashboard.

Quickstart

Send the images as multipart form data. Each image can be JPEG, PNG, WebP or HEIC, up to 8 MB.

// Server-side only. Create a key in Dashboard → API keys.
const form = new FormData()
form.set("documentType", "drivers_license")
form.set("front", frontFile)
form.set("back", backFile)              // omit for "passport"
form.set("selfie", selfieFile)
form.set("userReference", "usr_8f31")   // optional: your own user id

const res = await fetch("https://veriface.hlhunt.org/api/v1/verifications", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.VERIFACE_API_KEY}` },
  body: form,
})
if (!res.ok) throw new Error((await res.json()).error.message)
const verification = await res.json()     // 201 Created

if (verification.status === "verified") unlockAccount()
else if (verification.status === "needs_review") queueForReview(verification.id)
else showRejection(verification.result.reasons)
curl https://veriface.hlhunt.org/api/v1/verifications \
  -H "Authorization: Bearer idv_live_..." \
  -F documentType=passport \
  -F front=@passport.jpg \
  -F selfie=@selfie.jpg \
  -F userReference=usr_8f31

Authentication & errors

  • Every v1 request needs Authorization: Bearer <key>. Keys are created and revoked in Dashboard → API keys; the secret is shown once. Never send keys from a browser or mobile app.
  • idv_live_ and idv_test_ keys behave the same and both count toward usage; use test keys to keep development traffic separate in the dashboard.
  • Each key may make 600 requests per minute. Every response carries RateLimit-* headers; a 429 includes Retry-After.
  • Errors share one shape. Codes: invalid_request (400), unauthorized (401), plan_limit_reached (402), account_suspended (403), not_found (404), document_not_accepted (422), rate_limited (429), internal_error (500), service_unavailable (503).
HTTP/1.1 402 Payment Required
Veriface-Request-Id: req_Yx2...           // quote this when contacting support
RateLimit-Limit: 600
RateLimit-Remaining: 597
RateLimit-Reset: 41

{
  "error": {
    "code": "plan_limit_reached",
    "message": "The Free plan includes 100 verifications per month. Upgrade to continue.",
    "request_id": "req_Yx2..."
  }
}

POST /api/v1/verifications

FieldTypeNotes
documentTypestringdrivers_license (Driver's license), state_id (State ID card), passport (Passport), passport_card (Passport card), residence_permit (Residence permit)
frontFileRequired. Front of the card or passport data page.
backFileRequired for every type except passport.
selfieFileRequired. Live, front-facing capture.
userReferencestringOptional, up to 120 characters. Your user id; filterable later.
sourcestringOptional. "sdk" when the capture came from your mobile SDK, otherwise "api".

Returns 201 with the verification object. Analysis typically completes in 5–15 seconds; set your HTTP client timeout to at least 60 seconds. Images are processed in memory and never stored.

Verification object

{
  "id": "ver_7c0e3d41a9f24b0e8d11",
  "orgId": "org_x1y2z3",
  "status": "verified",              // "verified" | "needs_review" | "rejected"
  "documentType": "drivers_license",
  "source": "api",                   // "api" | "sdk" | "link" | "live"
  "userReference": "usr_8f31",       // who it was for: your user id / email (set it on every request)
  "initiatedBy": "api_key:key_9x2...", // team member email, api_key:<id>, or the link creator
  "durationMs": 8421,
  "createdAt": "2026-10-14T15:02:11.000Z",
  "reviewedAt": null,               // set when someone reviews it manually
  "result": {
    "id": "ver_7c0e3d41a9f24b0e8d11",
    "status": "verified",
    "documentType": "drivers_license",
    "checks": {
      "documentAuthentic": true, "documentReadable": true, "documentTypeMatches": true,
      "documentNotExpired": true, "faceMatch": true, "selfieLive": true
    },
    "scores": { "authenticity": 0.97, "faceMatch": 0.93 },
    "extracted": {
      "fullName": "Jane Sample", "dateOfBirth": "1990-04-12", "documentNumber": "S1234567",
      "expirationDate": "2030-04-12", "issuingAuthority": "California"
    },
    "reasons": [],                  // user-facing explanations when not verified
    "completedAt": "2026-10-14T15:02:11.000Z"
  }
}

When workspace redaction is enabled, document numbers are masked in the dashboard only; the API always returns the full value to your server.

List & retrieve

Results are newest first and cursor-paginated (limit 1–100, default 20). Filters: status, document_type, user_reference.

GET /api/v1/verifications?limit=20&status=needs_review&document_type=passport
GET /api/v1/verifications?user_reference=usr_8f31
GET /api/v1/verifications?starting_after=ver_7c0e3d41a9f24b0e8d11   // next page
GET /api/v1/verifications/:id

{
  "object": "list",
  "data": [ { ...verification }, ... ],
  "has_more": true,
  "next_cursor": "ver_51b2..."     // pass as starting_after
}

Manual review

Record a decision for a session in needs_review (or override any outcome). This fires the verification.reviewed webhook and is written to your audit trail.

PATCH /api/v1/verifications/:id
Content-Type: application/json

{ "status": "verified" }   // or "rejected" / "needs_review"

GET /api/v1/stats

Aggregates for the last days (1–90, default 14). Values are cached for up to 10 seconds, so polling is cheap.

GET /api/v1/stats?days=30

{
  "total": 1284, "verified": 1102, "needsReview": 97, "rejected": 85, "passRate": 0.858,
  "avgDurationMs": 8710,
  "durations": { "p50": 8100, "p95": 13900, "buckets": [ ... ] },
  "byDocumentType": [ { "type": "drivers_license", "count": 811 }, ... ],
  "daily": [ { "date": "2026-10-01", "verified": 41, "needsReview": 3, "rejected": 2 }, ... ],
  "topReasons": [ { "reason": "...", "count": 31 } ]
}

Webhooks

Configure one endpoint per workspace in Dashboard → Webhooks and choose which events to receive:

  • verification.completed — Fires once a session reaches a final status.
  • verification.needs_review — Fires when a session lands in the manual review queue.
  • verification.reviewed — Fires when a reviewer approves or rejects a session.
  • api_key.created — Fires when a new API key is issued.

Deliveries are signed with your endpoint secret (HMAC-SHA256 over ${timestamp}.${body}) and time out after 10 seconds. Respond with any 2xx status. Every attempt and its response code is listed in the dashboard.

POST https://your-server.example.com/webhooks/veriface
Veriface-Signature: t=1760540531,v1=5f2b...

{ "id": "evt_...", "type": "verification.completed", "created": "2026-10-15T14:22:11Z", "data": { ...verification } }

// Verify (Node.js) — use the raw request body
const [t, v1] = header.split(",").map((p) => p.split("=")[1])
const expected = crypto.createHmac("sha256", process.env.VERIFACE_WEBHOOK_SECRET)
  .update(`${t}.${rawBody}`).digest("hex")
const valid = crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
  && Math.abs(Date.now() / 1000 - Number(t)) < 300   // reject replays older than 5 min

Decision logic

Defaults are shown below. Each workspace can change the thresholds and accepted document types in Dashboard → Settings, and they apply to the next verification.

  • rejected when the document is not government issued, shows tampering or is a photo of a screen, is expired, the selfie fails liveness, or the face match score is below 0.55.
  • needs_review when no hard failure applies but at least one check doesn't pass, for example a face match between 0.55 and 0.75, unreadable text, a document that doesn't match the selected type, or authenticity below 0.70.
  • verified when every check passes: face match at least 0.75 and document authenticity at least 0.70.

Dashboard capture

Team members can also start a verification from the browser (Dashboard → Verifications → New verification). On a computer it shows a QR code to continue on a phone and updates live as the phone progresses; the result is stored with source: "link". "Use this device's camera instead" (and any phone or tablet) runs the capture in the same browser, recorded with source: "live".