laulamanapps/google-wallet

Generate Google Wallet passes from PHP

Maintainers

Package info

github.com/LauLamanApps/google-wallet

pkg:composer/laulamanapps/google-wallet

Transparency log

Statistics

Installs: 173

Dependents: 5

Suggesters: 3

Stars: 0

Open Issues: 0

v1.1.0 2026-07-13 06:27 UTC

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:

Service account setup

  1. Sign up for a Google Wallet issuer account in the Google Pay & Wallet Console and note your issuer id.
  2. Create a Google Cloud service account with the Wallet Object Issuer role and download its JSON key file.
  3. 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 / EventTicketObject
  • OfferClass / OfferObject
  • LoyaltyClass / LoyaltyObject
  • TransitClass / 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.