Skip to content

Webhooks in Laravel ​

The SDK verifies the signature and gives you the event as an array. See Receiving webhooks for how delivery works and the event reference for all events.

The route ​

Exclude the route from CSRF protection in bootstrap/app.php:

php
->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: ['webhooks/*']);
})

Then verify each request with the raw body and answer 2xx within 10 seconds:

php
use Anikeen\Id\Exceptions\SignatureVerificationException;
use Anikeen\Id\Webhooks\EventType;
use Anikeen\Id\Webhooks\Webhook;

Route::post('webhooks/anikeen', function (Request $request) {
    try {
        $event = Webhook::constructEvent(
            $request->getContent(),
            $request->header('Anikeen-Signature'),
            config('services.anikeen.webhook_secret'),
        );
    } catch (SignatureVerificationException) {
        abort(400);
    }

    if (! ProcessedEvent::firstOrCreate(['id' => $event['id']])->wasRecentlyCreated) {
        return response()->noContent(); // already handled
    }

    $object = $event['data']['object'];

    match ($event['type']) {
        EventType::ORDER_COMPLETED => Shop::fulfil($object['id'], $object['shipping_address']),
        EventType::ORDER_CANCELLED => Shop::release($object['id']),
        EventType::SUBSCRIPTION_ACTIVATED => Pass::grant($object['id']),
        EventType::SUBSCRIPTION_CANCELLED => Pass::revoke($object['id']),
        EventType::CHECKOUT_COMPLETED => Panel::provision($object['subscription_ids']),
        default => null,
    };

    return response()->noContent();
});

Webhook::constructEvent() rejects signatures older or newer than 5 minutes (the optional fourth argument changes the tolerance in seconds). Webhook::verify() runs the same checks (signature and timestamp) without decoding the event. EventType::all() lists every event type.

User and preference events ​

php
use Anikeen\Id\Resources\User;

$object = $event['data']['object'];

if ($event['type'] === EventType::USER_UPDATED) {
    $profile = new User($object);
    $account = Account::firstWhere('anikeen_id', $profile->id());

    if ($account && $profile->version() > $account->profile_version) {
        $account->update([
            'name' => $profile->name(),
            'email' => $profile->email() ?? $account->email, // null while unverified
            'profile_version' => $profile->version(),
        ]);
    }
}

if ($event['type'] === EventType::USER_DELETED) {
    Account::firstWhere('anikeen_id', $object['id'])?->anonymise();
}

if ($event['type'] === EventType::PREFERENCE_UPDATED) {
    Cache::forget("preferences:{$object['user_id']}");
}

App access beta ​

php
if ($event['type'] === EventType::APP_ACCESS_REVOKED) {
    // $object = {object: "app_access", user_id, client: {id, name}, revoked_at}
    Delegation::release($object['user_id'], $object['client']['id']);
}

Anikeen ID developer documentation