proofage / laravel-client
Laravel client package for ProofAge API with HMAC authentication
Requires
- php: ^8.2
- illuminate/config: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- proofage/php-sdk: ^0.1.2
Requires (Dev)
- guzzlehttp/guzzle: ^7.0
- laravel/pint: ^1.30
- mockery/mockery: ^1.6
- orchestra/testbench: ^10.0|^11.0
- phpunit/phpunit: ^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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:
- Configuration - API keys and base URL are properly set
- Workspace Connection - Can successfully connect to ProofAge API
- Webhook URL - Webhook endpoint is configured in your workspace
- Route Existence - Laravel route exists for the webhook path
- HTTP Method - Route accepts POST requests
- 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:
- Checks that
PROOFAGE_SECRET_KEYis configured - Verifies the
X-HMAC-Signatureheader is present - Generates the expected signature using the same algorithm as ProofAge
- Compares signatures using
hash_equals()for timing-safe comparison - 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 informationworkspace()->getConsent()- Get consent information
Verifications
verifications()->create(array $data)- Create a new verificationverifications()->find(string $id)- Get verification by IDverifications(string $id)->acceptConsent(array $data)- Accept consentverifications(string $id)->uploadMedia(array $data)- Upload media filesverifications(string $id)->submit()- Submit verification for processingverifications(string $id)->document()- Get sanitized document fields and source mediaverifications(string $id)->estimation()- Get age-threshold and gender estimationverifications(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
- Platform: https://proofage.xyz
- Live Demo: https://demo.proofage.xyz
- Node SDK:
@proofage/nodeon npm
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.