VeriSec

Identity, documents and payments, in one API.

Face, documents, MRZ, NFC, AML, cheques and cards. One key, 69 endpoints, seven official SDKs.

Quickstart

First call in 3 steps

Time to first response ≈ 2 minutes
1

Create a project

Sign up and name your project. Every new project starts with free credits, no card and no approval.

2

Copy your test key

Portal → API Keys. Test keys start with sk_test_ and are sent straight as a bearer token.

3

Make your first request

Run the snippet below. It reads a passport's machine-readable zone and checks every check digit.

read a passport MRZ
curl -X POST https://verisec.mmose.me/v1/mrz/parse \
  -H "Authorization: Bearer sk_test_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "lines": [
      "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<",
      "L898902C36UTO7408122F1204159ZE184226B<<<<<10"
    ]
  }'
Products as APIs

Nine products, one key.

Code examples

Verify a face, in your language.

curl -X POST https://verisec.mmose.me/v1/face/verify \
  -H "Authorization: Bearer $VERISEC_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"image1\": \"$(base64 -w0 selfie.jpg)\",
    \"image2\": \"$(base64 -w0 id-photo.jpg)\",
    \"threshold\": 0.8
  }"
response
{
  "ok": true,
  "request_id": "req_cd1d47ea5bf34b7a",
  "latency_ms": 2362,
  "data": {
    "similarity": 0.9984,
    "score": 0.2399,
    "match": true,
    "threshold": 0.75,
    "compared": [
      { "image": "image1", "roi": [182, 241, 173, 220] },
      { "image": "image2", "roi": [940, 245, 176, 216] }
    ]
  }
}
SDKs & libraries

Official clients, generated from the same contract.

Server SDKs

One per language, version 1.0.0, generated from the API spec: typed requests and answers, images from files, safe retries that never charge a call twice, and webhook verification.

Python

Live · PyPI
pip install verisec
from verisec import VeriSec
client = VeriSec()
client.face.verify(image1="a.jpg", image2="b.jpg")

Node.js

Live · npm
npm install @veriseclabs/node
import { VeriSec } from "@veriseclabs/node";
const client = new VeriSec();
await client.bin.lookup("424242");

Go

Live · Go
GitHub
go get github.com/mosesmmoisebidth/verisec-go
import "github.com/mosesmmoisebidth/verisec-go"
client, _ := verisec.New(key)
client.Bin.Lookup(ctx, "424242")

Java

Live · Maven Central
implementation("io.github.mosesmmoisebidth:verisec-java:1.0.0")
import dev.verisec.VeriSec;
VeriSec client = VeriSec.builder().build();
client.bin().lookup("424242");

.NET

Live · NuGet
GitHub
dotnet add package VeriSec
using VeriSec;
var client = new VeriSecClient();
await client.Bin.LookupAsync("424242");

PHP

Live · Packagist
GitHub
composer require verisec/verisec-php
use VeriSec\Client;
$client = new Client($key);
$client->bin->lookup('424242');

Ruby

Live · RubyGems
gem install verisec
require "verisec"
client = VeriSec::Client.new
client.bin.lookup("424242")

Mobile capture SDKsPublishing soon

On-device capture with quality checks before an image leaves the handset. In development.

Authentication & environments

Two key types, one code path.

Test

sk_test_…

A test key is sent straight as Authorization: Bearer. It runs against the real engines at https://verisec.mmose.me, with your free credits and no live data.

Every endpoint is available. Refused calls are never charged.

Live

ak_live_…

A live key is paired with a client id and secret. Your SDK exchanges them at POST /v2/oauth/token for a short-lived bearer token and renews it for you; the long-lived key is never sent on a request.

Per-key rate limits, an optional spend cap, and an IP allow-list.

Keys are created in the console, scoped per project, and shown once. Each call is charged in credits only when it succeeds; a refused call (no credits, no permission, rate limited) is free.

Test data

Try the APIs with a test key.

A test key runs against the real engines for free. Use the Playground to try any endpoint with sample images, or send one of these card numbers to the card APIs.

Test cards

Visa
4242 4242 4242 4242Reads cleanly, Luhn valid
Mastercard
5555 5555 5555 4444Reads cleanly, Luhn valid
Amex
3782 822463 1000515-digit PAN handling
Discover
6011 1111 1111 1117Reads cleanly
Visa
4000 0056 0000 0008UK issuer, debit
Visa
4111 1111 1111 1111Classic Visa test BIN
Open the Playground

The Playground runs inside the portal and asks you to sign in first.

Webhooks

Every decision, pushed to you.

EventDescription
verification.completedA verification finished and a decision is available.
verification.needs_reviewA verification landed in the manual review queue.
verification.declinedA verification was declined by the workflow rules.
aml.hitA screening returned one or more scored hits.
credits.lowRemaining credits fell below your configured threshold.
sample payload
{
  "type": "verification.completed",
  "timestamp": "2026-10-17T14:02:15.221Z",
  "environment": "test",
  "data": {
    "verification_id": "ver_7Kq2W9xLm3Ab",
    "reference": "u_10422",
    "decision": "approved",
    "trust_seal": { "score": 94 },
    "checks": {
      "document_authenticity": "pass",
      "face_match": "pass",
      "liveness": "pass",
      "aml_screening": "clear"
    }
  }
}

Verifying the signature

Deliveries are signed as Standard Webhooks: webhook-id, webhook-timestamp and webhook-signature. Your SDK verifies them in one call; here it is by hand. Pass the raw body before parsing, and reject a stale timestamp.

verify-signature.js
import crypto from "node:crypto";

// Headers: webhook-id, webhook-timestamp, webhook-signature: "v1,<base64 HMAC>"
export function verify(rawBody, headers, secret) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");

  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${ts}.${rawBody}`)
    .digest("base64");

  const ok = headers["webhook-signature"]
    .split(" ")
    .some((part) => {
      const [, value] = part.split(",");
      return value && crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected));
    });

  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return ok && fresh;
}
Guides & tutorials

Task-based walkthroughs.

5 min

1:1 face match against an ID

Compare a live selfie to the portrait on a document, with a threshold you set.

Read guide →
6 min

Read and authenticate a document

Classify ~1,400 document types, read every field, and check for tampering.

Read guide →
5 min

Screen a name against AML lists

Run a sanctions and PEP screening, then read the scored hits.

Read guide →
5 min

Scan a bank card → issuer

Read the card, validate Luhn, and resolve the BIN to a network and issuer.

Read guide →
4 min

Receive and verify webhooks

Register an endpoint, verify the Standard Webhooks signature, and read the event.

Read guide →
API reference

Endpoints, grouped by product.

  • POST/v1/face/verifyCompare two faces (1:1) and decide on a threshold.
  • POST/v1/face/matchMatch faces across images and return every pair.
  • POST/v1/face/searchSearch a gallery (1:N) for the closest faces.
  • POST/v1/livenessPassive liveness from one image.
  • POST/v1/face/attributesAge, expression, glasses, mask and more.
  • POST/v1/age/verifyEstimate age and check a minimum.

Response shape

A KYC answer comes in an envelope { ok, request_id, latency_ms, data, warnings, error } and the SDKs return its data. A Financial answer is an object with an object type. A field with no value is left out, never null.

Status codes

  • 200The call ran. Check ok in the body: a result is in data.
  • 400A field is missing or not valid, see error.code and error.param.
  • 401Missing, malformed, revoked or expired key.
  • 402Not enough credits, or the key hit its spend cap. Nothing was charged.
  • 403The key may not call this API, or live access isn't approved.
  • 404Nothing was found.
  • 429Rate limited, retry after the Retry-After header. Not charged.
  • 5xxOur side. The call did not complete and was not charged.
standard error format
{
  "ok": false,
  "request_id": "req_9Xb2LpQ4",
  "error": {
    "code": "document_image_unreadable",
    "message": "The front image could not be read. Send a JPEG or PNG under 12 MB.",
    "param": "image_front"
  }
}
Changelog & versioning

What shipped.

The API is versioned in its path: KYC under /v1, Financial under /v2. New fields are added without breaking the ones already there, and a field is never removed from a version, so your integration keeps working. There is no version header to set.

  1. v1.017 Oct 2026

    First public release: 69 endpoints across Face, Documents, MRZ, NFC, AML, Contact & risk, Cheques, Cards and BIN, with seven official server SDKs (Python, Node, Go, Java, .NET, PHP, Ruby) generated from the same contract.