All articles
// developers

The Google Wallet JWT Flow, Explained for Developers Who Know Apple's

No certificate, no manifest, no signed zip. Just a token. What actually carries over from Apple Wallet, mapped claim by claim.

Updated
The Google Wallet JWT Flow, Explained for Developers Who Know Apple's
Julio Song
11 min read

Theo shipped the Apple Wallet integration two weeks ago. The Pass Type ID certificate is in place, the web service handles APNs pushes, and boarding passes update correctly when a gate changes. Then the ask lands in a standup: add Google Wallet too, shouldn't take long, it's basically the same thing with a different logo. He opens Google's docs expecting a second certificate to generate and a second manifest to sign. What he finds instead is a JWT, a Google Cloud service account, and a save link that doesn't involve a file at all.

That reaction is common enough to be worth addressing directly, because the two platforms solve the same problem, getting a pass onto a phone, with almost nothing in common about how they do it. Apple hands you a certificate and asks you to sign a binary bundle. Google hands you a service account and asks you to sign a token. Knowing one flow in detail doesn't shortcut learning the other. It does tell you exactly which questions to ask.

Two different front doors

Apple's gate is a fee and a form. Ninety-nine dollars a year for an Apple Developer Program membership, then a self-service trip through Certificates, Identifiers & Profiles to register a Pass Type ID and download a certificate. Nobody at Apple reviews what you're planning to put in the pass. You can be signing passes within the hour.

Google's gate is a human being. Standing up a Google Wallet issuer account means signing into the Google Pay and Wallet Business Console, submitting a public business name, and agreeing to Google's terms. A new account starts in demo mode, which limits pass issuance to admins, developers, and accounts you've explicitly added as testers. Reaching every Google Wallet user takes a separate approval, and Google's own guidance puts the review at one to two business days once you submit it.

The REST API adds a second requirement on top of the issuer account: enabling the Google Wallet API inside a Google Cloud project and creating a service account there, the credential whose private key does the actual signing. An Apple Pass Type ID and a Google issuer ID are not interchangeable concepts wearing different names. One is a certificate you generate yourself in an afternoon. The other is a business relationship Google has to sign off on first.

Plan the review into your timeline rather than around it. A team that budgets a sprint for "add Google Wallet" and starts writing JWT code on day one can still lose most of that sprint waiting on an approval that never touches a line of code, since demo mode alone is enough to build and test against but not enough to hand a save link to a real customer.

The signature moves from a file to a token

A .pkpass is a signed ZIP. It contains pass.json, a manifest hashing every file in the bundle, and a PKCS#7 signature tying it all to your Pass Type ID certificate. Installing a pass means downloading that archive and letting Wallet verify the signature against Apple's trusted root.

Google Wallet has no equivalent archive. The unit you produce isn't a file at all. It's a JSON Web Token, signed with RS256 using the private key from your Google Cloud service account, the same credential that authenticates your REST API calls. There's no manifest step, because there's no bundle of separate files to hash. The claims and the pass data live inside one signed structure.

The two mechanisms are solving an identical trust problem with different shapes. Apple proves authorship by wrapping a file in a certificate chain a phone already trusts. Google proves authorship by signing a token with a key Google already knows belongs to your service account. Neither is more secure than the other. They're just built on different primitives, a binary artifact against a JSON claim set, and code that assumes one implies the other will reach for a manifest step Google Wallet simply doesn't have.

What's actually riding inside the JWT

The unsigned claims look like this before signing:

{
  "iss": "your-service-account@project.iam.gserviceaccount.com",
  "aud": "google",
  "typ": "savetowallet",
  "iat": 1757600000,
  "origins": ["https://example.com"],
  "payload": {
    "genericObjects": [{ "...": "the pass data" }]
  }
}

iss is your service account's email address rather than a device identifier or a pass type string. aud is always the literal value google. typ is always savetowallet. origins restricts which domains are allowed to trigger the save flow, the closest thing this token has to Apple's Team ID check. payload carries genericClasses and genericObjects, the actual pass content.

Sign that structure and append it to https://pay.google.com/gp/v/save/, and you have a working save link. No server-side download endpoint, no MIME type to get right, nothing for a phone to fetch. Opening the link in a mobile browser reads the token, verifies it against your service account, and creates the class and object it describes directly in the user's wallet.

One gap is worth calling out because it runs against the certificate habit. Google's own JWT reference documents iss, aud, typ, iat, origins, and payload as the claims that matter. There's no exp claim in that list, and nothing in the docs describes a validity window on the save link the way a Pass Type ID certificate carries a hard one-year expiration. Whatever key rotation discipline you want here, you're the one setting the calendar.

Two ways to hand over the same pass

Apple gives you exactly one path: build the full pass.json, sign it, serve it. Google gives you two, and picking the wrong one for your use case adds work you don't need.

The first path embeds the complete class and object definitions inside the JWT itself, images, barcode, text fields, styling, all of it. This is the only option the first time a given pass exists anywhere, since nothing about it is stored on Google's side yet. It's also the natural choice for a one-shot flow like a receipt email, where you're never going to call the REST API again for that specific pass.

The second path is lighter. If you've already created the class and object ahead of time through a REST insert call, later JWTs only need to reference them:

{
  "payload": {
    "genericObjects": [
      { "id": "issuerId.objectSuffix", "classId": "issuerId.classSuffix" }
    ]
  }
}

That's the whole payload. No fields, no styling, just the two IDs. This is the shape you want once your server already treats the Google Wallet object as a row it owns, updating it through the API as the underlying data changes and only building a fresh JWT when a user needs a new save link. Apple has nothing resembling this split. Every .pkpass you ever produce is regenerated and re-signed in full, because Apple never stores a template of the pass on its own servers for you to reference later.

A template Apple doesn't have

That reference-by-id shape works because Google Wallet passes are split into two tiers that Apple's model doesn't have at all. A genericClass is the shared template, the styling and layout every pass issued under it inherits. A genericObject is one user's instance of that template, the specific points balance or member name.

Both are identified as {issuerId}.{yourSuffix}, where issuerId is the number Google assigns once your issuer account is approved, and the suffix after the dot is whatever identifier your own system already uses. Update the class, and every object built from it picks up the change. Update one object, and only that user's pass moves.

Apple's passTypeIdentifier plays a role that looks similar at a glance, a reverse-domain string tying every pass back to your certificate, but it isn't a stored template. Apple never hosts a class-level record you can edit once and have every issued pass inherit from. Every pass.json is a complete, standalone document, every time. Google's class and object split changes how the two platforms model a pass, and it's the piece most likely to reshape your server-side schema if it was built Apple-first.

Think about where a rebrand lands on each platform. Change your loyalty program's logo or background color on Apple, and you're re-signing and redistributing every single pass you've ever issued, since there's no shared record for the new artwork to live in. Do the same on Google, and one patch to the genericClass reaches every object built from it. The tradeoff runs the other way for anything genuinely personal, a member's own barcode or points balance, where Apple's flat structure and Google's object tier behave almost identically, because that data was never going to live at the class level on either platform.

Updating without a push

Apple's update path is a signal and a fetch. Your server sends an empty APNs push to wake the device, and only then does the phone call back into your web service to ask what changed. The push carries no data. It's a doorbell, not a delivery. Miss a header on that push, an expired token, an apns-topic pointed at the wrong identifier, and APNs still reports success while the device never learns anything happened.

Google skips that round trip entirely. Your server calls patch (or update, though that one clears any field you leave out) directly on the genericObject, and Google propagates the change to the device without asking you to wake anything first. There's no registration table to maintain, no push token to keep valid, no separate fetch endpoint for the device to call. The write is the update.

That simplicity comes with a catch worth knowing before you rely on it. patch only touches the fields you send. update replaces the object outright, and any field you omit gets cleared rather than left alone. Google's own guidance recommends a get first, specifically so an update call doesn't silently wipe a field nobody meant to touch. A team porting update logic from Apple, where every re-signed pass is already a full replacement by definition, can carry that same instinct into a Google update call and lose fields that patch would have left untouched.

A silent Apple push and a silent Google patch end up in the same place: data changes, and the user doesn't necessarily notice. Neither platform treats "the pass changed" and "tell the user about it" as the same event, and Google makes that split more explicit than Apple does.

To put a banner or a lock screen alert in front of a Google Wallet user, you send a separate signal. Either an addMessage call with messageType set to textAndNotify, which adds a message to the back of the pass and pushes a notification pointing at it, or a notifyPreference field set to notifyOnUpdate on a specific field you're already patching, useful for something like a changed gate or seat. Google caps this at three notifications per pass in a 24 hour window, and the user has to have notifications enabled on their end for any of it to arrive.

Apple's version of the same opt-in lives in a different place, but it exists. A field in pass.json carries a changeMessage string, something like "Your gate has changed to %@", and only fields with one attached surface a banner. Everything else updates on the lock screen silently. Reserve the interruption for what the passenger actually needs to act on, on either platform. What differs is when you make that call: Apple bakes it into the pass definition ahead of time, while Google decides it per API call, at the moment you send the update.

What actually carries over

Almost none of the mechanics do. The signing model, the container format, the account you sign up for, and the way an update reaches a device are different at every layer. What does carry over is the discipline of treating pass content as data your server owns, separate from whatever protocol gets it onto a screen. A codebase that already keeps "what this pass should currently say" apart from "how we deliver it to Apple" can bolt a Google Wallet path onto that same data model without restructuring anything. One that wired Apple's signing directly into business logic will end up doing that separation now, under a deadline, instead of having done it already.

If you'd rather not stand up a second signing pipeline from scratch, Passmint issues and updates both Apple and Google Wallet passes from one API, so the class-versus-object split and the JWT signing happen behind a call your server already makes for Apple.

Primary sources

Common questions

Not on a fixed schedule. Google documents no exp claim on the savetowallet JWT, so there is no annual renewal clock the way there is for an Apple Pass Type ID certificate. You rotate the signing key yourself, on your own timeline, through the service account.
Yes. If you already created the class and object through the REST API, the JWT payload only needs the id and classId fields. The full definition is required only the first time, when nothing exists on Google's side yet.
No. A PATCH request changes the stored data silently. Getting a banner or lock screen alert in front of the user takes a separate step, either an addMessage call with messageType set to textAndNotify, or notifyPreference set to notifyOnUpdate on the changed field.
No. The two platforms run on separate accounts entirely. Google Wallet needs a Google Pay and Wallet Business Console issuer account and a Google Cloud service account, neither of which touches Apple's developer program.
Julio Song

Technical content writer, Passmint

Julio is a technical content writer at Passmint. He writes about Apple PassKit, the Google Wallet API, and what breaks when wallet passes meet production traffic.

More from Julio Song

Related reading

Share this article