All articles
// developers

The Pass Cannot Be Installed to Passbook: Every Real Cause, Checked in Order

One error covers a dozen different failures. The exact order to check pass.json, the archive, the certificate, and the signature.

Updated
The Pass Cannot Be Installed to Passbook: Every Real Cause, Checked in Order
Julio Song
6 min read

A developer we'll call Devon has rebuilt the same loyalty pass four times tonight. Same pass.json, same certificate, same zip command copied from a Stack Overflow answer with 800 upvotes. Every time, Safari downloads the file, Wallet's add screen flickers open for half a second, and then: "The pass cannot be installed to Passbook at this time." No line number, no field name, no hint about which of the dozen things that could be wrong actually is. Devon has read that sentence enough times tonight to have it memorized, and it still tells him nothing.

That's not a bug in the error message. It's the whole message, for every failure between parsing pass.json and verifying a signature. A .pkpass passes through five or six distinct checks before Wallet will add it, and a failure at any one of them produces the exact same line. The fastest way through it isn't guessing. It's checking the checks in the order Wallet runs them, so you stop at the first one that actually fails instead of re-signing a file that was never the problem.

Start with pass.json: six keys and one style key

Apple's own Wallet developer guide lists six keys every pass has to carry at the top level: formatVersion (the number 1), passTypeIdentifier, serialNumber, teamIdentifier, organizationName, and description, plus exactly one pass style key such as boardingPass, coupon, eventTicket, storeCard, or generic. Miss one and the file can still be syntactically valid JSON. A linter will tell you the brackets match. It won't tell you description is missing, and Wallet's error won't either.

teamIdentifier has to match the certificate that signs the pass, exactly. Apple's documentation says the value must match the Team ID of the signing certificate, and that field lives in the certificate's Organizational Unit, not somewhere you'd normally think to check. A pass generated against a certificate from a different Apple Developer team, even one that belongs to the same company, fails here.

A minimal, valid header looks like this:

{
  "formatVersion": 1,
  "passTypeIdentifier": "pass.com.example.loyalty",
  "serialNumber": "AA1180-2026-09-16",
  "teamIdentifier": "A1B2C3D4E5",
  "organizationName": "Example Coffee Co",
  "description": "Loyalty card",
  "storeCard": {}
}

If your file doesn't have every one of those seven lines in some form, this is where to stop looking and fix it before checking anything downstream.

Check how the archive was zipped

Even a perfect pass.json fails if the .pkpass archive itself is built wrong. Apple's packaging guide is specific that the ZIP has to contain the contents of the pass folder at the archive's root, not the folder itself. Run zip -r pass.pkpass ./mypass on a directory and you get mypass/pass.json inside the archive instead of pass.json. Wallet finds nothing where it expects pass.json to be and gives the same install error, with nothing distinguishing it from a JSON problem.

The fix is to cd into the pass directory first and zip its contents, not the directory itself:

cd mypass
zip -r ../pass.pkpass pass.json manifest.json signature icon.png icon@2x.png icon@3x.png logo.png

icon.png and icon@2x.png belong in this same check, and so does icon@3x.png on current hardware. The icon is the image Wallet shows on the lock screen and in search, and it's required in every pass style. Older iPhones and Macs only ever look for icon.png and icon@2x.png, but current iPhone hardware also looks for the @3x variant, and a pass missing it can fail to install on a current phone while installing without complaint on an older one or a Mac, the one cause on this list that isn't the same on every device.

Check the certificate: the right WWDR generation, not expired, matching the Pass Type ID

Two certificates travel with every signature: your Pass Type ID certificate and Apple's WWDR intermediate. Apple currently issues one WWDR generation that Pass Type ID certificates chain to, and an older copy of it pulled from a tutorial or a repository from before 2022 fails validation even though the download looks identical to the current one. If your signing code has bundled the same WWDR file for a year or more without re-checking it, that's worth ruling out early.

Your own Pass Type ID certificate is the other half, and it's worth two separate checks. Confirm it hasn't expired (it's valid for one year and isn't renewable, only replaceable), and confirm passTypeIdentifier in pass.json matches the identifier the certificate was actually issued for. A certificate generated for pass.com.example.gym will not sign a valid pass for pass.com.example.loyalty, and nothing in the signing step warns you before the file reaches a phone.

Check the manifest and signature: a hash that doesn't match

manifest.json lists a hash of every file in the bundle except itself and the signature. signature is a detached PKCS#7 blob computed over that manifest, using your certificate and the WWDR certificate together. Wallet recomputes every hash on install and compares it against what the manifest claims before it ever looks at the signature itself.

This is the check that catches "I only changed one thing after signing." Re-exporting logo.png at a different compression setting, touching pass.json to fix a typo, or letting a build step re-timestamp a file after the manifest was built all invalidate the signature, because the hash Wallet computes no longer matches the one on record. The fix is always the same: rebuild the manifest and re-sign, in that order, every time any file in the bundle changes, even a file that looks cosmetic.

What this error doesn't mean

A few failures get folded into this one by mistake, and none of them are actually an install failure. A hex color instead of an rgb() string doesn't block anything, Wallet's parser only accepts the rgb(R, G, B) form and silently falls back to its own default colors on anything else, so the pass installs looking like nobody styled it at all. A barcode that doesn't scan is usually an unsupported format string or the wrong text encoding, and Wallet installs the pass anyway since the barcode field is optional at the protocol level. A logo or strip image that renders as a blank rectangle is usually a missing @2x variant for that particular image, and Wallet also installs that pass, just with a gap where the art should be. If your pass actually lands in Wallet and only looks or scans wrong afterward, you're debugging a different problem than everything above, and the checks in this list won't find it.

Run through pass.json, the zip structure, the certificate, and the signature in that order, and you'll stop at whichever one is actually broken instead of re-signing a file that was fine all along. If you'd rather not own that whole chain yourself, Passmint validates and signs each pass server-side, so a bad certificate, a missing required field, or a manifest that doesn't match gets caught in your build instead of showing up as this one sentence on a customer's phone.

Primary sources

Common questions

It means Wallet rejected the .pkpass file somewhere between parsing pass.json and verifying the signature. The message is the same regardless of which step failed, so it never tells you which one did.
It might, but an expired certificate is only one of several causes. A malformed pass.json, a zip archive wrapped in a folder, a hash mismatch in the manifest, and a wrong WWDR generation all produce the identical message.
Usually a missing icon@3x.png. Current iPhone hardware looks for the 3x icon variant and rejects a pass that lacks it, while a Mac or an older iPhone that never checks for @3x installs the same file without complaint.
Yes. JSON syntax can be perfectly valid while a required key like description is missing, or the teamIdentifier does not match the certificate that signed the pass. Neither is a JSON error, so a linter will not catch either one.
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