.pkpass anatomy

Inside a .pkpass file: a field-by-field tour

A working tour of the .pkpass archive: pass.json keys, the five field buckets, image sizes that must match exactly, and how the manifest gets signed.

Waya TeamUpdated 18 August 202611 min read

A .pkpass is a signed zip, nothing more

A .pkpass file is a zip archive with a different extension. Rename one to .zip, unzip it, and you get a flat folder: pass.json, manifest.json, a file called signature with no extension, and a handful of PNGs. Apple Wallet checks the signature, reads pass.json, and renders the card from that.

The minimum Apple accepts is four entries: pass.json, icon.png, manifest.json, and signature. Everything else is optional. The merchant-side card preview in the Waya dashboard ships five, adding [email protected] so the preview stays sharp on a Retina screen.

Nothing in the archive is encrypted. Anyone holding the file can read every field in pass.json, so treat it as public. That is why the customer-facing card carries an opaque per-pass serial number in its barcode rather than a phone number or an email address.

pass.json: six required keys, then a style

Six top-level keys are mandatory. formatVersion is always 1. passTypeIdentifier and teamIdentifier come from your Apple Developer account. serialNumber must be unique within your Pass Type ID. organizationName and description are plain strings, and Wallet will refuse the pass if any of the six is missing.

Then you add exactly one style key: storeCard, coupon, eventTicket, boardingPass, or generic. A loyalty card is a storeCard. The style is not cosmetic labeling: it decides the layout, which field rows are visible, and which images the phone actually uses.

The rest of the top level is presentation and behavior. Colors are CSS-style strings such as rgb(16, 185, 129), not hex, across backgroundColor, foregroundColor, and labelColor. logoText prints beside the logo. barcodes is an array, and a loyalty card typically holds one entry with format PKBarcodeFormatQR, a message, and messageEncoding set to iso-8859-1. expirationDate takes an ISO 8601 timestamp.

One key is worth extra care: organizationName is the sender name iOS shows on wallet update banners. Set it to the shop's name, not your platform's. A customer who joined a coffee shop should see the coffee shop when the card updates.

The five field buckets and what actually shows

Inside the style key sit five arrays: headerFields, primaryFields, secondaryFields, auxiliaryFields, and backFields. headerFields is the valuable one, because it stays visible when the pass is collapsed in the wallet stack. primaryFields renders large over the strip image. secondaryFields and auxiliaryFields form the rows beneath it, and backFields is the scrollable flip side.

Each field is a small dictionary: a key that is unique within the pass, a label, and a value. The value may be a string, a number, or a date, with formatting keys such as dateStyle, numberStyle, and currencyCode. Optional extras include textAlignment (PKTextAlignmentLeft, PKTextAlignmentRight) and attributedValue, which accepts a small HTML anchor so a back field can be tappable.

A concrete example. Waya's stamp card leaves primaryFields and secondaryFields empty on purpose, because the stamp grid is drawn into the strip image instead of composed from text fields. The header carries the reward balance, plus an optional counter rendered as 3/10 when the merchant turns it on, and the auxiliary row holds the customer name on the left and the reward on the right. A points card is simpler: POINTS in the header, TO REWARD in the auxiliary row.

changeMessage is the field property that triggers a lock-screen banner on update. Set it to "%@" and iOS substitutes the new value. The catch is that iOS only shows the banner when the value genuinely changed, so re-pushing identical content is silent, and a banner you expected but never saw is usually an unchanged string rather than a broken push.

Images: the dimensions are not suggestions

Apple rejects a pass with mismatched image dimensions and does not tell you which file was wrong. You get a vague failure to add the card, which is why any production minting pipeline should resize server-side rather than trust whatever the merchant uploaded.

The sizes for a storeCard, at 1x, 2x, and 3x: icon at 29, 58, and 87 pixels square; logo at 160x50, 320x100, and 480x150; strip at 375x123, 750x246, and 1125x369. Optional thumbnails are 90, 180, and 270 pixels square. Waya renders the strip once at 1125x369 and downsamples, so the @3x asset is the full-resolution original rather than an upscale.

Naming is literal: icon.png, [email protected], [email protected]. There is no @1.5x, no SVG, and no fallback logic you control. Ship only @2x and @3x phones will upscale it, which reads as a soft, slightly blurry logo next to a competitor who shipped all three.

One behavior catches people out. A storeCard renders strip.png and ignores thumbnail.png entirely, because thumbnails only apply to the generic and eventTicket styles. Waya hit this in August 2026 shipping gift cards: the gift artwork was packed as [email protected], nothing appeared, and the fix was moving it into the strip slot.

manifest.json and signature: the part you cannot fake

manifest.json is a flat object mapping every filename in the bundle to its SHA-1 hex digest. It covers everything except itself and the signature. It is boring to build and unforgiving if you get the byte order wrong.

signature is a detached PKCS#7 (CMS) structure computed over the exact bytes of manifest.json, DER-encoded, with no file extension. Signing needs three things: your Pass Type ID certificate, its private key, and the Apple WWDR intermediate certificate in the chain. Waya signs with a SHA-256 message digest and embeds both certificates so the chain validates on device.

Order of operations is the usual failure point. Collect the final file bytes, hash them, serialize the manifest, sign that exact string, and zip the string you signed. Pretty-printing or re-serializing the manifest after signing changes the bytes, the digest no longer matches, and the pass fails with no useful error.

The practical consequence: you cannot hand-edit a .pkpass. Change one pixel of a logo or one character of a label and you must re-hash the manifest and re-sign it. Any test workflow that involves opening the archive to tweak JSON needs a rebuild step, not a text editor.

Localization: .lproj folders, and when to skip them

Apple's built-in mechanism is a folder per language inside the archive: en.lproj/pass.strings, ar.lproj/pass.strings, each holding "KEY" = "value" lines. Field labels and values then reference those keys, and localized images can live in the same folders so a card can swap artwork per language.

Waya does not use .lproj folders, and it is worth saying why. Language is resolved at mint time from the program's setting, falling back to the request's Accept-Language header when the merchant leaves it on automatic, and the resolved strings are baked into pass.json as literals. Every pass is already unique per customer, so there is nothing to gain from shipping both languages in every archive.

Use .lproj when one pass file is handed to an international audience: an event ticket, a transit pass, an airline boarding pass. Skip it when passes are minted per customer per shop, as loyalty cards are. A barber in Riyadh needs one language, the one his customers read, and mint-time resolution keeps the bundle smaller with fewer moving parts.

The keys that make a pass live instead of static

Two top-level keys turn a static card into an updating one: webServiceURL and authenticationToken. When both are present, the phone registers itself by POSTing to /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} with its push token, authenticating with an Authorization header of the form ApplePass followed by your token.

Updates are a nudge, not a payload. You send an empty push through Apple Push Notification service to the pass topic, the phone comes back to GET /v1/passes/{passTypeIdentifier}/{serialNumber}, and your server returns a freshly signed .pkpass. That means every visible change requires a full re-mint on the server side, which is why fast signing and cached images matter more than they look.

Geofencing lives in the same file. A locations array takes up to 10 entries of latitude, longitude, and an optional relevantText, paired with maxDistance in meters. Waya writes branch coordinates here with a default radius of 150 meters, and only when the merchant has switched location reminders on for that program. iOS decides when to surface the lock-screen hint, so treat proximity relevance as a suggestion to the phone rather than a scheduled message.

If you run the shop rather than the code

None of this is homework for a merchant. It matters because it explains what a wallet loyalty card can and cannot do. Updates always come from a server re-signing a file, so a card that changes on its own means someone is minting a new one behind the scenes every time a stamp lands.

On Waya, the customer scans a QR code, fills in a first name and mobile number, and taps once to add the pass. Waya builds pass.json, resizes the images, hashes the manifest, signs it, and hosts the update web service. There is no app for the customer to install, no POS integration, and no SMS involved. Staff add stamps by scanning the card on an ordinary phone.

The free plan is 0 SAR and covers up to 100 customers and 100 wallet messages a month, then stops there. Already-enrolled customers keep their cards and keep collecting. Growth is 85 SAR a month and Premium is 149 SAR a month if you outgrow those limits. You can start on the free plan and inspect the .pkpass your own shop mints, since the archive is just a zip.

Frequently asked questions

What files are inside a .pkpass file?

A .pkpass contains pass.json, manifest.json, a file named signature, and the pass images as PNGs. pass.json holds every text field and color, manifest.json lists the SHA-1 digest of each file, and signature is a detached PKCS#7 signature over the manifest. The images use the @2x and @3x naming convention, such as icon.png, [email protected], and [email protected].

Why does Apple Wallet refuse to add my pass?

The two most common causes are an image whose pixel dimensions do not match Apple's expected size and a manifest digest that no longer matches the bytes you shipped. Apple deliberately gives no detail in the error, so check each image against the documented size first: icon at 29, 58, and 87 pixels, logo at 160x50, 320x100, and 480x150. If the images are correct, verify you signed the exact manifest string that went into the zip, with no re-serialization afterwards.

Can I edit a .pkpass file by hand?

No, not without re-signing it. Any change to pass.json or an image invalidates the SHA-1 digest recorded in manifest.json, and the PKCS#7 signature covers those digests, so the pass is rejected on device. You can unzip and read a .pkpass freely, but writing to it requires rebuilding the manifest and signing again with your Pass Type ID certificate.

Do I need an Apple Developer account to create passes?

Yes. Generating a valid .pkpass requires a Pass Type ID and its signing certificate from an Apple Developer account, plus the Apple WWDR intermediate certificate in the signing chain. There is no way to produce a pass Wallet will accept without those credentials, which is why most shops use a platform that already holds them rather than setting up certificates themselves.

Does Google Wallet use .pkpass files?

No. Google Wallet has no file format equivalent to .pkpass; you create a loyalty class and a loyalty object through the Google Wallet API, then hand the user a signed JWT behind an Add to Google Wallet link. The concepts overlap, since both describe fields, images, and barcodes, but the delivery mechanism is entirely different, and a platform supporting both maintains two separate builders.

Put Waya on your counter — free forever up to 100 customersStart Free
Start Free