Search by

proofage / laravel-client

dragg

Laravel client package for ProofAge API with HMAC authentication

Package info

github.com/ProofAge/laravel-client

Documentation

pkg:composer/proofage/laravel-client

Statistics

Installs: 1 521

Dependents: 0

Suggesters: 1

Stars: 0

Open Issues: 0

v0.7.0 2026-09-03 09:25 UTC

This package is auto-updated.

Last update: 2026-09-03 09:26:43 UTC


README

Platform: https://proofage.xyz | Packagist: https://packagist.org/packages/proofage/laravel-client

A Laravel package for integrating with the ProofAge API, featuring automatic HMAC authentication and a fluent interface.

Full API reference: https://docs.proofage.xyz/api-reference.html#/

About ProofAge

ProofAge is an online age verification platform enabling websites to confirm users meet minimum age requirements through a hosted, privacy-focused KYC process — without server-side document handling. It supports alcohol/tobacco/cannabis commerce, adult content platforms, gambling sites, and age-restricted subscriptions.

This package provides a first-class Laravel integration: a service provider with auto-discovery, a facade, HMAC-signed webhook middleware, and a setup verification command.

It is built on proofage/php-sdk, the framework-neutral ProofAge client, which does the request signing, the retries and the resource calls. This package adds the Laravel wiring and sends every request through the Http facade, so Http::fake() intercepts the client in your tests. Upgrading from 0.6? See UPGRADE.md.

Installation

Install the package via Composer:

composer require proofage/laravel-client

Configuration

Publish the configuration file:

php artisan vendor:publish --provider="ProofAge\Laravel\ProofAgeServiceProvider" --tag="config"

Configure your environment variables:

PROOFAGE_API_KEY=your-api-key
PROOFAGE_SECRET_KEY=your-secret-key
PROOFAGE_BASE_URL=https://api.proofage.xyz
PROOFAGE_VERSION=v1

Setup Verification

After configuration, verify your setup using the built-in command:

php artisan proofage:verify-setup

Successful Setup Output

When everything is configured correctly, you should see:

✅ Configuration is valid
✅ Workspace connection successful
✅ Webhook URL is configured https://yoursite.com/webhooks/proof-age
✅ Webhook route found: POST webhooks/proof-age -> App\Http\Controllers\WebhookController@handleProofAgeWebhook
✅ Webhook route is protected with VerifyWebhookSignature middleware
✅ ProofAge setup verified successfully!

What the Command Checks

The verification command ensures:

  1. Configuration - API keys and base URL are properly set
  2. Workspace Connection - Can successfully connect to ProofAge API
  3. Webhook URL - Webhook endpoint is configured in your workspace
  4. Route Existence - Laravel route exists for the webhook path
  5. HTTP Method - Route accepts POST requests
  6. Security Middleware - Route is protected with HMAC signature verification

Troubleshooting

If you see errors about missing middleware, add it to your webhook route:

Route::post('/webhooks/proof-age', [WebhookController::class, 'handleProofAgeWebhook'])
    ->middleware('proofage.verify_webhook');

Usage

Basic Usage

use ProofAge\Laravel\Facades\ProofAge;
use ProofAge\Laravel\Resources\VerificationResource;

// Get workspace information
$workspace = ProofAge::workspace()->get();

// Create a verification
$verification = ProofAge::verifications()->create([
    'callback_url' => 'https://your-app.com/webhook',
    'metadata' => ['user_id' => 123]
]);

// Get verification details
$verification = ProofAge::verifications()->find('verification-id');

// Get age estimation details
$estimation = ProofAge::verifications('verification-id')->estimation();
// [
//     'verification_id' => '...',
//     'attempt_id' => '...',
//     'age_threshold' => [
//         'minimum' => 18,
//         'passed' => true,
//         'confidence' => 0.98,
//     ],
//     'gender' => [
//         'value' => VerificationResource::GENDER_FEMALE, // 0 = female, 1 = male
//         'confidence' => 0.93,
//     ],
// ]

// Accept consent for verification
ProofAge::verifications('verification-id')->acceptConsent([
    'consent_version_id' => 1,
    'text_sha256' => 'hash-value'
]);

// Upload media
ProofAge::verifications('verification-id')->uploadMedia([
    'type' => 'selfie',
    'file' => $uploadedFile
]);

// Submit verification
ProofAge::verifications('verification-id')->submit();

Using the Client Directly

use ProofAge\Laravel\ProofAgeClient;

$client = app(ProofAgeClient::class);   // app(\ProofAge\Sdk\Client::class) resolves the same singleton
$workspace = $client->workspace()->get();

// Lower level: makeRequest() returns a ProofAge\Sdk\Http\Response
$response = $client->makeRequest('GET', 'workspace');
$response->status();
$response->json();

Middleware and events

ProofAgeClient is the SDK client, so its middleware and events are available: a middleware runs once per HTTP attempt, before signing; events observe the signed request and the response, with the API key and signature masked.

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use ProofAge\Laravel\ProofAgeClient;
use ProofAge\Sdk\Events\ResponseEvent;
use ProofAge\Sdk\Http\Request;
use ProofAge\Sdk\Http\Response;

$client = app(ProofAgeClient::class);

$client->pushMiddleware(fn (Request $request, callable $next): Response => $next(
    $request->withHeader('X-Request-Id', (string) Str::uuid())
));

$client->onResponse(fn (ResponseEvent $e) => Log::info('proofage.response', [
    'status' => $e->status(),
    'attempt' => $e->attempt(),
    'ms' => $e->durationMs(),
]));

See the SDK's README for the full middleware and event API and for what raw() on an event exposes.

Secrets in dumps

dd(), dump(), print_r() and var_dump() of the client, of a request or of a caught exception show the SDK's redacted view: the secret key as [redacted], the API key and the HMAC signature masked, a request body as its size and sha256 rather than its bytes. The SDK covers print_r() and var_dump() itself through __debugInfo(); this package registers casters with Symfony's VarDumper — what Laravel's dd() and dump() use, and which otherwise reads the real properties by reflection — for the same classes when Composer's autoloader loads. var_export() and reflection are not covered.

Webhook Security

The package includes middleware to verify HMAC signatures on incoming webhook requests from ProofAge.

Using the Middleware

Apply the middleware to your webhook routes:

// In your routes/web.php or routes/api.php
Route::post('/proofage/webhook', [WebhookController::class, 'handle'])
    ->middleware('proofage.verify_webhook');

Or apply it to a route group:

Route::middleware(['proofage.verify_webhook'])->group(function () {
    Route::post('/proofage/decision-webhook', [WebhookController::class, 'handleDecision']);
    Route::post('/proofage/track-webhook', [WebhookController::class, 'handleStatusChanged']);
});

How It Works

The middleware:

  1. Checks that PROOFAGE_SECRET_KEY is configured
  2. Verifies the X-HMAC-Signature header is present
  3. Generates the expected signature using the same algorithm as ProofAge
  4. Compares signatures using hash_equals() for timing-safe comparison
  5. Returns appropriate error responses for invalid requests

Multiple Workspaces

Some applications need separate verification flows for different user roles. For example, a marketplace where buyers go through a basic age check while sellers require full identity verification -- each with its own ProofAge workspace, credentials, and webhook endpoint.

The package supports this out of the box. All shared settings (base_url, version, timeout, etc.) are inherited from the default proofage config, so additional workspaces only need their own api_key and secret_key.

Example: marketplace with buyer and seller verification

1. Configure credentials for each workspace

The default workspace (buyers) is configured via config/proofage.php as usual. For sellers, add a second set of credentials anywhere in your application config -- config/services.php is a common choice:

// config/services.php
'proofage_seller' => [
    'api_key' => env('PROOFAGE_SELLER_API_KEY'),
    'secret_key' => env('PROOFAGE_SELLER_SECRET_KEY'),
],
# .env
# Buyer workspace (default)
PROOFAGE_API_KEY=pk_live_...
PROOFAGE_SECRET_KEY=sk_live_...

# Seller workspace
PROOFAGE_SELLER_API_KEY=pk_live_...
PROOFAGE_SELLER_SECRET_KEY=sk_live_...

2. Create verifications with the correct client

The ProofAge facade and app(ProofAgeClient::class) singleton always use the default (buyer) workspace. For the seller workspace, use ProofAgeClientFactory:

use ProofAge\Laravel\Facades\ProofAge;
use ProofAge\Laravel\ProofAgeClientFactory;

// Buyer verification -- uses default proofage.* config
$buyerVerification = ProofAge::verifications()->create([
    'callback_url' => 'https://marketplace.com/webhooks/proofage',
]);

// Seller verification -- uses services.proofage_seller config
$sellerClient = app(ProofAgeClientFactory::class)->make('services.proofage_seller');
$sellerVerification = $sellerClient->verifications()->create([
    'callback_url' => 'https://marketplace.com/webhooks/proofage-seller',
]);

3. Set up separate webhook routes

Each workspace sends webhooks signed with its own secret key. Use the middleware's config prefix parameter to verify signatures with the correct credentials:

// routes/api.php

// Buyer webhooks -- verified with default proofage.* keys
Route::post('/webhooks/proofage', [BuyerWebhookController::class, 'handle'])
    ->middleware('proofage.verify_webhook');

// Seller webhooks -- verified with services.proofage_seller keys
Route::post('/webhooks/proofage-seller', [SellerWebhookController::class, 'handle'])
    ->middleware('proofage.verify_webhook:services.proofage_seller');

4. Verify setup for each workspace

# Check the buyer (default) workspace
php artisan proofage:verify-setup

# Check the seller workspace
php artisan proofage:verify-setup --config=services.proofage_seller

The command checks configuration, API connectivity, webhook route existence, and that the middleware uses the matching config prefix -- so you'll be warned if the keys would mismatch.

Config resolution

When a custom config prefix is used, the following resolution rules apply:

Key Resolution
api_key Read from the specified prefix (required)
secret_key Read from the specified prefix (required)
base_url Specified prefix, falls back to proofage.base_url
version Specified prefix, falls back to proofage.version
timeout Specified prefix, falls back to proofage.timeout
retry_attempts Specified prefix, falls back to proofage.retry_attempts
retry_delay Specified prefix, falls back to proofage.retry_delay
webhook_tolerance Specified prefix, falls back to proofage.webhook_tolerance (default: 300s)

Additional workspaces only need api_key and secret_key. If a workspace connects to a different ProofAge environment (e.g. staging), add base_url under the same prefix and it will take priority over the default.

API Methods

Workspace

  • workspace()->get() - Get workspace information
  • workspace()->getConsent() - Get consent information

Verifications

  • verifications()->create(array $data) - Create a new verification
  • verifications()->find(string $id) - Get verification by ID
  • verifications(string $id)->acceptConsent(array $data) - Accept consent
  • verifications(string $id)->uploadMedia(array $data) - Upload media files
  • verifications(string $id)->submit() - Submit verification for processing
  • verifications(string $id)->document() - Get sanitized document fields and source media
  • verifications(string $id)->estimation() - Get age-threshold and gender estimation
  • verifications(string $id)->blockFace(?array $data) - Block the verification face for AML

Every method's exact request and response shape is documented in the SDK: its AGENTS.md (vendor/proofage/php-sdk/AGENTS.md), the @param/@return PHPDoc on ProofAge\Sdk\Resources\*, and the bundled vendor/proofage/php-sdk/resources/openapi.json.

Enums

ProofAge\Sdk\Enums\VerificationStatus, ProofAge\Sdk\Enums\WebhookReason and ProofAge\Sdk\Enums\BlockFaceReasonCode model the status, AML reason and reason_code values. (ProofAge\Laravel\Enums\* was removed in 0.7.0.)

Error Handling

Inside a Laravel application, catch the Laravel name for a specific status and the SDK base class for everything:

use ProofAge\Laravel\Exceptions\AuthenticationException;   // 401
use ProofAge\Laravel\Exceptions\ValidationException;       // 422, getErrors()
use ProofAge\Sdk\Exceptions\TransportException;            // connection refused, DNS, TLS, timeout
use ProofAge\Sdk\Exceptions\ProofAgeException;             // every other non-2xx, and the base class of all of the above

try {
    $verification = ProofAge::verifications()->create($data);
} catch (AuthenticationException $e) {
    // 401: $e->getErrorCode()
} catch (ValidationException $e) {
    // 422: $e->getErrors()
} catch (TransportException $e) {
    // The API could not be reached; $e->getResponse() is null
} catch (ProofAgeException $e) {
    // Everything else: $e->getCode() is the HTTP status, $e->getResponse() the ProofAge\Sdk\Http\Response
}

The client throws ProofAge\Laravel\Exceptions\AuthenticationException for a 401, ValidationException for a 422 and ProofAgeException for every other non-2xx; the webhook middleware throws WebhookVerificationException. All four descend from ProofAge\Laravel\Exceptions\ProofAgeException, which descends from ProofAge\Sdk\Exceptions\ProofAgeException — the catch-all, and the only one of the two bases that also catches TransportException.

They do not descend from the SDK's own ProofAge\Sdk\Exceptions\AuthenticationException, ValidationException or WebhookVerificationException: PHP allows one parent, and keeping the pre-0.7 catch (ProofAge\Laravel\Exceptions\ProofAgeException) working won. A catch on one of those three SDK names therefore never matches inside a Laravel application — a 422 would fall through to whatever comes next. The Laravel names are deprecated in 0.7 and removed in 1.0, when the SDK names become what is thrown; see UPGRADE.md.

Webhook Exception Handling

The webhook middleware throws ProofAge\Laravel\Exceptions\WebhookVerificationException on invalid requests. It descends from ProofAge\Laravel\Exceptions\ProofAgeException (not from the SDK's WebhookVerificationException; see Error Handling above) and carries errorCode, statusCode and toArray(). By default, the exception renders a JSON error response:

{
    "error": {
        "code": "INVALID_SIGNATURE",
        "message": "HMAC signature is invalid"
    }
}

To customize this response, register a renderable in your application's exception handler:

Laravel 11+ (bootstrap/app.php):

use ProofAge\Laravel\Exceptions\WebhookVerificationException;

->withExceptions(function (Exceptions $exceptions) {
    $exceptions->renderable(function (WebhookVerificationException $e) {
        return response()->json([
            'error' => [
                'code' => $e->errorCode,
                'message' => $e->getMessage(),
            ],
        ], $e->statusCode);
    });
})

Laravel 10 (app/Exceptions/Handler.php):

use ProofAge\Laravel\Exceptions\WebhookVerificationException;

public function register(): void
{
    $this->renderable(function (WebhookVerificationException $e) {
        return response()->json([
            'error' => [
                'code' => $e->errorCode,
                'message' => $e->getMessage(),
            ],
        ], $e->statusCode);
    });
}

Testing

composer test

In your own application's tests, Http::fake() intercepts every request the client makes, including multipart uploads ($request->hasFile('file')) and the signed headers ($request->header('X-HMAC-Signature')):

Http::fake(['api.proofage.xyz/v1/workspace' => Http::response(['id' => 'ws_1', 'name' => 'Acme'])]);

ProofAge::workspace()->get();

Http::assertSent(fn ($request) => $request->hasHeader('X-API-Key'));

If you would rather not go through the facade, ProofAge\Sdk\Testing\FakeHttpClient is a transport double the SDK ships: new ProofAgeClient($config, $fake).

Additional Resources

Integrations for other platforms

Platform Repository Use-case
Node.js ProofAge/node-client Node.js age verification client — HMAC-signed API calls, webhook verification for Express, Hono, Next.js and other Node.js frameworks
WordPress ProofAge/wordpress-plugin Age gate plugin for WordPress — WooCommerce age verification, age-restricted pages, adult content gating
Laravel this repo Laravel age verification client — HMAC-signed API calls, webhook handling, middleware for age-restricted routes
Next.js ProofAge/demo Full-stack age verification demo with JS SDK, server routes, and webhook receiver

License

The MIT License (MIT). Please see License File for more information.