A .pkpass file is a zip archive. Rename it, open it, and you get a directory. Almost everything about supporting the format follows from what is in that directory.

The contents

pass.json is the pass. Type — boarding pass, event ticket, coupon, store card, generic — organisation name, description, colours, a barcode definition, and the fields displayed in each region of the layout: header, primary, secondary, auxiliary, and back.

Images. An icon and a logo, sometimes a strip, a thumbnail, a background, or a footer, each potentially at @2x and @3x densities. Which images are meaningful depends on the pass type; a boarding pass and a coupon lay out differently and use different image slots.

manifest.json is a mapping from each file in the archive to a SHA-1 hash of its contents.

signature is a detached PKCS#7 signature over the manifest, produced with a certificate issued to the pass's creator.

.lproj directories contain localised strings and images, one per language.

The structure is straightforward and each piece implies a decision for anyone reading it on a different platform.

Signatures, and what verification means here

The manifest-and-signature arrangement gives you a real integrity guarantee: hash each file, compare against the manifest, verify the signature over the manifest, and you know the archive has not been altered since it was signed.

The part that does not transfer cleanly is the trust anchor. Verifying that a pass was signed by a legitimately issued pass certificate means validating a chain up to a root that belongs to a specific platform's ecosystem — an ecosystem PassWallet is not part of.

So we are precise about the claim. We verify that the archive is internally consistent: every file matches its manifest hash, nothing has been added or swapped. A pass that fails that check is rejected, because a mismatch means the file is damaged or has been tampered with. We do not claim to have verified the issuer's identity, and we do not display anything that implies we have.

This matters because a wallet that shows a reassuring badge it has not earned is worse than one that shows nothing. The pass says which organisation issued it, and that string is in the pass, which means it is only as trustworthy as the file you received.

The barcode field, and the encoding that catches everyone

The barcode definition is where implementations most often go wrong, and there are two traps in it.

The first is that there are two fields. Older passes carry a single barcode object. Newer ones carry a barcodes array, in preferred order, so a pass can offer a two-dimensional code and a linear fallback. A reader that only looks at the singular field mishandles modern passes; one that only looks at the array mishandles old ones. Both must be read, with the array preferred.

The second is messageEncoding. Every barcode object declares the character encoding of its message, and the value is very often iso-8859-1 rather than UTF-8.

This is not decorative. The message is encoded to bytes using that charset before being placed in the symbol, and a scanner decodes it back the same way. Encode an ISO-8859-1 message as UTF-8 and any character outside ASCII produces different bytes — which means a different scanned value. For a pass with a plain alphanumeric reference this never shows up. For a pass whose message contains an accented name, it produces a code that scans as something subtly wrong.

Reading the declared encoding and honouring it is a few lines. Not reading it is a bug that appears only for some passengers, which is the worst kind.

The formats themselves are QR, PDF417, Aztec, and Code 128, and the pass specifies which. As with any stored code, rendering in the specified symbology is not optional — the same message in a different symbology is a different scan.

Relevance data we read and location data we do not act on

A pass can carry a relevant date, and locations or beacons at which it should surface.

The relevant date is useful and we use it: a pass with a date sorts and displays in a way that reflects it, and an expired one is presented as expired rather than sitting in the list looking current.

The location data we read and deliberately do not act on. Using it as intended means monitoring geofences in the background, which means background location permission, which means an app that currently sends nothing anywhere and asks for very little starts asking for a great deal. For a wallet, we do not think that trade is right, and we would rather the pass be one tap away than have it appear automatically at the cost of continuous location access.

Updates we do not fetch

A pass may include a web service URL and an authentication token, allowing a wallet to fetch an updated version when the issuer changes something — a gate, a time, a seat.

PassWallet does not implement this, and the reason is the same as for locations. It would mean the app making authenticated network requests to arbitrary issuer endpoints, which changes what the app is. We are not ruling it out; we are not doing it silently.

The practical consequence is important enough to state plainly: an imported pass is a snapshot. If your flight changes gate, the pass in the wallet will not know. The email will. Keep the original message until the trip is over.

Importing well

Import in advance, not at the gate. The worst moment to discover a problem is in a queue.

Check the pass against the source after importing. Name, date, seat or reference, and that the barcode renders. A pass can import cleanly and still be last week's version.

Keep the original file or message. Reissued passes arrive where the first one did, and the imported copy does not update itself.

Remove passes after the event. Expired items mixed with live ones make the list slower to read at exactly the moment speed matters.

Know when a pass cannot be stored at all. Rotating or dynamically validated codes — some transit passes, some event tickets — are sessions rather than values. A static copy of one has already expired by the time you present it. Where that is the case, keep the issuer's app.

What we would like to add

Localisation is the gap that bothers us most. The format carries per-language strings and images, and we currently render the pass's default. A Korean traveller with a pass containing a Korean localisation gets the English one, which is a worse experience than the format is capable of delivering, and it is entirely our doing.