apple-passbook-laravel maintained by laulamanapps
Apple Passbook Laravel
This package provides Laravel integration for the LauLamanApps Apple Passbook Package:
generate Apple Wallet passes (.pkpass files) and serve the Apple PassKit Web Service endpoints from your Laravel application.
Requirements
- PHP 8.1+
- Laravel 11.x, 12.x or 13.x
Installation
composer require laulamanapps/apple-passbook-laravel
The service provider (LauLamanApps\ApplePassbookLaravel\ApplePassbookServiceProvider) is registered automatically via Laravel package auto-discovery.
Run Tests
composer install
./bin/phpunit
Configuration
Publish the config file:
php artisan vendor:publish --tag=apple-passbook-config
This creates config/apple-passbook.php:
| Key | Default | Description |
|---|---|---|
certificate |
env('APPLE_PASSBOOK_CERTIFICATE') |
Path to your .p12 Pass Type ID certificate |
certificate_password |
env('APPLE_PASSBOOK_CERTIFICATE_PASSWORD') |
Password protecting the .p12 file |
apple_wwdr_ca |
env('APPLE_PASSBOOK_WWDR_CA') (null) |
Optional path to an Apple WWDR CA .pem; null uses the library default |
web_service.enabled |
env('APPLE_PASSBOOK_WEB_SERVICE_ENABLED', true) |
Whether the PassKit web service routes are registered |
web_service.route_prefix |
env('APPLE_PASSBOOK_WEB_SERVICE_ROUTE_PREFIX', '/v1') |
Route prefix for the web service endpoints |
Add the ENV variables to your .env file:
###> laulamanapps/apple-passbook-laravel ###
APPLE_PASSBOOK_CERTIFICATE=/path/to/certificates/pass.p12
APPLE_PASSBOOK_CERTIFICATE_PASSWORD=password
###< laulamanapps/apple-passbook-laravel ###
Get Certificate
Head over to the Apple Developer Portal to get yourself a certificate to sign your passbooks with.
Export the certificate and key to a .p12 file using Keychain Access.
Generating a Pass
The package registers LauLamanApps\ApplePassbook\Build\Compiler as a singleton, configured with your certificate. Inject it anywhere:
namespace App\Http\Controllers;
use LauLamanApps\ApplePassbook\Build\Compiler;
use LauLamanApps\ApplePassbook\GenericPassbook;
use LauLamanApps\ApplePassbook\MetaData\Barcode;
use LauLamanApps\ApplePassbook\Style\BarcodeFormat;
final class PassbookController
{
public function __construct(
private readonly Compiler $compiler,
) {
}
public function download()
{
$passbook = new GenericPassbook('8j23fm3');
$passbook->setTeamIdentifier('<TeamId>');
$passbook->setPassTypeIdentifier('<PassTypeId>');
$passbook->setOrganizationName('Toy Town');
$passbook->setDescription('Toy Town Membership');
$barcode = new Barcode();
$barcode->setFormat(BarcodeFormat::Pdf417);
$barcode->setMessage('123456789');
$passbook->setBarcode($barcode);
return response($this->compiler->compile($passbook), 200, [
'Content-Description' => 'File Transfer',
'Content-Type' => 'application/vnd.apple.pkpass',
'Content-Disposition' => 'filename="passbook.pkpass"',
]);
}
}
See the core package documentation for all pass types (event ticket, coupon, boarding pass, store card, generic), fields, images, colors and locations.
PassKit Web Service
When web_service.enabled is true the package registers all Apple PassKit Web Service endpoints under the configured prefix (default /v1):
| Method | URI | Purpose |
|---|---|---|
POST |
/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} |
Register a device for pass updates |
DELETE |
/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} |
Unregister a device |
GET |
/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier} |
Serial numbers of updated passes |
GET |
/v1/passes/{passTypeIdentifier}/{serialNumber} |
Retrieve the latest version of a pass |
POST |
/v1/log |
Device diagnostic logging |
For the web service to work, the pass itself must point at your application:
$passbook->setWebService('https://example.com/', $authenticationToken);
Events
The controllers delegate all persistence decisions to your application through mutable event objects. Each event starts with Status::Unhandled — your listener must set the appropriate status, otherwise a LogicException is thrown.
| Event | Dispatched when |
|---|---|
DeviceRegisteredEvent |
A device registers for pass updates |
DeviceUnregisteredEvent |
A device unregisters from pass updates |
DeviceRequestUpdatedPassesEvent |
A device requests the serial numbers of updated passes (passesUpdatedSince is available via getPassesUpdatedSince()) |
RetrieveUpdatedPassbookEvent |
A device requests an updated pass (If-Modified-Since is available via getUpdatedSince()) |
The /v1/log endpoint needs no listener: messages are written to the application logger at info level.
Authenticating requests
Devices authenticate with an Authorization: ApplePass <token> header carrying the token you set on the pass with setWebService().
The token-carrying events expose isAuthenticatedBy(string $expectedToken): bool.
Always use
$event->isAuthenticatedBy(...)— never compare$event->getAuthenticationToken()with!==.isAuthenticatedBy()uses PHP'shash_equals(), which compares the two tokens in constant time. A plain string comparison short-circuits on the first differing byte, which leaks timing information an attacker can use to reconstruct a valid token byte by byte.
Example listeners
namespace App\Listeners;
use App\Models\Pass;
use DateTimeImmutable;
use LauLamanApps\ApplePassbookLaravel\Events\DeviceRegisteredEvent;
final class RegisterDevice
{
public function handle(DeviceRegisteredEvent $event): void
{
$pass = Pass::where('serial_number', $event->getSerialNumber())->first();
if ($pass === null || !$event->isAuthenticatedBy($pass->getAuthToken())) {
$event->notAuthorized();
return;
}
$registration = $pass->registrations()->firstOrCreate(
['device_library_identifier' => $event->getDeviceLibraryIdentifier()],
['push_token' => $event->getPushToken()],
);
if (!$registration->wasRecentlyCreated) {
$event->alreadyRegistered();
return;
}
$event->deviceRegistered();
}
}
namespace App\Listeners;
use App\Models\Pass;
use LauLamanApps\ApplePassbookLaravel\Events\DeviceUnregisteredEvent;
final class UnregisterDevice
{
public function handle(DeviceUnregisteredEvent $event): void
{
$pass = Pass::where('serial_number', $event->getSerialNumber())->first();
if ($pass === null || !$event->isAuthenticatedBy($pass->getAuthToken())) {
$event->notAuthorized();
return;
}
$pass->registrations()
->where('device_library_identifier', $event->getDeviceLibraryIdentifier())
->delete();
$event->deviceUnregistered();
}
}
namespace App\Listeners;
use App\Models\Pass;
use LauLamanApps\ApplePassbookLaravel\Events\DeviceRequestUpdatedPassesEvent;
final class ListUpdatedPasses
{
public function handle(DeviceRequestUpdatedPassesEvent $event): void
{
$passes = Pass::registeredTo($event->getDeviceLibraryIdentifier())
->where('pass_type_identifier', $event->getPassTypeIdentifier())
->when($event->getPassesUpdatedSince(), fn ($query, $since) => $query->where('updated_at', '>', $since))
->get();
if ($passes->isEmpty()) {
$event->notFound();
return;
}
$event->setSerialNumbers(
$passes->pluck('serial_number')->all(),
$passes->max('updated_at')->toDateTimeImmutable(),
);
}
}
namespace App\Listeners;
use App\Models\Pass;
use LauLamanApps\ApplePassbookLaravel\Events\RetrieveUpdatedPassbookEvent;
final class RetrieveUpdatedPassbook
{
public function handle(RetrieveUpdatedPassbookEvent $event): void
{
$pass = Pass::where('serial_number', $event->getSerialNumber())->first();
if ($pass === null) {
$event->notFound();
return;
}
if (!$event->isAuthenticatedBy($pass->getAuthToken())) {
$event->notAuthorized();
return;
}
$updatedAt = $pass->updated_at->toDateTimeImmutable();
if ($event->getUpdatedSince() !== null && $updatedAt <= $event->getUpdatedSince()) {
$event->notModified();
return;
}
$event->setPassbook($pass->toPassbook(), $updatedAt);
}
}
Since Laravel 11, listeners in app/Listeners are auto-discovered — no manual registration is required.
The compiled pass is returned to the device as application/vnd.apple.pkpass with a Last-Modified header, and 304 Not Modified is honored via the If-Modified-Since request header.
Credits
This package has been developed by LauLaman.