Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Mobile Pass Laravel Package

spatie/laravel-mobile-pass

Generate Apple Wallet and Google Wallet passes—boarding passes, event tickets, coupons, store cards, membership and gift cards—and push live updates so issued passes always stay up to date on your users' devices.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish.

composer require spatie/laravel-mobile-pass
php artisan vendor:publish --tag="mobile-pass-config"
php artisan vendor:publish --tag="mobile-pass-migrations"
php artisan migrate

The package ships a mobile_passes table plus supporting tables (Google events, Apple device registrations). If you're upgrading rather than installing fresh, read UPGRADING.md — v1.0.5 shipped a required migration for routing Apple webservice lookups by pass_serial.

2. Configure credentials in config/mobile-pass.php (env-driven).

  • Apple: Pass Type Identifier, Team Identifier, and the .p12 signing certificate + password. Apple validates the certificate at build time.
  • Google: issuer ID, service account email, and private key from a Google Wallet service account with the Wallet Object API enabled.
  • mobile-pass.apple.webservice.host must be a real HTTPS host. Apple rejects passes whose webServiceURL isn't HTTPS, and the package now throws InvalidConfig::webserviceHostMustBeHttps rather than silently generating a broken pass. Local dev needs valet share, ngrok, or similar.

3. Read in this order: docs/basic-usage/generating-your-first-pass.md → the config file → the demo repo (spatie/laravel-mobile-pass-demo), which has runnable examples of every pass type and live push updates. The demo is the fastest way to see the shape of a complete integration.

4. First use case: an event ticket. Build the pass, persist it, and hand the user a wallet link.

use Spatie\LaravelMobilePass\Builders\Apple\EventTicketPassBuilder;

$pass = EventTicketPassBuilder::make()
    ->setEventName($event->name)
    ->setLocation($event->venue_address)
    ->setDate($event->starts_at)
    ->save();

return view('tickets.show', [
    'addToWalletUrl' => $pass->addToWalletUrl(),
]);

addToWalletUrl() is unified across platforms — it returns the signed Apple .pkpass route or the Google save URL. Since v1.1.1 the Apple download is served Content-Disposition: inline, so Safari opens the Wallet preview directly instead of dumping a file.

Implementation Patterns

Build passes in an action class, not a controller. Builders are mutable and configuration-heavy; you'll re-run the same builder whenever the underlying record changes. Put it behind something like App\Actions\BuildEventTicketPass so generation and re-generation share one code path.

Generate on domain events, not on page views. An OrderPaid listener or IssueTicketPass job is the right trigger. Signing a .pkpass is CPU work (zip + PKCS#7 signing); don't do it inside a Blade render. Persist the MobilePass and let the page link to the stored pass.

Relate passes back to your models. v1.0.5 added a reverse relationship on MobilePass. Keep that relation and use it to answer "does this user already have a pass for this event?" before generating a duplicate.

Treat the pass as a projection of your data. The update path is the whole point of the package: mutate the pass, save it, and MobilePass::boot() dispatches a PushPassUpdateJob (platform-aware — Apple push + Google PATCH via NotifyGoogleOfPassUpdateAction). Set MOBILE_PASS_QUEUE_CONNECTION so that save doesn't block the request. Use $pass->expire() for refunds and cancelled events — it's unified across both platforms.

Split Google classes from Google objects. A class describes the campaign/event; an object is a per-user instance. Classes are shared, so create them in a seeder or artisan command, not per request:

EventTicketPassClass::make('beatles-shea-1965')
    ->setEventName('The Beatles | Live at Shea')
    ->addLocation(40.7569, -73.8458)
    ->addLink('https://fabfour.example.com', 'Official site')
    ->addTextModule('Doors', 'Doors open at 18:30')
    ->save();

Objects then reference the class ID. Reuse the class ID on every object build for that event.

Use the Google callbacks for lifecycle, not polling. The /mobile-pass/google/callbacks endpoint fires GoogleMobilePassSaved and GoogleMobilePassRemoved. Listen to them to update CRM state, trigger welcome emails, or increment a "wallet installs" metric. Pair with $pass->isCurrentlySavedToGoogleWallet() and the $pass->googleEvents relation to render "Add to Wallet" vs. "View in Wallet" instead of a button that does nothing useful.

Handle i18n per user. Apple loc support (1.7.1) and Google's language payload (1.0.2, 1.7.0) mean pass text is localizable. Set the locale from the recipient's profile at build time — this is a build-time decision, not a render-time one, so it belongs in the action class.

Images: Apple local, Google remote. Apple builders take local paths (setStripImage, setThumbnailImage, and since 1.4.0 setBackgroundImage), with @2x/@3x densities bundled into the .pkpass. Remote variants exist (setRemoteBackgroundImage, and remote image support landed in 1.1.0). For Google, Image::fromUrl() with a publicly reachable HTTPS URL is the supported path — local-path hosting on the Google side is deferred, so don't build a workflow that depends on it.

Test with Http::fake(). The Google side talks HTTP; fake it in feature tests and assert on the outgoing payload rather than hitting the Wallet API. Build passes inside a job so tests don't accidentally sign certificates during a request test.

Gotchas and Tips

  • Nothing updates until the user actually installs the pass. PushPassUpdateJob reaches devices that registered with your webServiceURL. Generating a link is not registration. If you're debugging "updates don't arrive," check the Apple device-registration rows and the Google object state before touching your queue.
  • Certificate expiry is the number one silent failure. Apple signing certs are time-limited; renew proactively. v1.0.3 hashes certificate contents into the cached temp filename, so rotating a cert on a live server no longer leaves you serving a stale cached artifact — but restart your queue workers after a rotation either way.
  • Google class fields are effectively immutable. Changing certain class properties after creation returns a 400 from Google. Version your class IDs (event-123-v2) instead of mutating a live class.
  • Callback verification uses ECv2SigningOnly (v1.2.0). Automatic JWKS fetching for the callback signing key is not implemented — plan to source and rotate Google's public keys yourself.
  • Custom model/table: overriding mobile-pass.models.mobile_pass is honored throughout as of 1.2.1, but the Apple pass builder and the check-for-updates request were the last holdouts. If you're pinned below 1.2.1 and use a custom model, expect inconsistency.
  • Watch for update loops. The package reacts to MobilePass saves. If you also touch the pass inside your own observers/listeners (e.g. in a GoogleMobilePassSaved handler), you can trigger repeated push jobs. Mutate the pass from one place only.
  • "0" was historically dropped. v1.8.1 fixed serialization of falsy scalars like "0" in Apple entities — relevant if you show balances, counts, or seat numbers that can legitimately be zero. Upgrade if you're below 1.8.1.
  • Long text belongs in back fields. Apple enforces strict limits on primary/secondary/auxiliary fields, and StoreCard/Coupon backFields emission was broken until 1.0.1. Put terms, instructions, and URLs in back fields.
  • Round-trip your classes after upgrades. find()/all() re-hydration has had bugs (e.g. timeStyle fixed in 1.0.4; module hydration added in 1.3.0). A quick find($id)->save() smoke test after each bump catches serialization regressions early.
  • Prune google_events. The callbacks table grows unboundedly; schedule a prune job alongside your other housekeeping.
  • The package is a fast mover (1.x releases monthly, mostly additive). Pin the minor version, read the changelog before bumping, and keep your builder logic isolated in action classes so upstream API additions (new field setters, new modules) are a one-file change rather than a codebase-wide refactor.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ably/pubsub-server
mstfkhazaal/defender
mfonte/base62x
karnoweb/metapilot
tobya/bcsapiwrapper
swalker2/cpanel
vish4395/laravel-translation-manager
morningtrain/economic
teksite/systeminfo-laravel
atomcms/voting
run6/weather
siokas/laravelembeddirectives
adamlien/pkuuids
gilsonsouza/lara-doc-generator
manelgavalda/enrollment_mobile
unabomber/timezone
delatbabel/contacts
astritzeqiri/g-recaptcha
kevinongko/laravel-genode
developeruz/laravel-analytics