Skip to main content

Laravel Integration

Latest Version on Packagist Tests PHP Version

The genvoris/laravel package is the official Laravel integration for the Genvoris Virtual Try-On platform. It provides a service provider, Facade, Blade directives, webhook verification and dispatch, and a server-side proxy — everything you need to add virtual try-on to a Laravel application without exposing your API key to the browser.

Requirements: PHP ^8.1, Laravel ^10 | ^11 | ^12

Links: Packagist · GitHub · Changelog


Installation

composer require genvoris/laravel
php artisan genvoris:install

Add your credentials to .env:

GENVORIS_API_KEY=gvk_live_your_key_here
GENVORIS_API_URL=https://genvoris.org/api/v1
GENVORIS_PROXY_UPSTREAM=https://api.genvoris.org
GENVORIS_PROXY_PATH=genvoris-proxy
GENVORIS_WEBHOOK_SECRET=your_webhook_secret_here
GENVORIS_WEBHOOK_PATH=webhooks/genvoris

GENVORIS_API_BASE_URL is also accepted as an alias for GENVORIS_API_URL; GENVORIS_TRYON_UPSTREAM and TRYON_BACKEND_URL are accepted as aliases for GENVORIS_PROXY_UPSTREAM.

Confirm the connection:

php artisan genvoris:test-connection

Configuration

Publish the config file:

php artisan vendor:publish --tag=genvoris-config

Key options in config/genvoris.php:

KeyDefaultDescription
api_keyenv('GENVORIS_API_KEY')Platform API key
api_base_urlhttps://genvoris.org/api/v1API base URL (GENVORIS_API_URL / GENVORIS_API_BASE_URL)
timeout30HTTP timeout (seconds)
retry.times3Retries on 429 / 5xx (200/800/3200ms jittered backoff)
retry.sleep[200, 800, 3200]Sleep intervals in ms
webhook.secretenv('GENVORIS_WEBHOOK_SECRET')HMAC signing secret
webhook.pathwebhooks/genvorisWebhook route prefix
webhook.middleware['api']Middleware groups for the webhook route
webhook.listeners[]Additional event-to-listener bindings
proxy.pathgenvoris-proxyProxy route prefix (GENVORIS_PROXY_PATH)
proxy.upstreamhttps://api.genvoris.orgTry-on/widget upstream (GENVORIS_PROXY_UPSTREAM, GENVORIS_TRYON_UPSTREAM, or TRYON_BACKEND_URL)
proxy.middleware['throttle:60,1']Middleware for proxy routes
proxy.allowed_paths['api/analyze', 'api/tryon', 'api/config', 'api/status', 'api/v1/events']Allowed upstream paths
proxy.events_pathapi/v1/eventsWidget analytics path under the proxy base
proxy.enforce_origintrueWhen true, checks Origin against APP_URL
external_id_prefixlaravel_Prefix for external IDs
widget_urlhttps://api.genvoris.org/widget.jsWidget script URL
cache.sessionstrueCache minted session tokens
cache.storenullCache store (null = default)
cache.ttl840Session cache TTL in seconds

Quick Start

1. Add the trait to your User model

use Genvoris\Laravel\Concerns\HasGenvorisAccess;

class User extends Authenticatable
{
use HasGenvorisAccess;
}

The trait auto-prefixes your user IDs with the configured external_id_prefix (default laravel_). It also exposes methods for syncing, session minting, and quota checks (see HasGenvorisAccess methods below).

2. Mint a session token in a controller

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class TryOnController extends Controller
{
public function session(Request $request): JsonResponse
{
$session = $request->user()->genvorisSession();
return response()->json(['token' => $session->token]);
}
}

3. Add the widget to your Blade layout

{{-- In your <head> or before </body> --}}
@genvorisConfig(['productId' => $product->id, 'token' => $session->token])
@genvorisScripts(['token' => $session->token, 'noFab' => true])

{{-- Where you want the button --}}
@genvorisTryOnButton([
'productId' => $product->id,
'productTitle' => $product->name,
'productImage' => $product->image_url,
'label' => 'Try On',
])

The rendered script uses data-api-url, data-events-url, data-platform="laravel", and the short-lived token. It never prints GENVORIS_API_KEY into HTML.


API

Facade

use Genvoris\Laravel\Facades\Genvoris;

$customer = Genvoris::upsertCustomer('42', ['email' => 'alice@example.com']);
$session = Genvoris::mintSession($customer->id);
$plans = Genvoris::listPlans();
$usage = Genvoris::customerUsage($customer->id);

Resource methods

Access the underlying resource classes directly for fine-grained control:

use Genvoris\Laravel\Facades\Genvoris;

// Customers
Genvoris::customer()->upsert('42', ['email' => 'alice@example.com']);
Genvoris::customer()->find('ec_abc');
Genvoris::customer()->findByExternalId('laravel_42');
Genvoris::customer()->list(['status' => 'ACTIVE']);
Genvoris::customer()->update('ec_abc', ['email' => 'new@example.com']);
Genvoris::customer()->cancel('ec_abc'); // soft cancel
Genvoris::customer()->usage('ec_abc'); // returns CustomerUsage

// Plans
Genvoris::plan()->create(['name' => 'Pro', 'monthlyTryOns' => 100]);
Genvoris::plan()->find('pln_abc');
Genvoris::plan()->list();
Genvoris::plan()->update('pln_abc', ['name' => 'Pro Plus']);
Genvoris::plan()->disable('pln_abc'); // soft-disable

// Sessions
Genvoris::session()->mint('ec_abc', ['ttlSeconds' => 900]);
Genvoris::session()->mintForUser($user); // upserts + mints in one call

// Webhook verifier (stateless)
Genvoris::webhooks()->verify($rawBody, $signatureHeader, $secret);

HasGenvorisAccess methods

When you add the HasGenvorisAccess trait to an Eloquent model:

MethodReturn typeDescription
genvorisExternalId()stringReturns {prefix}_{id} (e.g. laravel_42)
syncToGenvoris(array $attrs)CustomerUpserts the user in the Genvoris platform and caches the customer ID locally
genvorisCustomerId()?stringReads the cached Genvoris customer ID from the local sessions table (if migration was run), or calls the API
genvorisSession(int $expiresIn)SessionMints a session token (optionally cached)
genvorisUsage()CustomerUsageFetches current usage and quota
canTryOn()boolWhether the user has remaining quota (returns false on any error)
genvorisPortalCustomer()CustomerFetches the full Genvoris customer object
resolveOrSyncCustomerId()string(internal) Resolves the customer ID, syncing if not found

Blade directives

DirectiveOutput
@genvorisScripts($opts)Loads the widget with data-api-url, data-events-url, data-platform, optional token, and optional no_fab.
@genvorisConfig($opts)<script>window.genvorisConfig = {...};</script> — safe config JSON with proxy/event URLs.
@genvorisWidget($opts)Config + scripts combined in one directive.
@genvorisTryOnButton($opts)<button data-genvoris-trigger data-genvoris-product="...">Try On</button> with optional product metadata.

Security: @genvorisConfig never includes api_key or webhook.secret in its output. JSON is encoded with JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP to prevent XSS.

Components

ComponentUsage
<x-genvoris-try-on-button />$productId, $class (default: genvoris-try-on-btn), $label (default: Try On)
<x-genvoris-try-on-script />Emits the deferred widget script tag (no config)

Webhooks

The package auto-registers a webhook route and verifies every payload with HMAC-SHA256 before it reaches your event listeners. Register your endpoint in the Genvoris dashboard:

POST https://yourapp.com/webhooks/genvoris

Verification

Verification is handled automatically by the VerifyGenvorisWebhook middleware. It:

  1. Reads the raw body via $request->getContent().
  2. Parses the X-Genvoris-Signature header (t=<unix>,v1=<hex>).
  3. Validates the timestamp is within ±300 seconds (replay protection).
  4. Computes HMAC-SHA256 of ${t}.${rawBody} and compares with hash_equals() (constant-time).
  5. Aborts with 401 on failure.

The underlying WebhookVerifier class is also available for manual use:

use Genvoris\Laravel\Webhooks\WebhookVerifier;

$verifier = new WebhookVerifier();
$valid = $verifier->verify($rawBody, $signatureHeader, $secret);
// returns bool — never throws

Listening to events

use Genvoris\Laravel\Webhooks\Events\CustomerCreated;

Event::listen(CustomerCreated::class, function (CustomerCreated $event) {
$data = $event->payload['data'];
// provision local resources, send welcome email, etc.
});

All typed events expose a single $payload property (the full decoded JSON object).

Supported event types

Portal event typeLaravel event classFires when
tryon.completedTryOnCompletedTry-on generation succeeded
tryon.failedTryOnFailedTry-on generation failed
customer.plan_changedCustomerPlanChangedCustomer plan changed
customer.quota_exhaustedCustomerQuotaExhaustedTry-on rejected for quota
credit.low_balanceCreditLowBalanceStore credits crossed low-balance threshold
credit.balance_addedCreditBalanceAddedStore credits were added
end_customer.createdCustomerCreatedFirst upsert of a customer
end_customer.updatedCustomerUpdatedCustomer re-upserted or PATCH'd
end_customer.cancelledCustomerCancelledCustomer DELETEd (soft cancel)
end_customer.quota_warningCustomerQuotaWarningCustomer crosses 80% of plan quota
end_customer.quota_exhaustedCustomerQuotaExhaustedLegacy quota-exhausted alias
end_customer.period_rolledCustomerPeriodRolledPeriod auto-rolled to new 30-day window
plan.createdPlanCreatedA plan was created
plan.updatedPlanUpdatedA plan was updated
plan.disabledPlanDisabledA plan was soft-deleted

You can also listen to GenvorisWebhookReceived to catch all events in a single listener:

use Genvoris\Laravel\Webhooks\Events\GenvorisWebhookReceived;

Event::listen(GenvorisWebhookReceived::class, function (GenvorisWebhookReceived $event) {
// $event->type — the raw event type string
// $event->id — the event envelope id
// $event->payload — full decoded JSON
});

Idempotency

The WebhookController deduplicates deliveries using the X-Genvoris-Delivery header. Duplicates within 24 hours return {"received": true, "duplicate": true} and are not dispatched to listeners.

Webhook test command

Send a synthetic signed webhook to test your endpoint:

php artisan genvoris:webhook-test

Optional: --url to override the destination, --event to change the event type (default: end_customer.created).


Proxy

The package registers /genvoris-proxy/{path} for GET, POST, PUT, PATCH, DELETE, and OPTIONS to forward widget requests to the configured upstream with your API key injected server-side — the browser never sees your key.

Security features

  • Path allowlist — only paths in proxy.allowed_paths are forwarded (default: api/analyze, api/tryon, api/config, api/status, api/v1/events).
  • Path traversal guard — rejects paths containing .. or null bytes.
  • HTTP method whitelist — only GET, POST, PUT, PATCH, DELETE, OPTIONS are allowed; other methods return 405.
  • Origin enforcement — when proxy.enforce_origin is true, the Origin/Referer header is checked against APP_URL (case-insensitive).
  • Rate limiting — configurable via proxy.middleware (default: 60 requests/min per IP).
  • Key isolation — the X-API-Key header is added server-side; it is stripped from the response body.
  • Connection failure handling — upstream 5xx or connection errors return 502 without exposing internal details.

@genvorisConfig automatically sets window.genvorisConfig.apiProxyBase and window.genvorisConfig.eventsUrl to the same-origin proxy URLs.


Artisan Commands

CommandDescription
php artisan genvoris:installInteractive setup — publishes config, optionally views and migration
php artisan genvoris:test-connectionVerify API key connectivity (lists plans on success)
php artisan genvoris:list-plansDisplay all plans in a table (ID, Name, Status, Monthly Try-Ons)
php artisan genvoris:list-customersDisplay paginated customer list (ID, External ID, Email, Status, Plan ID)
php artisan genvoris:webhook-testSend a signed synthetic webhook to test your handler

Optional: Customer Sessions Table

Publish and run the optional migration to cache customer IDs locally:

php artisan vendor:publish --tag=genvoris-migrations
php artisan migrate

This creates a genvoris_customer_sessions table with a polymorphic user relationship:

ColumnTypePurpose
user_typestringMorphs to your User model
user_idbigintLocal user primary key
genvoris_customer_idstringThe ec_xxx id from Genvoris
external_idstringlaravel_{local_user_id}
plan_idstringCurrent plan snapshot
statusstringCurrent customer status
session_tokentextCached JWT
session_expires_atdatetimeToken expiry
last_synced_atdatetimeLast sync timestamp

When present, HasGenvorisAccess reads from it instead of calling the API on every request.


Error handling

All Genvoris exceptions extend GenvorisException (which extends RuntimeException):

ExceptionHTTPThrown when
AuthException401 / 403Invalid or revoked API key
ApiException4xx / 5xxAPI returned an error (has statusCode, errorCode, requestId)
WebhookExceptionWebhook signature verification failed
GenvorisExceptionNetwork errors (DNS failure, connection refused, timeout)

The SDK never exposes the API key in exception messages.

Testing

composer test

Relaunch checklist

Run these in the consuming Laravel app before relaunching:

php artisan config:clear
php artisan config:cache
php artisan genvoris:test-connection
php artisan route:list | grep genvoris

Confirm rendered pages contain data-api-url, data-events-url, and the short-lived token, but never contain GENVORIS_API_KEY.

The test suite uses Http::fake() — no live API calls are made. The package ships 40+ tests across unit and feature suites:

  • ClientTest — auth headers, response unwrapping, error mapping
  • DataObjectsTest — DTO construction from API responses
  • WebhookVerificationTest — valid signatures, tampered bodies, expired timestamps
  • BladeDirectivesTest — script output, XSS escaping, config isolation
  • CustomerResourceTest — upsert, prefix handling, find
  • SessionResourceTest — mint, TTL clamping
  • ProxyControllerTest — key injection, path allowlist, method whitelist, 502 handling
  • WebhookControllerTest — event dispatch, duplicate detection, signature enforcement
  • ServiceProviderTest — facade resolution, config loading

Further Reading