laulamanapps / google-wallet
Generate Google Wallet passes from PHP
Requires
- php: ^8.1
- ext-json: *
- ext-openssl: *
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.0|^11.0
Suggests
- ext-curl: Required to use the Google Wallet REST API client for updating issued passes
This package is auto-updated.
Last update: 2026-07-13 06:29:46 UTC
README
Generate Google Wallet passes from PHP. The Google counterpart of laulamanapps/apple-passbook.
Passes are distributed via "Add to Google Wallet" links: an RS256-signed JWT whose payload
embeds the pass classes and objects (a so-called "fat JWT"). Because the classes and objects
travel inside the JWT, no Google Wallet REST API call is needed — the only dependency is
ext-openssl for signing.
Installation
composer require laulamanapps/google-wallet
Or use one of the framework integrations, which wire the service account and save-URL factory from configuration:
- Symfony: laulamanapps/google-wallet-symfony
- Laravel: laulamanapps/google-wallet-laravel
Service account setup
- Sign up for a Google Wallet issuer account in the Google Pay & Wallet Console and note your issuer id.
- Create a Google Cloud service account with the Wallet Object Issuer role and download its JSON key file.
- Link the service account to your issuer account in the console.
Quickstart
use LauLamanApps\GoogleWallet\Object\Barcode; use LauLamanApps\GoogleWallet\Object\BarcodeType; use LauLamanApps\GoogleWallet\Object\GenericClass; use LauLamanApps\GoogleWallet\Object\GenericObject; use LauLamanApps\GoogleWallet\Object\Image; use LauLamanApps\GoogleWallet\Object\TextModule; use LauLamanApps\GoogleWallet\PassPayload; use LauLamanApps\GoogleWallet\SaveUrlFactory; use LauLamanApps\GoogleWallet\ServiceAccount; $serviceAccount = ServiceAccount::fromJsonFile('/path/to/service-account.json'); // Ids are '<issuerId>.<identifier>' $object = new GenericObject('3388000000012345678.member-0001', '3388000000012345678.membership'); $object->setCardTitle('ACME Membership'); $object->setHeader('John Doe'); $object->setSubheader('Gold member'); $object->setHexBackgroundColor('#4285f4'); $object->setLogo(Image::fromUri('https://example.com/logo.png', 'ACME logo')); $object->setBarcode(new Barcode(BarcodeType::QrCode, 'member-0001')); $object->addTextModule(new TextModule('Member since', '2020')); $payload = new PassPayload(); $payload->addGenericClass(new GenericClass('3388000000012345678.membership')); $payload->addGenericObject($object); $factory = new SaveUrlFactory($serviceAccount, ['https://example.com']); $saveUrl = $factory->create($payload); // https://pay.google.com/gp/v/save/eyJhbGciOiJSUzI1NiIs...
Render the url as an Add to Google Wallet button and you are done.
Pass types
Next to GenericClass/GenericObject the same pattern is available for:
EventTicketClass/EventTicketObjectOfferClass/OfferObjectLoyaltyClass/LoyaltyObjectTransitClass/TransitObject
Add them to the PassPayload with the matching add*Class()/add*Object() methods.
Location-triggered notifications
Google Wallet shows a Nearby Passes notification when a pass holder is close to one of the pass's merchant locations (the user must have granted always-on location access; Google controls the notification text and timing). Add up to 10 locations per object:
$object->addMerchantLocation(52.3676, 4.9041);
Updating passes
Once a pass is saved to a wallet you can change it through the Google Wallet REST API. Unlike
Apple Passbook there are no push tokens or APNs involved: Google pushes updates to devices
automatically after an API call. The API client uses ext-curl by default (or bring your own
HttpClient implementation); an OAuth2 access token is obtained and cached for you via the
service-account JWT-bearer grant.
use LauLamanApps\GoogleWallet\Api\AccessTokenProvider; use LauLamanApps\GoogleWallet\Api\Message; use LauLamanApps\GoogleWallet\Api\WalletApiClient; use LauLamanApps\GoogleWallet\Object\GenericObject; use LauLamanApps\GoogleWallet\Object\State; use LauLamanApps\GoogleWallet\ServiceAccount; $serviceAccount = ServiceAccount::fromJsonFile('/path/to/service-account.json'); $client = new WalletApiClient(new AccessTokenProvider($serviceAccount)); // Partially update an issued pass (PATCH; use updateObject() for a full PUT) $object = new GenericObject('3388000000012345678.member-0001', '3388000000012345678.membership'); $object->setHeader('John Doe'); $object->setState(State::Active); $client->patchObject($object); // Notify pass holders $client->addMessage($object, new Message('Membership renewed', 'Your membership is extended until 2027.')); // Fetch the current server-side state of a pass $data = $client->getObject('genericObject', '3388000000012345678.member-0001');
The same pattern works for classes (insertClass(), updateClass(), patchClass()) and for
all pass types. Errors surface as LauLamanApps\GoogleWallet\Exception\ApiException.
Receiving save/delete callbacks
Google can notify you whenever a user saves or deletes a pass. Set a callback url on the pass class (available on all five class types) and Google will POST a signed message to that endpoint on every save and delete:
$class = new GenericClass('3388000000012345678.membership'); $class->setCallbackUrl('https://example.com/wallet/callback'); // must be https://
Google signs every callback with the ECv2SigningOnly scheme (ECDSA over NIST P-256 with
SHA-256, sender GooglePayPasses, your issuer id as recipient). Verify the raw request
body with CallbackVerifier; it fetches Google's root signing keys, checks the
intermediate signing key against them, checks the message signature against the
intermediate key and rejects expired keys and messages:
use LauLamanApps\GoogleWallet\Callback\CallbackVerifier; use LauLamanApps\GoogleWallet\Callback\EventType; use LauLamanApps\GoogleWallet\Callback\GoogleKeyProvider; use LauLamanApps\GoogleWallet\Exception\CallbackException; $verifier = new CallbackVerifier('3388000000012345678', GoogleKeyProvider::production()); try { $event = $verifier->verify($rawPostBody); } catch (CallbackException $exception) { // Respond with a 4xx and do NOT act on the payload. } $event->getClassId(); // '3388000000012345678.membership' $event->getObjectId(); // '3388000000012345678.member-0001' $event->getEventType(); // EventType::Save or EventType::Delete $event->getNonce(); // callbacks are delivered at least once; deduplicate on the nonce
Warning
Never act on a callback without running it through CallbackVerifier. The endpoint is
public and anyone can POST to it; only the signature chain proves the request came from
Google. verify() never returns unverified data — every failure throws a
CallbackException.
Callbacks are best-effort and may arrive more than once: use getNonce() to deduplicate.
Reuse one CallbackVerifier/GoogleKeyProvider instance where possible — the root signing
keys are cached per GoogleKeyProvider instance. GoogleKeyProvider::test() points at
Google's test root keys for integration testing.
Signing a JWT yourself
SaveUrlFactory covers the common case. If you need the raw JWT, use JwtSigner directly:
use LauLamanApps\GoogleWallet\JwtSigner; $signer = new JwtSigner($serviceAccount); $jwt = $signer->sign([ 'iss' => $serviceAccount->getClientEmail(), 'aud' => 'google', 'typ' => 'savetowallet', 'iat' => time(), 'origins' => ['https://example.com'], 'payload' => $payload->toArray(), ]);
Credits
License
The MIT License (MIT). Please see License File for more information.