Search by

verisec / verisec-php

mosesmucyo

The official PHP library for the VeriSec API: identity (KYC) and Financial checks.

1.0.0 2026-10-18 00:29 UTC

This package is auto-updated.

Last update: 2026-10-18 00:36:52 UTC


README

The official PHP library for the VeriSec API: identity (KYC) and Financial checks, with images from files, safe retries and webhook verification.

composer require verisec/verisec-php

PHP 8.1 or later, with the curl and json extensions. No other runtime dependencies. Version 1.0.0.

The package is not on Packagist yet. Until it is, point Composer at a copy of this folder:

{
    "repositories": [{ "type": "path", "url": "../vsec_sdks/php" }],
    "require": { "verisec/verisec-php": "^1.0" }
}

Configure

use VeriSec\Client;

$client = new Client(getenv('VERISEC_API_KEY')); // sk_test_… or ak_live_…

Create keys in the console under API Keys. A test key (sk_test_…) is used as it is. A live key (ak_live_…) also needs the client id and secret issued with it; the library exchanges them at POST /v2/oauth/token for a short-lived token, reuses it for every call, renews it 60 seconds before it expires, and fetches a new one once if a call answers 401:

$client = new Client(getenv('VERISEC_API_KEY'), [   // ak_live_…
    'client_id' => getenv('VERISEC_CLIENT_ID'),
    'client_secret' => getenv('VERISEC_CLIENT_SECRET'),
]);
Option Default
client_id, client_secret VERISEC_CLIENT_ID, VERISEC_CLIENT_SECRET Live keys only. A live key without them is refused at construction.
base_url VERISEC_BASE_URL, then https://verisec.mmose.me
timeout 300 Seconds per attempt. A deep cheque read can take about 250 s, and a call cut off by the client may still finish, and be charged, on the server.
max_retries 2 Retries after the first attempt, only where nothing was charged (see Retries).
headers [] Extra headers for every request.
http_client VeriSec\HttpClient\CurlClient Any VeriSec\HttpClient\HttpClientInterface. new CurlClient([CURLOPT_PROXY => '…']) takes extra curl options.

With no key argument the client reads VERISEC_API_KEY. A missing key or an option it does not know throws VeriSec\Exception\VeriSecException at once.

KYC: pass images as files

Every API is a method, grouped the way the API groups them: $client->face, $client->documents, $client->mrz, $client->screening, $client->phone->otp, … (the full list, with prices, is in ../spec/README.md). The method names come from the API's operation ids: face.verify is $client->face->verify(), age.verifyLive is $client->age->verifyLive(), phone.otp.send is $client->phone->otp->send().

Methods take the API's fields as an array, spelled exactly as the API spells them (it uses both detectAll and image_front; nothing is renamed). An image field takes a file path, VeriSec\Image::fromBytes($bytes), an open stream, a data: URI, or base64 you already have; the library sends plain base64, the way the KYC APIs take it.

use VeriSec\Image;

$result = $client->face->verify([
    'image1' => 'selfie.jpg',                                  // a path
    'image2' => Image::fromBytes(file_get_contents('id.jpg')), // bytes
    'threshold' => 0.8,
]);
var_dump($result->match, $result->similarity);

$doc = $client->documents->read(['image_front' => 'id-front.jpg', 'image_back' => fopen('id-back.jpg', 'rb')]);

// lists take the images themselves; the index and ids the API expects are filled in for you
$match = $client->face->match(['images' => ['selfie.jpg', 'passport.jpg']]);
$found = $client->face->search(['probe' => 'selfie.jpg', 'gallery' => ['alice.jpg', ['image' => 'bob.jpg', 'id' => 'bob']]]);

$screening = $client->screening->sanctions(['name' => 'Jane Example']);

A string that is not an existing file, a data: URI or base64 is refused before anything is sent, with the field's name in the message (image2: not a file that exists, a data: URI or base64 …). Your array is never changed.

A KYC call returns the answer's data. The rest of the response is on getLastResponse():

$info = $result->getLastResponse();
$info->requestId;  // req_… (quote it to support)
$info->warnings;   // notes about the result that are not errors
$info->latencyMs;  // how long the check took on the server
$info->status;     // 200
$info->headers;    // response headers, names in lower case

Results

Results are objects you can also read as arrays. Their fields are the API's own:

$result->similarity;                // as a property
$result['similarity'];              // as an array key
$result->get('similarity', 0.0);    // with a default
$result->metadata->ctx->userIp;     // nested objects are objects (VeriSec\Model\FaceVerifyMetadata, …)
$result->toArray();                 // a plain array copy
json_encode($result);               // the JSON the API sent

Each answer has a class under VeriSec\Model (FaceVerify, DocumentFields, CardCheck, BinRecord, …) whose fields are documented for your IDE. A field the API adds later is still readable, and a field it did not send reads as null.

The APIs that return their engine's full result (Face Analyze, Face Match, Face Age, Face Quality, Liveness, Document Read, Proof of Address, MRZ Read, IP screening, Sanctions screening, Contact dossier) answer an open result: a VeriSec\OpenObject holding the whole map as sent, with no fixed fields. Read it the same way:

$read = $client->documents->read(['image_front' => 'id-front.jpg']);
foreach ($read['data']['ContainerList']['List'] ?? [] as $container) { /* $container['result_type'], … */ }
$read->toArray();            // everything, as plain arrays
$read->getLastResponse();    // like every result

Financial: cheques, cards, BIN, banks

The Financial reads upload the image as multipart/form-data, exactly as given (never resized), and are filed under "SDK" in the console's scan logs. The image can be a path, an open stream, or a VeriSec\FileInput:

use VeriSec\FileInput;

$cheque = $client->cheques->read(['image' => 'cheque.jpg']);
$line = $client->cheques->micrLine(['image' => FileInput::fromBytes($bytes, 'cheque.jpg'), 'format' => 'e13b']);
$card = $client->cards->read(['image' => fopen('card.jpg', 'rb')]);

$check = $client->cards->check(['number' => '4242 4242 4242 4242', 'expiry' => '09/29']);
$bin = $client->bin->lookup('424242');
$bank = $client->banks->lookup(['name' => 'Harbor Point Bank', 'country' => 'US']);

A Financial call returns the answer object as it is (most have an object type such as cheque_read or card_check), with the same getLastResponse().

Per-request options

The last argument of every method:

$client->cheques->read(['image' => 'cheque.jpg'], ['timeout' => 280, 'max_retries' => 0, 'headers' => ['X-Trace' => 'abc']]);

Credits

Each call costs its API's price in credits, charged only when it succeeds. Refused calls (no credits, no permission, rate limited) and calls that could not run are free. Each method's doc comment shows its price.

Errors

use VeriSec\Exception\ApiConnectionException;
use VeriSec\Exception\ApiException;
use VeriSec\Exception\InsufficientCreditsException;
use VeriSec\Exception\InvalidRequestException;
use VeriSec\Exception\RateLimitException;

try {
    $client->face->verify(['image1' => 'selfie.jpg', 'image2' => 'id-photo.jpg']);
} catch (InsufficientCreditsException $e) {
    echo 'Top up credits in the console.';           // 402: nothing was charged
} catch (InvalidRequestException $e) {
    echo $e->getErrorCode(), ' ', $e->getParam(), ' ', $e->getMessage();
} catch (RateLimitException $e) {
    echo 'Wait ', $e->getRetryAfter(), ' s.';
} catch (ApiException $e) {
    echo $e->getHttpStatus(), ' ', $e->getErrorCode(), ' ', $e->getRequestId();
} catch (ApiConnectionException $e) {
    // no answer: $e->wasNeverSent() is true when the request never reached the server (nothing ran)
}
Class (VeriSec\Exception\…) When
InvalidRequestException 400, 413, 415, 422 (and other 4xx): a field is missing, unknown or not valid; the file is too large or not an image
AuthenticationException 401: no key, or the key (or a live key's client id and secret) is not valid, expired or revoked
InsufficientCreditsException 402: not enough credits, or the key reached its spend cap; nothing was charged
PermissionException 403: the key may not call this API
NotFoundException 404
RateLimitException 429, with getRetryAfter()
ApiServerException 5xx: the check could not run; nothing was charged
ApiConnectionException no answer; ApiTimeoutException (a subclass) when the timeout passed
SignatureVerificationException a webhook delivery did not verify

All of them extend VeriSecException; the API errors extend ApiException, with getHttpStatus(), getErrorCode(), getMessage(), getParam(), getRequestId() and getHttpHeaders(). PHP's own getCode() is the HTTP status.

Retries

Calls are charged only when they succeed, so the library sends a call again only when nothing can have been charged: a rate limit (429), a server error (5xx), or a connection that never reached the server (refused, DNS failure, unreachable, connect timeout). It waits min(0.5 × 2^n, 8) seconds with ±25% jitter, or what Retry-After says (at most 60 s), up to max_retries times.

It never sends a call again after a timeout, or after a connection dropped once the request was sent: the check may already have run, and been charged. You get an ApiTimeoutException / ApiConnectionException saying so. For the same reason every attempt opens its own connection (libcurl would otherwise re-send a request silently when a reused connection closes before the answer).

Webhooks

Deliveries are signed as Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature). Pass the raw body, the headers and your endpoint's signing secret (whsec_…):

use VeriSec\Exception\SignatureVerificationException;
use VeriSec\Webhook;

try {
    $event = Webhook::constructEvent(file_get_contents('php://input'), getallheaders(), getenv('VERISEC_WEBHOOK_SECRET'));
} catch (SignatureVerificationException $e) {
    http_response_code(400);
    exit;
}
if ($event->type === 'verification.completed') {
    // $event->data, $event->environment ('test' or 'live'), $event->timestamp
}
http_response_code(200);

The headers can be an array (any case), $_SERVER, or a PSR-7 request. Timestamps more than 300 s from now are refused (the last argument changes the tolerance). Webhook::verifySignature() returns a bool, and Webhook::generateTestHeaders($body, $secret) signs a body for your own tests.

Calls this library does not wrap yet

$result = $client->request('POST', '/v1/some/new-check', ['image' => base64_encode($bytes)]);

Paths under /v1/ are read as KYC answers (the result is data), others as they are. Images must already be base64.

Development

composer install
composer generate   # after the spec changes: rewrites src/Model, src/Service and src/BaseClient.php
composer test       # unit + integration tests

composer test (or vendor/bin/phpunit) runs both suites. The integration tests start the shared test server (node ../test-server/server.mjs --port 4025) and stop it when the run ends; they call every operation of the spec. Node 18 or later must be on the PATH (or set NODE).

What is generated and what is not:

  • Generated by scripts/generate.php from ../spec/openapi.json (do not edit): src/Model/* (one class per answer schema and per nested object, plus the string enums as constants), src/Service/*Service.php (one class per resource, one method per operation, with the summary, description, price, request fields and errors) and src/BaseClient.php.
  • Written by hand: src/Client.php, src/ApiRequestor.php (auth, headers, retries, envelope, errors), src/HttpClient/*, src/Auth/*, src/Exception/*, src/Image.php, src/FileInput.php, src/Webhook.php, src/WebhookEvent.php, src/VeriSecObject.php, src/OpenObject.php, src/ResponseInfo.php, src/Service/AbstractService.php and src/Util/Encoder.php.