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

Passes Laravel Package

chiiya/passes

chiiya/passes is a Laravel package for creating Apple Wallet passes. Build pass models with a familiar Eloquent-style API, add images, barcodes, and fields, then sign and serve ready-to-download .pkpass files directly from your PHP application.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and publish its configuration:

composer require chiiya/passes
php artisan vendor:publish --tag="passes-config"

config/passes.php is the first file to read. It is split into an apple and a google block, and everything the library does is driven from those credentials. Get them in place before writing any pass code, because there is no meaningful "dry run" mode — signing fails late if a certificate or key is missing.

Apple Wallet needs three things from your Apple Developer account: the Pass Type ID certificate (.p12 or .pem + password), its private key, and the Apple WWDR intermediate certificate. Point the config at their filesystem paths (or pass the contents directly, depending on the version).

Google Wallet needs a service account JSON with the Wallet Object Issuer scope enabled, plus your Issuer ID from the Google Wallet Business Console. The service account email must be granted access to that issuer.

Your first use case should be a single, boring pass — a store card or event ticket — rendered to a file on disk. Something like:

use Chiiya\Passes\Apple\PassFactory;
use Chiiya\Passes\Apple\Passes\StoreCard;

$pass = (new StoreCard)
    ->setOrganizationName('Acme Coffee')
    ->setDescription('Loyalty card')
    ->setSerialNumber('member-1234')
    ->setLogoText('Acme')
    ->setForegroundColor('rgb(255, 255, 255)')
    ->setBackgroundColor('rgb(30, 30, 30)');

$path = app(PassFactory::class)->create($pass)->save(storage_path('passes/member-1234.pkpass'));

Open the resulting .pkpass in Finder/Preview on macOS (or AirDrop it to a device) to confirm the certificate chain, images, and pass.json are all valid. Do this before you build the download endpoint — debugging a signed archive is far easier than debugging an HTTP response.

For Google, the flow is class-then-object: create a LoyaltyClass/EventTicketClass once per program, then a LoyaltyObject per user. The package gives you both, plus a SaveLink/JWT helper. Start by generating a save URL for one test object and opening it on an Android device with Wallet installed.

Implementation Patterns

Wrap the package in your own service. Do not sprinkle PassFactory calls through controllers. Create a WalletPassService (or per-domain services like LoyaltyPassService, TicketPassService) that owns serial-number generation, image resolution, and persistence. The library objects are mutable builders — they are not models, and you do not want them leaking into your domain layer.

Persist the pass data, not just the file. Apple passes with webServiceURL + authenticationToken must be re-issued identically (same serial number, same auth token) on every update, and your web service endpoints must return the latest signed .pkpass. Store the serial number, auth token, and the source data in your own table, then regenerate on demand from that record. Treat the .pkpass as a cacheable artifact with an ETag, not as the source of truth.

Use the "update" path for push notifications. When a user's balance changes, update your record and call Apple's APNs push endpoint with an empty payload to the push token registered against that serial. Your webServiceURL then serves the freshly signed pass. This is the part most people underestimate — the library signs passes; the device registration, push, and log endpoints are yours to implement. Check the docs for the exact PassKit Web Service routes; they are a small but strict contract (/v1/devices/{device}/registrations/{passType}/{serial}, etc.).

Google: separate class lifecycle from object lifecycle. Classes are long-lived and rarely change; objects change per user and per event. Keep a google_classes table or config map so your code looks up classId ({issuerId}.{suffix}) rather than constructing strings inline. Use addMessage() for notification text on updates, and setState('EXPIRED') rather than deleting objects — deletion invalidates the user's saved pass.

Template the visuals, don't hardcode them. Apple passes require icon.png, [email protected], [email protected], and typically logo.png plus @2x variants; Google wants hero/logo images by URL. Put these in a per-brand directory and resolve them through a small PassAssets resolver keyed by brand or program. This is exactly the kind of thing that gets duplicated across five controllers otherwise.

Generate in a queued job for bulk campaigns. Signing is CPU bound (zip + crypto) and the Google API is network bound. For "issue 50,000 loyalty cards" or "regenerate all passes after a program-wide change", queue it. Chunk Google API calls — the Wallet API has per-second quotas, and the package will happily let you hammer it.

Testing: assert against the generated pass.json payload (the library exposes the array/JSON representation before zipping) rather than against the binary .pkpass. For Google, bind a fake/stub HTTP client so tests never touch the real API; the Google objects serialize to plain arrays, which makes snapshot-style assertions easy. Keep a checked-in set of test certificates (self-signed or a dedicated dev Pass Type ID) so your test suite can actually sign.

Gotchas and Tips

  • Apple certificates expire yearly, and the WWDR intermediate expires too. Both will silently break issuance with a cryptic OpenSSL error. Add a scheduled check that opens the .p12/.pem, reads validTo, and alerts well before expiry. This is the single highest-value piece of glue code you can write around this package.
  • serialNumber must be unique per Pass Type ID, and Apple deduplicates by it. Reusing a serial with different content causes confusing "pass already exists" behaviour on device. Namespace your serials (loyalty-{id}, ticket-{id}).
  • authenticationToken must be at least 16 characters. Apple rejects shorter tokens at install time, not at signing time, so you find out on the device.
  • Colors are rgb(r, g, b) strings on Apple, hex on Google. Easy to mix up. Also, foreground/background contrast matters — a dark logo on a dark background is invisible and users will notice before you do.
  • Barcodes need messageEncoding: iso-8859-1 unless you set UTF-8 explicitly. Scanners on older hardware choke otherwise. Set format (PKBarcodeFormatQR, PDF417, Aztec, Code128) deliberately per program.
  • Dates on Apple passes are W3C/ISO 8601 with timezone (2026-05-29T14:00:00+02:00). Passing a Unix timestamp or a naive datetime produces a pass that installs but shows wrong times, or is rejected. Use Carbon::toIso8601String() and be explicit about the venue timezone.
  • The zip structure is strict. pass.json at the root, manifest.json with SHA-1 of every file, signature last. If you ever hand-modify or re-zip, you will break it. Always regenerate through the package rather than patching an existing .pkpass.
  • Serve with the correct MIME type. application/vnd.apple.pkpass for Apple. application/octet-stream will cause Safari to download it as a file instead of prompting Wallet, and on some iOS versions it fails entirely. Don't forget Content-Disposition: attachment; filename="pass.pkpass".
  • Google save links are signed JWTs and expire. Don't cache the JWT — cache the object and mint a fresh save link on each request. The SaveLink/JWT has a short TTL, and a stale link produces a blank or error screen with no useful diagnostics.
  • Google class IDs are {issuerId}.{suffix} and the issuer must own the class. If you switch environments, use a different suffix per environment (prod-loyalty, dev-loyalty) rather than the same class ID with different content.
  • Images are the most common cause of a rejected pass. Wrong dimensions, missing @2x, or PNGs saved as JPEGs renamed. Apple silently drops invalid images; the pass installs and looks broken. Run sips -g pixelWidth -g pixelHeight (macOS) over your assets once and record the required sizes in a comment.
  • Extension points are limited by design. The Apple pass classes map fairly directly to the PassKit JSON, so for fields the builders don't expose, check whether the underlying pass object accepts arbitrary keys before forking. The Google side is more array-driven and easier to extend with raw payloads — prefer that over subclassing the builders, which are not designed as a stable extension surface.
  • Where to look when something breaks: the error usually surfaces from ZipArchive, openssl_sign, or the Google HTTP client — not from your own code. Log the full pass.json (it contains no secrets) alongside any failure; nine times out of ten the answer is in the payload, not the stack trace.
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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels