reachweb / laravel-wallet-passes
Laravel package for generating Apple Wallet (.pkpass) and Google Wallet digital passes
Requires
- php: ^8.3
- firebase/php-jwt: ^7.0
- google/apiclient: ^2.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- fakerphp/faker: ^1.23
- larastan/larastan: ^3.0
- laravel/boost: ^2.2
- laravel/framework: ^13.0
- laravel/pail: ^1.2.5
- laravel/pint: ^1.27
- laravel/tinker: ^3.0
- mockery/mockery: ^1.6
- nunomaduro/collision: ^8.6
- pestphp/pest: ^4.4
- pestphp/pest-plugin-laravel: ^4.1
README
A Laravel package for generating, signing, and managing Apple Wallet (.pkpass) and Google Wallet digital passes behind a single, fluent API.
Requirements
- PHP 8.3+
- Laravel 12 or 13
ext-openssl(Apple pass signing)ext-zip(Apple.pkpasspackaging)
Installation
composer require reach/laravel-wallet-passes
Publish the config file:
php artisan vendor:publish --tag=wallet-passes-config
Publish and run the migrations (required for Apple pass-update web service):
php artisan vendor:publish --tag=wallet-passes-migrations php artisan migrate
Configuration
Add the following to your .env file. Only configure the providers you plan to use.
Apple Wallet
APPLE_PASS_CERTIFICATE_PATH=/path/to/pass-certificate.p12 APPLE_PASS_CERTIFICATE_PASSWORD=your-certificate-password APPLE_WWDR_CERTIFICATE_PATH=/path/to/AppleWWDRCAG4.pem APPLE_PASS_TYPE_IDENTIFIER=pass.com.example.mypass APPLE_TEAM_IDENTIFIER=ABCDE12345 APPLE_ORGANIZATION_NAME="Your Company" APPLE_WEB_SERVICE_URL=https://example.com/wallet APPLE_APNS_ENVIRONMENT=production
| Variable | Description |
|---|---|
APPLE_PASS_CERTIFICATE_PATH |
Absolute path to the .p12 Pass Type ID certificate exported from Keychain Access |
APPLE_PASS_CERTIFICATE_PASSWORD |
Password used when exporting the .p12 file |
APPLE_WWDR_CERTIFICATE_PATH |
Absolute path to Apple's WWDR G4 intermediate certificate (.pem) |
APPLE_PASS_TYPE_IDENTIFIER |
The Pass Type ID registered in your Apple Developer account (e.g. pass.com.example.mypass) |
APPLE_TEAM_IDENTIFIER |
Your 10-character Apple Developer Team ID |
APPLE_ORGANIZATION_NAME |
Displayed on the pass lock screen |
APPLE_WEB_SERVICE_URL |
Base URL for pass-update callbacks (must match the webServiceURL in your passes) |
APPLE_APNS_ENVIRONMENT |
production or sandbox (defaults to production) |
Google Wallet
GOOGLE_WALLET_SERVICE_ACCOUNT_PATH=/path/to/service-account.json GOOGLE_WALLET_ISSUER_ID=3388000000012345678 GOOGLE_WALLET_APPLICATION_NAME=MyApp
| Variable | Description |
|---|---|
GOOGLE_WALLET_SERVICE_ACCOUNT_PATH |
Absolute path to the Google Cloud service account JSON key file |
GOOGLE_WALLET_ISSUER_ID |
Your Google Wallet API Issuer ID (found in the Google Pay & Wallet Console) |
GOOGLE_WALLET_APPLICATION_NAME |
Application name sent with API requests (defaults to WalletPasses) |
Additional Config
The published config/wallet-passes.php file also supports:
'default' => env('WALLET_PASS_PROVIDER', 'apple'), // 'apple', 'google', or 'both' 'routes' => [ 'enabled' => true, // Enable Apple web service routes 'prefix' => 'wallet', // URL prefix for all package routes 'middleware' => [], // Additional middleware to apply ], 'logging' => [ 'enabled' => env('WALLET_PASS_LOGGING', false), 'channel' => env('WALLET_PASS_LOG_CHANNEL', 'stack'), ],
Usage
Using the Facade
The WalletPass facade is the main entry point. It returns pre-configured builders for each provider.
use Reach\WalletPasses\Facades\WalletPass; // Get an Apple builder $builder = WalletPass::apple(); // Get a Google builder $builder = WalletPass::google(); // Attach a Passable model (auto-populates builder fields) $builder = WalletPass::forModel($order)->apple();
Creating an Apple Wallet Pass
use Reach\WalletPasses\Facades\WalletPass; use Reach\WalletPasses\Apple\Enums\PassStyle; use Reach\WalletPasses\Apple\Enums\BarcodeFormat; $pkpass = WalletPass::apple() ->setStyle(PassStyle::EventTicket) ->setSerialNumber('E-001') ->setDescription('Concert Ticket') ->setHeader('Live Concert') ->setColors( background: '#1E3A5F', foreground: '#FFFFFF', label: '#AABBCC', ) ->setIcon(storage_path('passes/icon.png')) ->setLogo(storage_path('passes/logo.png')) ->setStrip(storage_path('passes/strip.png')) ->addHeaderField('date', 'Date', 'Jun 15, 2025') ->addPrimaryField('event', 'Event', 'Summer Music Festival') ->addSecondaryField('location', 'Location', 'Main Stage') ->addAuxiliaryField('seat', 'Seat', 'GA-042') ->addBackField('terms', 'Terms', 'No refunds.') ->setBarcode('E-001', BarcodeFormat::QR) ->addLocation(37.3349, -122.0090, 'Venue') ->setRelevantDate(new DateTime('2025-06-15T18:00:00-07:00')) ->setWebServiceUrl(config('wallet-passes.apple.web_service_url')) ->setAuthenticationToken('your-auth-token') ->generate(); // Returns binary .pkpass content
Returning a download response
// In a controller: return WalletPass::apple() ->setSerialNumber('E-001') // ... other fields ... ->download('concert-ticket.pkpass');
Creating a Google Wallet Pass
use Reach\WalletPasses\Facades\WalletPass; use Reach\WalletPasses\Apple\Enums\BarcodeFormat; // Option 1: Generate a save URL via JWT (no server-side API call) $saveUrl = WalletPass::google() ->setClassId('concert-class') ->setClassName('Live Concerts') ->setObjectId('ticket-001') ->setHeader('Summer Music Festival') ->setSubheader('Main Stage') ->setHeroImage('https://example.com/hero.png', 'Concert banner') ->setLogo('https://example.com/logo.png', 'Logo') ->setBackgroundColor('#1E3A5F') ->addField('date', 'Date', 'Jun 15, 2025') ->addField('seat', 'Seat', 'GA-042') ->addLink('https://example.com/event', 'Event Details') ->setBarcode('ticket-001', BarcodeFormat::QR) ->addLocation(37.3349, -122.0090) ->setState('ACTIVE') ->setExpiration(new DateTime('2025-06-16T00:00:00Z')) ->generateSaveUrl(); // Returns "Add to Google Wallet" URL // Option 2: Create via REST API first, then generate save URL $saveUrl = WalletPass::google() ->setClassId('concert-class') ->setObjectId('ticket-001') // ... other fields ... ->createViaApi(); // Creates class/object via API, returns save URL
Model Integration with the Passable Interface
For models that represent passes (e.g. orders, tickets, memberships), implement the Passable interface and use the HasWalletPass trait:
use Illuminate\Database\Eloquent\Model; use Reach\WalletPasses\Contracts\Passable; use Reach\WalletPasses\Traits\HasWalletPass; use Reach\WalletPasses\DTOs\PassField; use Reach\WalletPasses\DTOs\PassLocation; class Order extends Model implements Passable { use HasWalletPass; public function getPassSerialNumber(): string { return 'ORDER-' . $this->id; } public function getPassDescription(): string { return 'Order #' . $this->id; } public function getPassFields(): array { return [ new PassField('item', 'Item', $this->item_name), new PassField('qty', 'Quantity', (string) $this->quantity), ]; } public function getPassBarcodeMessage(): string { return 'ORDER-' . $this->id; } public function getPassLocations(): array { return [ new PassLocation( latitude: $this->store_lat, longitude: $this->store_lng, relevantText: 'Pickup location', ), ]; } }
Then use the trait methods:
$order = Order::find(1); // Generate an Apple .pkpass binary $pkpass = $order->generateApplePass(); // Return a download response return $order->getApplePassResponse('order-pass.pkpass'); // Generate a Google Wallet save URL $url = $order->generateGooglePassUrl(); // Generate a signed download URL (expires in 24 hours) $downloadUrl = $order->getPassDownloadUrl(); // Notify registered devices of a pass update (Apple) $notified = $order->notifyPassUpdate();
Apple Wallet Setup Guide
1. Create a Pass Type ID
- Go to Apple Developer > Certificates, Identifiers & Profiles
- Under Identifiers, click + and select Pass Type IDs
- Enter a description and an identifier (e.g.
pass.com.yourcompany.mypass) - Register the identifier
2. Create and Export the Pass Certificate
- In the Identifiers list, select your Pass Type ID and click Create Certificate
- Follow the prompts to create a Certificate Signing Request (CSR) via Keychain Access
- Upload the CSR and download the certificate (
.cerfile) - Double-click the
.certo install it in Keychain Access - In Keychain Access, find the certificate under My Certificates, right-click, and select Export
- Save as a
.p12file with a password - Place the
.p12file in a secure location on your server (outside the web root) - Set
APPLE_PASS_CERTIFICATE_PATHandAPPLE_PASS_CERTIFICATE_PASSWORDin.env
3. Download the WWDR Intermediate Certificate
- Download the Apple Worldwide Developer Relations (WWDR) G4 certificate from Apple PKI
- Convert to PEM if needed:
openssl x509 -inform der -in AppleWWDRCAG4.cer -out AppleWWDRCAG4.pem
- Place the
.pemfile alongside your.p12and setAPPLE_WWDR_CERTIFICATE_PATH
4. Find Your Team Identifier
Your 10-character Team ID is shown at the top right of the Apple Developer Membership page. Set it as APPLE_TEAM_IDENTIFIER.
Google Wallet Setup Guide
1. Create a Google Cloud Project
- Go to Google Cloud Console
- Create a new project or select an existing one
- Enable the Google Wallet API under APIs & Services > Library
2. Create a Service Account
- Go to IAM & Admin > Service Accounts
- Click Create Service Account
- Give it a name and grant no specific roles (the Wallet API uses its own permissions)
- On the service account detail page, go to Keys > Add Key > Create new key
- Select JSON format and download the key file
- Place the JSON file in a secure location and set
GOOGLE_WALLET_SERVICE_ACCOUNT_PATH
3. Set Up a Wallet API Issuer Account
- Go to the Google Pay & Wallet Console
- Sign up for the Wallet API and create an Issuer account
- Copy your Issuer ID (a numeric string) and set
GOOGLE_WALLET_ISSUER_ID - Under Manage > API Access, add your service account email as a user with Developer permissions
4. Demo Mode
New Wallet API issuer accounts start in demo mode. Passes can only be saved by users added as test users in the Google Pay & Wallet Console. Apply for production access once your integration is ready.
Apple Web Service (Pass Updates)
This package implements Apple's web service protocol for pass updates. When enabled, the following endpoints are registered automatically:
| Method | Endpoint | Description |
|---|---|---|
| POST | {prefix}/v1/devices/{deviceId}/registrations/{passTypeId}/{serial} |
Register a device for push updates |
| DELETE | {prefix}/v1/devices/{deviceId}/registrations/{passTypeId}/{serial} |
Unregister a device |
| GET | {prefix}/v1/devices/{deviceId}/registrations/{passTypeId} |
List serial numbers for a device |
| GET | {prefix}/v1/passes/{passTypeId}/{serial} |
Get the latest version of a pass |
| POST | {prefix}/v1/log |
Receive log messages from Apple Wallet |
| GET | {prefix}/download/{serial} |
Download a pass via signed URL |
Implementing the PassRetriever
When Apple requests the latest version of a pass, the package needs to know how to build it. Create a class implementing PassRetriever and register it in your config:
// app/Services/MyPassRetriever.php use Reach\WalletPasses\Apple\ApplePassBuilder; use Reach\WalletPasses\Contracts\PassRetriever; use Reach\WalletPasses\Facades\WalletPass; class MyPassRetriever implements PassRetriever { public function retrieve(string $passTypeIdentifier, string $serialNumber): ?ApplePassBuilder { $order = Order::where('serial', $serialNumber)->first(); if (! $order) { return null; // Returns 404 to Apple } return WalletPass::forModel($order)->apple() ->setIcon(storage_path('passes/icon.png')) ->setAuthenticationToken($order->pass_token); } }
Register it in config/wallet-passes.php:
'apple' => [ // ... 'pass_retrieval_handler' => \App\Services\MyPassRetriever::class, ],
Sending Pass Update Notifications
When a pass's data changes, notify registered devices to fetch the updated version:
use Reach\WalletPasses\Apple\ApplePassUpdater; $updater = new ApplePassUpdater(config('wallet-passes.apple')); // Notify devices registered for a specific pass $count = $updater->notifyDevices('pass.com.example.mypass', 'ORDER-42'); // Notify all devices for a pass type $count = $updater->notifyAllDevices('pass.com.example.mypass');
Or via the HasWalletPass trait:
$order->notifyPassUpdate();
Events
The package dispatches the following events. Listen for them in your EventServiceProvider or with Event::listen():
| Event | Properties | When |
|---|---|---|
PassCreated |
provider, serialNumber, passTypeIdentifier |
A pass is generated |
PassUpdated |
provider, serialNumber, passTypeIdentifier, updateType |
A pass is updated |
PassRetrieved |
serialNumber, deviceLibraryIdentifier |
A device fetches the latest pass |
DeviceRegistered |
deviceLibraryIdentifier, serialNumber, pushToken |
A device registers for updates |
DeviceUnregistered |
deviceLibraryIdentifier, serialNumber |
A device unregisters |
All events live under Reach\WalletPasses\Events\.
use Reach\WalletPasses\Events\DeviceRegistered; Event::listen(DeviceRegistered::class, function (DeviceRegistered $event) { Log::info("Device {$event->deviceLibraryIdentifier} registered for pass {$event->serialNumber}"); });
Exceptions
All package exceptions extend Reach\WalletPasses\Exceptions\WalletPassException, so you can catch every package error with one type:
| Exception | Thrown When |
|---|---|
CertificateNotFoundException |
.p12 or WWDR certificate file is missing or unreadable |
PassSigningException |
OpenSSL signing fails (bad cert, wrong password, etc.) |
InvalidPassDataException |
Required fields are missing when calling generate() or generateJwt() |
GoogleApiException |
Google Wallet REST API returns an error |
use Reach\WalletPasses\Exceptions\WalletPassException; use Reach\WalletPasses\Exceptions\CertificateNotFoundException; try { $pkpass = WalletPass::apple()->setSerialNumber('001')->generate(); } catch (CertificateNotFoundException $e) { // Handle missing certificate } catch (WalletPassException $e) { // Handle any other package error }
Troubleshooting
Apple Wallet
"Invalid certificate" or signing errors
- Ensure the
.p12was exported from a Pass Type ID certificate, not a development/distribution certificate - Verify the password matches what was set during export
- Make sure you're using the WWDR G4 certificate (not G1/G2/G3)
- Check that
ext-opensslis installed:php -m | grep openssl
Pass doesn't appear on device
- The icon image is required. Ensure
setIcon()is called with a valid PNG path - Serial number, pass type identifier, and team identifier are all required
- Check that the
.pkpassMIME type isapplication/vnd.apple.pkpass
Pass updates not working
- Verify
webServiceURLin the pass matches yourAPPLE_WEB_SERVICE_URL - The authentication token must be at least 16 characters
- Ensure routes are enabled in config (
wallet-passes.routes.enabled) - Check that your
PassRetrieverimplementation is registered in config - For sandbox testing, set
APPLE_APNS_ENVIRONMENT=sandbox
Google Wallet
"Permission denied" API errors
- Ensure the service account email has Developer access in the Google Pay & Wallet Console
- Verify the JSON key file path is correct and readable
"Class not found" errors
- When using
generateSaveUrl(), the class and object are created inline via JWT -- no API call needed - When using
createViaApi(), the class is created first; ensure your issuer ID is correct
Passes only visible to test users
- New issuer accounts are in demo mode. Add test user emails in the Google Pay & Wallet Console
- Apply for production access to make passes available to all users
General
"Class not found" PHP errors
- Run
composer dump-autoloadafter installation - Ensure the service provider is auto-discovered (check
composer.jsonextra.laravel.providers)
Missing migrations
- Run
php artisan vendor:publish --tag=wallet-passes-migrationsthenphp artisan migrate
Testing
The package includes a comprehensive Pest test suite. To run the tests:
php artisan test --compact
Test certificates and Google service account fixtures are generated on the fly -- no real credentials are needed to run the test suite.
License
MIT -- see LICENSE.