Search by

alphabalex / identity-made-easy

alphabalex

A Laravel package for identity verification, starting with QoreID

Package info

gitlab.com/Alphabalex/identity-made-easy

Issues

pkg:composer/alphabalex/identity-made-easy

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-10-20 03:00 UTC

This package is auto-updated.

Last update: 2026-10-20 00:03:30 UTC


README

A Laravel package for identity verification, with QoreID, Mono, and IdenVault provider drivers. Providers expose named operations through optional capability interfaces; provider HTTP transport and credentials remain internal.

Documentation: View the Identity Made Easy documentation.

QoreID collection API operations use an API bearer token. Its Web/Mobile SDK uses a short-lived SDK session token. Do not interchange these tokens.

Requirements

  • PHP 8.1+
  • Laravel 8.83–13

Installation

composer require alphabalex/identity-made-easy
php artisan vendor:publish --tag=identity-made-easy-config

Composer's Laravel package discovery registers the service provider and Identity facade. Add QoreID credentials to the server application's environment:

QOREID_CLIENT_ID=your_qoreid_client_id
QOREID_SECRET=your_qoreid_secret
QOREID_BASE_URL=https://api.qoreid.com
QOREID_HTTP_TIMEOUT=30

Keep both credentials on your backend. Never ship QOREID_SECRET, the API access token, or a minted SDK session token as a public build-time value. The base URL defaults to QoreID's production API and can be changed for an approved test environment.

To opt into Mono instead of QoreID, add its secret key to the server environment and select the provider explicitly (or set IDENTITY_MADE_EASY_PROVIDER=mono):

MONO_SECRET_KEY=your_mono_secret_key
MONO_WEBHOOK_SECRET=your_mono_webhook_secret
MONO_BASE_URL=https://api.withmono.com
MONO_HTTP_TIMEOUT=30
$mono = Identity::driver('mono');

Mono's mono-sec-key is sent only from the backend. MONO_WEBHOOK_SECRET is separate from MONO_SECRET_KEY: it authenticates inbound webhook requests, while the latter authenticates outbound API calls. Mono's test and live secret keys select the corresponding environment where supported; Watchlist Screening is documented as live-only. The package defaults Mono calls to https://api.withmono.com and does not make live calls automatically.

Protecting application-owned Mono webhook routes

The package provides an opt-in middleware for your Prove and Watchlist webhook endpoint. Attach it to an application-owned POST route; this package does not register routes or process/acknowledge webhook events:

use Alphabalex\IdentityMadeEasy\Http\Middleware\VerifyMonoWebhookSecret;
use Illuminate\Support\Facades\Route;

Route::post('/webhooks/mono', [MonoWebhookController::class, 'handle'])
    ->middleware(VerifyMonoWebhookSecret::class);

Configure MONO_WEBHOOK_SECRET on the server using the webhook secret from your Mono dashboard. The middleware checks Mono's documented mono-webhook-secret header against this value and returns HTTP 401 when it is missing or invalid. This is a shared-secret header comparison, not an HMAC/body signature. On success, it passes the request unchanged to your handler. Your application owns event validation, processing, and returning HTTP 200 after receipt. See Mono's Prove and Watchlist event references. Mono's docs do not clearly establish whether Prove and Watchlist share the same webhook secret/configuration; confirm the dashboard setup for your account before using one secret for both.

A controller can validate the event envelope and enqueue only events for the product families served by this route. This example also handles the nested envelope shown for Prove's data_verification_failed event:

use App\Jobs\ProcessMonoWebhook;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

final class MonoWebhookController
{
    public function __invoke(Request $request): JsonResponse
    {
        $payload = $request->json()->all();

        // Most events use {"event":"...", "data":{...}}. The Prove failed-event
        // examples also show {"data":{"event":"...", "data":{...}}}.
        if (array_key_exists('event', $payload)) {
            $event = $payload['event'];
            $data = $payload['data'] ?? null;
        } else {
            $event = data_get($payload, 'data.event');
            $data = data_get($payload, 'data.data');
        }

        if (!is_string($event) || !is_array($data)) {
            return response()->json(['message' => 'Invalid webhook payload.'], 400);
        }

        // Ignore unrelated events on this route; acknowledge them so Mono does not retry them.
        if (!str_starts_with($event, 'mono.prove.')
            && !str_starts_with($event, 'mono.events.watchlist.')) {
            return response()->json(['received' => true], 200);
        }

        // Configure a durable queue and make this job safe to run more than once.
        ProcessMonoWebhook::dispatch($event, $data);

        // Acknowledge only after the job has been accepted for dispatch.
        return response()->json(['received' => true], 200);
    }
}

If validation or job dispatch fails, do not return 200; Mono treats non-200 responses as unsuccessful and retries. Do not log webhook bodies or sensitive identity data. Make processing idempotent: Mono may redeliver events, and the Lookup event examples do not consistently document a stable event ID, so exactly-once delivery or a universal deduplication key cannot be assumed.

Provider drivers and capabilities

qoreid is the default driver. Select it explicitly with Identity::driver('qoreid'). The package uses Laravel's manager pattern, so applications can register another provider from their own service provider with Identity::extend():

use Alphabalex\IdentityMadeEasy\Contracts\IdentityDriverInterface;
use Alphabalex\IdentityMadeEasy\Contracts\IdentityLookupDriverInterface;
use Alphabalex\IdentityMadeEasy\Facades\Identity;
use App\Identity\ExampleIdentityDriver;
use Illuminate\Support\ServiceProvider;

class IdentityProvidersServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Identity::extend('example', function ($app): IdentityDriverInterface {
            $config = $app->make('config')->get('identity-made-easy.providers.example', []);

            return new ExampleIdentityDriver($config);
        });
    }
}

The shared IdentityDriverInterface is a marker, not a generic HTTP client. Implement the optional capability interfaces your provider supports, such as IdentityLookupDriverInterface or IdentitySessionDriverInterface. Drivers are resolved and cached by provider name. Provider credentials are configured in config/identity-made-easy.php under identity-made-easy.providers.{provider}.

Mono capabilities

Mono exposes separate capability interfaces because its Lookup and Prove contracts differ from QoreID operations. Check support before calling a provider-specific family:

use Alphabalex\IdentityMadeEasy\Contracts\MonoIdentityLookupDriverInterface;
use Alphabalex\IdentityMadeEasy\Contracts\MonoProveDriverInterface;
use Alphabalex\IdentityMadeEasy\Facades\Identity;

$mono = Identity::driver('mono');

if ($mono instanceof MonoIdentityLookupDriverInterface) {
    $result = $mono->lookupNin(['nin' => '12345678901']);
}

if ($mono instanceof MonoProveDriverInterface) {
    $session = $mono->initiateProve([
        'reference' => 'customer-123',
        'redirect_url' => 'https://example.test/verify/complete',
        'kyc_level' => 'custom',
        'identities' => ['bvn'],
    ]);
    // Send the returned Mono URL to the customer through your application.
}

The capability interfaces expose these named routes:

CapabilityMethodHTTP route
Identity LookupgetBanks()GET /v3/lookup/banks
Identity LookuplookupAccountNumber($payload)POST /v3/lookup/account-number
Identity LookuplookupCreditHistory($provider, $payload)POST /v3/lookup/credit-history/{provider} (crc, xds, or all)
Identity LookupverifyDriversLicense($payload)POST /v3/lookup/driver_license
Identity LookupverifyHouseAddress($payload)POST /v3/lookup/address
Identity LookupverifyInternationalPassport($payload)POST /v3/lookup/passport
Identity LookuplookupMashup($payload)POST /v3/lookup/mashup
Identity LookuplookupNin($payload, $query = [])POST /v3/lookup/nin
Identity LookupgetNinPdfJob($jobId)GET /v3/lookup/nin/{jobId}/job
Identity LookuplookupTin($payload)POST /v3/lookup/tin
Identity LookupinitiatePhoneNin($payload)POST /v3/lookup/phone/initiate
Identity LookupverifyPhoneNin($payload)POST /v3/lookup/phone/verify
BVN iGreeinitiateBvnIgree($payload)POST /v2/lookup/bvn/initiate
BVN iGreeverifyBvnIgree($payload, $sessionId)POST /v2/lookup/bvn/verify
BVN iGreefetchBvnIgreeDetails($payload, $sessionId)POST /v2/lookup/bvn/details
CACsearchCac($query)GET /v3/lookup/cac
CACgetCacShareholders($companyId)GET /v3/lookup/cac/company/{id}
CACgetCacPsc($companyId)GET /v3/lookup/cac/company/{id}/psc
CACgetCacSecretary($companyId)GET /v3/lookup/cac/company/{id}/secretary
CACgetCacDirectors($companyId)GET /v3/lookup/cac/company/{id}/directors
CACgetCacProfile($rcNumber)GET /v3/lookup/cac/profile/{rcnumber}
CACgetCacStatusReport($companyId)GET /v3/lookup/cac/company/{id}/status-report (PDF)
WatchlistsubmitWatchlistScreening($payload)POST /v3/lookup/watchlist
WatchlistsubmitWatchlistBatch($payload)POST /v3/lookup/watchlist/batch
WatchlistgetWatchlistScreening($screeningId)GET /v3/lookup/watchlist/{id}
WatchlistgetWatchlistAuditLog($screeningId)GET /v3/lookup/watchlist/{id}/audit-log
WatchlistgetWatchlistReport($screeningId)GET /v3/lookup/watchlist/{id}/report (PDF)
WatchliststartWatchlistMonitoring($payload)POST /v3/lookup/watchlist/monitor
WatchliststopWatchlistMonitoring($monitoringId)DELETE /v3/lookup/watchlist/monitor/{id}
ProveinitiateProve($payload)POST /v1/prove/initiate
ProvegetProveCustomer($reference)GET /v1/prove/customers/{reference}
ProvegetProveCustomers($query = [])GET /v1/prove/customers
ProveblacklistProveCustomer($payload)POST /v1/prove/customers/blacklist
ProvewhitelistProveCustomer($payload)POST /v1/prove/customers/whitelist
ProverevokeProveCustomer($reference)DELETE /v1/prove/customers/{reference}

getBanks() retrieves the NIP-supported bank list. For BVN iGree, pass the session ID returned by initiation to verifyBvnIgree() and fetchBvnIgreeDetails(); these calls send it as x-session-id. The CAC status report and Watchlist report methods return Laravel Illuminate\Http\Client\Response instances so callers can preserve PDF bytes and headers. Other JSON operations return Mono's decoded response array unchanged. The Mono API contract documented in this package has unresolved details: Prove fetch/revoke references are passed through without identifier interpretation, and the Watchlist guide's sample report URL uses v2 while its dedicated reference specifies v3. Mono's CAC change-of-name and previous-address operations are excluded because they are deprecated. This is verified operation coverage, not a claim that every Mono product or undocumented route is implemented.

IdenVault identity verification

IdenVault is an opt-in server-to-server provider. Configure a sandbox key first; it uses the same routes and response shapes as live, returns fixture data, and is not billed. Set IDENTITY_MADE_EASY_PROVIDER=idenvault or resolve it explicitly with Identity::driver('idenvault'):

IDENVAULT_API_KEY=your_idenvault_api_key
IDENVAULT_BASE_URL=https://idenvault.com/api/v1
IDENVAULT_HTTP_TIMEOUT=30
IDENVAULT_WEBHOOK_SECRET=your_idenvault_webhook_secret

Every identity query returns real personal information in live mode. Your application is the data controller: obtain and retain consent or another lawful basis before calling, and send a truthful request_reason when appropriate. The package forwards your payload unchanged and does not collect consent or choose a reason for you. Keep the API key server-side; do not log keys, full identifiers, or response bodies.

The driver exposes optional capabilities. Verification methods require an idempotency key that your application generates once for each logical operation and reuses on retries:

use Alphabalex\IdentityMadeEasy\Contracts\IdenVaultIdentityVerificationDriverInterface;
use Alphabalex\IdentityMadeEasy\Facades\Identity;
use Illuminate\Support\Str;

$idenvault = Identity::driver('idenvault');
$operationIdempotencyKey = (string) Str::uuid(); // Store and reuse this for retries of this operation.
if ($idenvault instanceof IdenVaultIdentityVerificationDriverInterface) {
    $response = $idenvault->verifyNinBasic(
        ['nin' => '12345678901', 'request_reason' => 'creditBackgroundCheck'],
        $operationIdempotencyKey
    );

    $record = $response->json();
    $requestReference = $response->header('X-Request-Ref');
    $charge = $response->header('X-Charge-Amount');
    $balance = $response->header('X-Wallet-Balance');
}

All IdenVault operations return Laravel Illuminate\Http\Client\Response objects so callers can inspect the JSON/body and accounting/audit headers (X-Request-Ref, X-Request-Id, X-Charge-Amount, X-Wallet-Balance, X-Cache, X-IdenVault-Mode, X-API-Version, rate-limit headers and Retry-After). Methods are verifyNinBasic, verifyNinMatch, verifyPhone, verifyBvn, verifyBvnBasic, verifyShareCode, verifyDemography, verifyBankAccount, getBanks, getAccountBalance, and getMeta. Phone lookup may return a top-level JSON array; use $response->json() rather than assuming an object.

The API returns structured error responses for insufficient balance (402), validation failures (422), rate limiting (429), and upstream outages (502), among others. Check $response->status() and $response->json(); non-2xx responses are returned unchanged so their error bodies and headers remain available. Respect Retry-After on 429. After a timeout or a safe-to-retry 502, reuse the same Idempotency-Key; do not create a new key for the same logical verification. A live NIMC lookup that returns not-found can still be billed, while sandbox calls are never billed. Retain request references for support without storing unnecessary identity data.

IdenVault wallet webhooks

The package includes opt-in middleware for checking IdenVault's timestamped HMAC-SHA256 signature on the raw body. Attach it to an application-owned POST route; this package does not register routes or process/deduplicate events:

use Alphabalex\IdentityMadeEasy\Http\Middleware\VerifyIdenVaultWebhookSignature;
use Illuminate\Support\Facades\Route;

Route::post('/webhooks/idenvault', [IdenVaultWebhookController::class, 'handle'])
    ->middleware(VerifyIdenVaultWebhookSignature::class);

Configure IDENVAULT_WEBHOOK_SECRET separately from the API key. The middleware validates X-IdenVault-Signature: t=<unix timestamp>,v1=<hex> against HMAC-SHA256(secret, "{t}.{raw body}"), with a default 300-second timestamp tolerance. Do not parse or re-encode the body before the middleware verifies it. Your handler should deduplicate deliveries by event id and return 2xx within 10 seconds; event handling and persistence remain application responsibilities.

QoreID SDK sessions

When your frontend/mobile client should launch a QoreID workflow, mint the session on the backend using the workflow ID and an application-generated reference:

$session = Identity::driver('qoreid')->createSession([
    'type' => 'workflow',
    'workflowId' => 123,
    'reference' => 'onboarding-456',
]);

return response()->json([
    'sessionToken' => $session['sdkSessionToken'],
    'expiresAt' => $session['expiresAt'] ?? null,
]);

Workflow sessions use QoreID's documented HTTP Basic authentication with the server-side client ID and secret. Collection sessions use a product code available to your account:

$session = Identity::driver('qoreid')->createSession([
    'productCode' => 'nin',
    'reference' => 'onboarding-789',
    'subjectRef' => 'internal-user-42', // optional pseudonymous ID; do not put PII here
    'ttlSeconds' => 120,
    'maxAttempts' => 3,
]);

A session requires exactly one of productCode or positive integer workflowId, and a non-empty reference. See the official product-code list. The SDK token is short-lived and single-use; do not send it as a Bearer token for server-side API operations.

Named QoreID operation capabilities

Get the driver and call its named methods directly, or use instanceof checks to discover which operation-family capabilities a different provider implements. Every QoreID named operation below sends a fixed HTTP method and path, performs backend-only API token exchange/caching, and returns the decoded provider response unchanged. There is no public arbitrary-path request method.

Digital identity — DigitalIdentityDriverInterface

MethodHTTP route
verifyBvnBasic($bvnNumber, $payload = [], $query = [])POST /v1/ng/identities/bvn-basic/{bvnNumber}
verifyBvnPremium($bvnNumber, $payload = [], $query = [])POST /v1/ng/identities/bvn-premium/{bvnNumber}
matchBvn($idNumber, $payload = [])POST /v1/ng/identities/bvn-match/{idNumber}
verifyNin($idNumber, $payload = [])POST /v1/ng/identities/nin/{idNumber}
verifyNinByPhone($phoneNumber, $payload = [])POST /v1/ng/identities/nin-phone/{phoneNumber}
verifyNinPremium($idNumber, $payload = [])POST /v1/ng/identities/nin-premium/{idNumber}
verifyNinPremiumByPhone($phoneNumber, $payload = [])POST /v1/ng/identities/nin-phone-premium/{phoneNumber}
verifyVirtualNin($vNIN, $payload = [])POST /v1/ng/identities/virtual-nin/{vNIN}
verifyLasrraId($idNumber, $payload = [])POST /v2/ng/identities/lasrra/{idNumber}
verifyVotersCard($vin, $payload = [])POST /v1/ng/identities/vin/{vin}
verifyNigerianPassport($passportNumber, $payload = [])POST /v1/ng/identities/passport/{passportNumber}
verifyGhanaCard($idNumber, $payload = [])POST /v1/gh/identities/ghana-id/{idNumber}
verifyKenyaId($idNumber, $payload = [])POST /v1/ke/identities/kenya-id/{idNumber}
verifyKenyaPassport($passportNumber, $payload = [])POST /v1/ke/identities/passport/{passportNumber}
verifyKenyaAlienCard($idNumber, $payload = [])POST /v1/ke/identities/alien-card/{idNumber}
verifyDriversLicense($idNumber, $payload = [])POST /v1/ng/identities/driver-license/{idNumber}

For BVN Basic and BVN Premium, QoreID documents optional isBvnIgree as a query parameter. Send the documented payload fields in $payload; path identifiers are encoded as single URL path segments.

Biometrics — BiometricVerificationDriverInterface

MethodHTTP route
verifyNinFace($payload)POST /v1/ng/identities/face-verification/nin
verifyBvnFace($payload)POST /v1/ng/identities/face-verification/bvn
verifyDriversLicenseFace($payload)POST /v1/ng/identities/face-verification/drivers-license
verifyVirtualNinFace($payload)POST /v1/ng/identities/face-verification/vnin
verifyNigerianPassportFace($payload)POST /v1/ng/identities/face-verification/passport
verifyKenyaPassportFace($payload)POST /v1/ke/identities/face-verification/passport

The docs describe each endpoint as requiring idNumber plus one of photoBase64, photoUrl, or photo, but some page schemas declare binary photo while indicating JSON content. The package forwards the JSON payload and does not invent multipart behavior; confirm the desired photo transport with QoreID if using binary uploads.

Business and bank accounts

BusinessVerificationDriverInterface:

MethodHTTP route
verifyCacBasic($payload)POST /v1/ng/identities/cac-basic
verifyCacPremiumV2($payload)POST /v3/ng/identities/cac-premium-1
verifyCacPremiumV3($payload)POST /v3/ng/identities/cac-premium
verifyTin($tin)GET /v2/ng/identities/tin/{tin}

QoreID's CAC Premium pages are titled V2 and V3 but both document /v3 routes; the method names mirror page labels and each method preserves its route exactly.

BankAccountVerificationDriverInterface:

MethodHTTP route
verifyBvnNuban($payload)POST /v1/ng/identities/bvn-nuban
verifyNuban($payload)POST /v1/ng/identities/nuban
verifyNubanWithBvnConsent($bvn, $payload)POST /v1/ng/identities/bvn-accounts-with-consent/{bvn}
getNubanBanksForBvnData()GET /v1/banks/bvn-nuban

Address, background and assets

  • AddressVerificationDriverInterface::verifyAddressPro($payload) — POST /v1/addresses/pro.
  • BackgroundVerificationDriverInterface::verifyGuarantor($payload) — POST /v1/guarantors.
  • BackgroundVerificationDriverInterface::getGuarantorVerification($guarantorId) — GET /v1/guarantors/{guarantorId}.
  • BackgroundVerificationDriverInterface::verifyGuarantorPlus($payload) — POST /v1/guarantors/plus.
  • AssetVerificationDriverInterface::createPropertyVerification($payload) — POST /v1/properties.
  • AssetVerificationDriverInterface::getPropertyVerification($propertyId) — GET /v1/properties/{propertyId}.
  • AssetVerificationDriverInterface::verifyVehicle($payload) — POST /v1/vehilces (the reference spells the path this way).
  • AssetVerificationDriverInterface::verifyVehicleSelf($payload) — POST /v1/vehicles/self.
  • AssetVerificationDriverInterface::verifyLicensePlateBasic($plateNumber, $payload) — POST /v1/ng/identities/license-plate-basic/{plateNumber}.
  • AssetVerificationDriverInterface::verifyLicensePlatePremium($plateNumber, $payload) — POST /v1/ng/identities/license-plate-premium/{plateNumber}; required payload fields are firstname and lastname; phone is optional.
  • EmploymentVerificationDriverInterface::verifyEmployment($payload) — POST /v1/employments (official guide route; not an OpenAPI reference schema). Required fields: customerReference, employerName, employerEmail, employerPhone, and nested applicant.firstname, applicant.lastname, applicant.phone.

Workflows, reports, webhooks and lookup

  • WorkflowDriverInterface::getWorkflowByCustomerReference($customerReference) — GET /v1/workflows/customerReference/{customerReference}.
  • WorkflowDriverInterface::getWorkflowById($id) — GET /v1/workflows/{id}.
  • WorkflowDriverInterface::getWorkflowReport($workflowId, $query = []) — GET /v1/api/workflow/{workflowId}; query can include requestId or customerReference.
  • WorkflowDriverInterface::getCollectionReport($collectionId, $productCode, $query = []) — GET /v1/api/collections/{collectionId}/products/{productCode}; provide documented requestId or customerReference selector.
  • CollectionWebhookDriverInterface::resendCollectionWebhook($verificationType, $query = []) — POST /v1/webhooks/collection/{verificationType}; docs list verification types including realtime, address, property, guarantor, and employment, plus either requestId or customerReference.
  • IdentityLookupDriverInterface::getCountries() — GET /v1/countries.
  • IdentitySessionDriverInterface::createSession($data) — POST /v1/sessions with HTTP Basic auth.

Example:

use Alphabalex\IdentityMadeEasy\Contracts\WorkflowDriverInterface;
use Alphabalex\IdentityMadeEasy\Facades\Identity;

$driver = Identity::driver('qoreid');
if ($driver instanceof WorkflowDriverInterface) {
    $report = $driver->getCollectionReport('collection-1', 'nin', ['requestId' => 'request-1']);
}

Documentation gaps and scope

QoreID's public llms.txt and reference navigation do not form a complete, verifiable route catalogue. This package exposes named methods for individually verified operation definitions found in the reviewed docs; it does not claim to cover every QoreID endpoint in existence. These topics are not implemented because their exact route contract could not be confirmed: regular NUBAN-bank list, States/LGAs, webhook resend by flow ID/reference (distinct from the collection webhook route above), Kenya CR12/business search, OCR, standalone liveness, business-address verification, employment fetch by ID, and additional document/face variants. License Plate Premium and Employment Verification submission are included as named methods; employment submission is documented in an official guide rather than a page-level OpenAPI schema.

AML/PEP is also excluded: the API reference and guide disagree on /v2/ng/identities/aml-pep vs /v1/ng/identities/aml-pep, and the reference metadata is inconsistent. Address Verification Status documents status values rather than an operation. Some QoreID schemas contain literal spelling errors such as /v1/vehilces, chasisNo, SideMirror Cracks, and fistname; named methods preserve verified paths and provider-native payloads. Do not assume paths for excluded topics.

See QoreID's API reference, reference index, product codes, License Plate Premium schema, and Employment Verification guide.

Errors and privacy

  • HTTP failures from QoreID and transport errors propagate to the caller; catch them at your application boundary and apply an appropriate retry/reporting policy.
  • Invalid session input fails before network access.
  • Verification responses may contain sensitive identity information. Do not log full request/response payloads by default; encrypt or minimize retained data and follow your organization's consent, privacy, and retention requirements.

QoreID references