spatie/laravel-mobile-pass
Generate Apple Wallet and Google Wallet passes for boarding passes, event tickets, coupons, store cards, membership cards, and gift cards—with push updates to keep issued passes current on users' devices.
Install and publish. The package is a normal Laravel package: composer require spatie/laravel-mobile-pass, then publish the config and migrations (vendor:publish --tag="mobile-pass-config" and --tag="mobile-pass-migrations") and run php artisan migrate. The migration creates the mobile_passes table plus the Google event bookkeeping tables used by the callback endpoint.
Where to look first. Three places, in this order:
config/mobile-pass.php — it is the map of everything you will actually need to change. It holds the Apple block (certificate path + password, team identifier, pass type identifier, and webservice.host), the Google block (issuer ID, service-account credentials), the models.mobile_pass override, and the queue connection used for update pushes.basic-usage/generating-your-first-pass — the README is intentionally a teaser; the real API surface lives there.spatie/laravel-mobile-pass-demo — a working app that generates all five pass types and pushes a live update. When a pass silently fails to install, diffing against the demo is faster than reading Apple's spec.Credentials are the real setup cost. Apple requires a Pass Type ID with its own certificate (.p12 + password) and a Team ID; Google requires a Wallet API issuer account with the service account added as a user. Budget an afternoon for provisioning and almost none for code. Apple passes also cannot be validated in the iOS Simulator — you need a real device to confirm the .pkpass installs.
First use case. An event ticket is the best smoke test because it exercises fields, images, dates, and updates all at once:
use Spatie\LaravelMobilePass\Builders\Apple\EventTicketPassBuilder;
$pass = EventTicketPassBuilder::make()
->setEventName('Laracon Europe 2026')
->setHolderName($order->user->name)
->setStartDate($order->event->starts_at)
->setEndDate($order->event->ends_at)
->setLocation($order->event->venue_name, $order->event->lat, $order->event->lng)
->save();
return $pass->addToWalletUrl(); // signed .pkpass download
addToWalletUrl() is deliberately unified across platforms, so the same "Add to Wallet" button in your Blade view works for both. Google passes are built with the equivalent builder under Spatie\LaravelMobilePass\Builders\Google\….
Model your pass as a side effect of a domain event, not as a resource. The pattern that holds up is: an order/payment/registration is the source of truth, and a CreateMobilePass action is dispatched from a listener. Store the resulting mobile_passes foreign key on your order row so you can re-issue, revoke, or push later. Builders are fluent and throwaway; the saved MobilePass model is the durable artifact.
One class per real-world thing, one object per person (Google). Google Wallet separates a class (the template — event, venue, branding) from an object (the individual attendee's pass). Building the class once per event and only creating objects on purchase avoids hitting quota and keeps branding consistent:
EventTicketPassClass::make('laracon-eu-2026')
->setEventName('Laracon Europe 2026')
->addLocation(52.3676, 4.9041)
->addLink('https://laracon.eu', 'Event site')
->save();
The 1.3.0/1.6.0 additions — locations, links, text modules, image modules — exist on both classes and objects and hydrate back through find()/all(), so treat the class as config-as-code you can re-run idempotently in a seeder.
Wire updates to model saves, but keep them off the request path. MobilePass::boot() is platform-aware and dispatches PushPassUpdateJob when the pass changes, so the idiomatic flow is simply $pass->update([...]). Set MOBILE_PASS_QUEUE_CONNECTION to a real queue in production — otherwise a customer-facing request blocks on Apple's and Google's APIs. Use $pass->expire() for the unified "void this pass" case (refund, cancelled event, revoked membership); it handles both platforms.
Use the Google callbacks as a state machine, not a log. The /mobile-pass/google/callbacks endpoint fires GoogleMobilePassSaved / GoogleMobilePassRemoved and populates $pass->googleEvents. The practical use is gating: only push updates to users who actually have the pass installed ($pass->isCurrentlySavedToGoogleWallet()), and treat "removed" as a signal to stop notifying rather than deleting your record.
Testing and local workflows. Wrap builder calls behind your own action class and inject it, so feature tests can assert "a pass was created for this order" without touching certificates. Use Queue::fake() to assert the update job is dispatched rather than asserting the HTTP payload. Build the pass payload in a test and snapshot the JSON — most regressions in this package's history are payload-shaped (missing backFields, dropped "0" values, missing timeStyle), and a snapshot catches them cheaply.
InvalidConfig::webserviceHostMustBeHttps instead of emitting a pass Apple will reject. For local development that means a TLS tunnel (Expose, ngrok, Valet share) — localhost will not cut it, and the failure mode on-device is silent.mobile-pass.apple.pass_type_identifier must match the certificate's, and the certificate must be current. If the .pkpass downloads but Wallet refuses it, check that before checking your field definitions.webServiceURL; the package routes those lookups by pass_serial (added in 1.0.5 along with a required migration — read UPGRADING.md, don't skip it on an existing install). A pass that never updates is usually a registration problem, not a job problem.MOBILE_PASS_QUEUE_CONNECTION and a worker.mobile-pass.models.mobile_pass works across the builder and the check-for-updates request as of 1.2.1 — you must extend the package model and add the reverse relations yourself, and older versions silently ignored the override in some paths.Image::fromUrl() for Google; local-path hosting on Google objects is explicitly deferred, so a public_path() image that works for Apple may simply not render on Google. Apple supports setStripImage, setThumbnailImage, and (since 1.4.0) setBackgroundImage / setRemoteBackgroundImage, each bundled with @2x/@3x densities — supply the densities or the pass looks blurry on modern devices."0" dropped in Apple entity serialization, fixed 1.8.1) and missing backFields on StoreCard/Coupon passes (1.0.1). If you depend on a zero balance, a zero points total, or back-of-pass terms, pin a recent version and add a test..pkpass is served Content-Disposition: inline since 0.1.1 so Safari opens the Wallet preview; non-Safari browsers download it. Don't "fix" this to attachment — it breaks the iPhone flow that most of your traffic uses.How can I help you explore Laravel packages today?