verisec / verisec-php
The official PHP library for the VeriSec API: identity (KYC) and Financial checks.
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.0 || ^13.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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.phpfrom../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) andsrc/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.phpandsrc/Util/Encoder.php.