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_8f31Authentication & 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_andidv_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; a429includesRetry-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
| Field | Type | Notes |
|---|---|---|
| documentType | string | drivers_license (Driver's license), state_id (State ID card), passport (Passport), passport_card (Passport card), residence_permit (Residence permit) |
| front | File | Required. Front of the card or passport data page. |
| back | File | Required for every type except passport. |
| selfie | File | Required. Live, front-facing capture. |
| userReference | string | Optional, up to 120 characters. Your user id; filterable later. |
| source | string | Optional. "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.
Phone capture links (QR)
Computers rarely have a camera good enough to photograph an ID. A verification link lets the person finish on their phone: create a link from your server, show its QR code on your desktop page, and Veriface hosts the capture flow (document picker, camera guidance, selfie) at /v/… on the phone. The link is single use, expires after 30 minutes and only works with your workspace's accepted documents and thresholds.
The resulting verification is stored with source: "link", counts toward your plan like any other and fires the usual verification.* webhooks. The url and qrCode are only returned when the link is created; keep them server-side until you show them to the user.
// 1. Your server creates a single-use link for the user
const res = await fetch("https://veriface.hlhunt.org/api/v1/verification-links", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.VERIFACE_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ userReference: "usr_8f31" }), // optional
})
const link = await res.json() // 201 Created
{
"object": "verification_link",
"id": "cap_3k9d0x2q8w1m5n7b4v6c",
"status": "pending", // pending → opened → processing → completed | failed | expired
"userReference": "usr_8f31",
"expiresAt": "2026-10-15T15:32:11.000Z", // 30 minutes, single use
"openedAt": null,
"completedAt": null,
"error": null,
"url": "https://veriface.hlhunt.org/v/Xk2...",
"qrCode": "data:image/svg+xml;base64,PHN2Zy...", // render with <img src={link.qrCode}>
"verificationId": null,
"verification": null
}
// 2. Show link.qrCode on desktop (or send link.url by SMS / email on mobile)
// 3. Poll for the outcome, or listen for the verification.* webhooks
GET /api/v1/verification-links/cap_3k9d0x2q8w1m5n7b4v6c
→ { ..., "status": "completed", "verificationId": "ver_...", "verification": { ...verification } }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 minDecision 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".