UrsaKit cover for the guide to building and signing an Apple Wallet pass

An Apple Wallet pass is a signed ZIP archive — the .pkpass file — not a document format. Wallet does not render an invalid pass with a warning: it refuses to add it, which is why most people meet this format by staring at a pass that simply will not open. This guide builds one from the four required parts, then covers the failure modes that produce a silent rejection.

Apple also ships Pass Designer for building a pass visually, and you can use it to prototype the layout. When the pass has to be generated by a server, you build the archive yourself.

01What a .pkpass file actually is

Rename any working pass to .zip, extract it, and you will find these pieces:

FileRequiredPurpose
pass.jsonYesThe pass definition: identifiers, style, colors, barcode, fields
manifest.jsonYesSHA-1 hash of every other file in the package
signatureYesPKCS#7 detached signature of manifest.json
icon.png, icon@2x.pngYesThe small icon shown in Wallet and notifications
logo.png, strip.png, thumbnailsPer styleOptional artwork, must match Apple's sizes

The manifest is the integrity layer, and Apple's build steps keep it deliberately plain: a flat JSON object mapping each source file's path to its SHA-1 hex digest. It never contains itself or the signature, and metadata files such as .DS_Store must not be listed either.

{
  "icon.png": "6c4f1b2a4e2b7d3c9a5f8e1d0b6a3c2e4f5a6b7c",
  "logo.png": "b1d2c3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0",
  "pass.json": "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c"
}

The manifest must match the ZIP exactly

If manifest.json lists logo@2x.png and that file is missing — or if a file exists in the ZIP but is absent from the manifest — the signature validation fails and Wallet rejects the pass. Regenerate the manifest after every image change.

02Build pass.json for the five pass styles

Every pass shares a set of top-level keys and then declares exactly one style. The shared keys are formatVersion (always 1), passTypeIdentifier, serialNumber, teamIdentifier, organizationName and description. The serializer that publishes updates also needs webServiceURL and authenticationToken.

The style dictionary is one of generic, storeCard, coupon, eventTicket or boardingPass. Each style exposes a different set of field regions — a boarding pass has primaryFields, secondaryFields, auxiliaryFields and backFields in fixed positions, while a generic pass uses the same regions with different intent.

The field counts are capped. In general a pass carries up to three header fields, one primary field, four secondary fields and four auxiliary fields; a boarding pass allows two primary fields and five auxiliary fields, and coupons, store cards and generic passes with a square barcode share four secondary and auxiliary fields between them. (PassKit guide)

{
  "formatVersion": 1,
  "passTypeIdentifier": "pass.com.example.loyalty",
  "serialNumber": "member-8842",
  "teamIdentifier": "ABCDE12345",
  "organizationName": "Example Coffee",
  "description": "Example Coffee loyalty card",
  "backgroundColor": "rgb(67, 56, 202)",
  "foregroundColor": "rgb(255, 255, 255)",
  "labelColor": "rgb(224, 231, 255)",
  "storeCard": {
    "primaryFields": [
      { "key": "balance", "label": "Balance", "value": 12.5, "currencyCode": "USD" }
    ],
    "secondaryFields": [
      { "key": "tier", "label": "Tier", "value": "Gold" }
    ]
  },
  "barcodes": [
    { "format": "PKBarcodeFormatQR", "message": "member-8842", "messageEncoding": "iso-8859-1" }
  ]
}

Two details trip people up. Colors use the rgb(r, g, b) string form, not hex and not an array. And serialNumber is your identity for the pass — reusing it for a different member overwrites the wrong card when you push an update.

03Generate the manifest and the PKCS#7 signature

Signing is the step that cannot be faked. You need a Pass Type ID certificate and its private key from your Apple Developer account, plus Apple's WWDR intermediate certificate.

  1. Hash every file except the manifest and signature. Run this from inside the pass directory:
for f in $(find . -type f ! -name 'manifest.json' ! -name 'signature' -maxdepth 1 | sed 's|^\./||'); do
  printf '  "%s": "%s",\n' "$f" "$(shasum -a 1 "$f" | cut -d' ' -f1)"
done
  1. Assemble manifest.json from those lines. It must be valid JSON with no trailing comma after the last entry — the loop above emits a trailing comma on purpose, so remove it or build the file with a script.
  2. Sign the manifest, not the individual files:
openssl smime -binary -sign \
  -certfile WWDR.pem \
  -signer passcert.pem \
  -inkey passkey.pem \
  -in manifest.json \
  -out signature \
  -outform DER
  1. Zip the directory without a parent folder and without macOS metadata:
zip -r example.pkpass . -x '.*' -x '__MACOSX' -x '.*/'
  1. Verify before shipping. The signature must validate against the manifest you actually packaged:
openssl smime -verify -in signature -inform DER -content manifest.json -noverify

If that command prints Verification successful, the cryptographic half is done.

04Add and size the images Wallet expects

Wallet crops and scales the artwork you provide, so the number that matters is the space each image is allotted, measured in points. Images that do not match their allotted space are scaled and then cropped, and icon.png is required.

ImageAllotted spaceNotes
icon.png29 × 29 pointsRequired; also used on the lock screen and in Mail
logo.png160 × 50 pointsUsually kept narrower than the full space
strip.png375 × 123 points375 × 144 for store cards and coupons, 375 × 98 for event tickets
thumbnail.png90 × 90 pointsKeep the aspect ratio between 2:3 and 3:2
background.png180 × 220 pointsCropped slightly on all sides and blurred

One point is exactly one pixel on a non-Retina display and two or three pixels on a Retina display, so ship the base file plus @2x and @3x variants. (PassKit guide)

Regenerate the manifest last

Edit images, then rebuild manifest.json, then sign, then zip. Any earlier ordering leaves stale hashes in the manifest and produces a pass that looks finished but fails validation.

05Barcode rules that fail silently

The barcode is where a pass stops being a design exercise. Barcode scanners and software typically expect ISO 8859-1 (Latin-1) or Windows-1252, and Unicode is poorly supported by most systems, so a message carrying an emoji or a non-Latin name is the classic silent failure. Keep the barcode payload in ASCII and put the human-readable text in a field instead.

SymptomCauseFix
Pass adds but the barcode is blankbarcodes array missing or emptyAdd at least one entry with a valid format
Barcode shows garbled textmessage is not ISO 8859-1 or Windows-1252Keep message in ASCII and move the text into a field
No barcode on Apple WatchCode 128 is the only entry, and watchOS does not support itAdd a QR or PDF 417 entry as a fallback
Older iPhone shows no barcodeOnly the legacy barcode key is presentEmit barcodes as well; the array is used from iOS 9

Wallet supports QR, PDF 417, Aztec, Code 128, Code 39, Codabar, EAN 13 and ITF, and PassKit renders the first supported entry in the array. QR, PDF 417, Aztec and Code 128 are all valid on iOS 9 and later, so those four need no fallback. (Barcodes, PassKit guide)

Keep the payload short

Each format has a hard maximum, and real scanning conditions lower it — the guide's own example is a barcode on Apple Watch, in poor light, read by an inexpensive scanner. Test with the hardware your venue actually uses.

06Test on a device, then plan updates

Apple's documented test loop is to drop the pass onto an iPhone running in the Simulator, which shows the Add dialog if the pass is valid; run the full Add to Wallet flow on a real device before you ship to customers. AirDrop the file, or serve it from an HTTPS endpoint with the application/vnd.apple.pkpass MIME type so Safari offers the Add button. (Technical Note TN2302)

If the pass changes after issuance — a balance, a gate, a boarding time — you do not rebuild it on the device. You add webServiceURL and authenticationToken to pass.json, the device registers itself with your server and hands over a push token, and your server later sends a push notification that makes the device pull the updated pass. That is why authenticationToken must be long and random: it is the shared secret that authenticates every call to your update endpoints.

07Common errors and fixes

ErrorLikely causeFix
"Cannot add pass" with no detailManifest/signature mismatchRebuild manifest, re-sign, re-zip
Signature verifies but Wallet rejectspassTypeIdentifier does not match the certificateUse the exact Pass Type ID from the portal
Pass appears unstyledBackground/foreground colors are hex stringsUse rgb(r, g, b) strings
Update never arrivesWrong serialNumber or expired push tokenKeep serial numbers unique and stable per pass

The pattern behind almost every failure is the same: Wallet treats the pass as a signed artifact, so any change to any byte — including re-saving an image — invalidates the signature. Build a script that regenerates the manifest, signs it, and zips in one command, and you will stop debugging hashes by hand.

08Sources

Frequently asked questions

Why won't my .pkpass add to Apple Wallet?

Nine times out of ten the signature does not match the manifest, or the manifest lists a file that is not in the ZIP. Wallet rejects the pass silently in both cases.

Can I create a Wallet pass without an Apple Developer account?

No. Signing requires a Pass Type ID certificate issued from your Apple Developer account. You can build and test the JSON and images without it, but Wallet will not accept an unsigned pass.

Does the manifest use SHA-1 or SHA-256?

The manifest format uses SHA-1 hashes over each file in the package, which is separate from the SHA-1/SHA-256 discussion about Git object hashes.

Why does my barcode disappear on Apple Watch?

watchOS does not support Code 128. If Code 128 is the only entry in the barcodes array, the watch shows no barcode at all, so add a QR or PDF 417 entry as a fallback.

Why is my barcode not scanning?

The payload usually does not match what the scanner expects: barcode hardware typically reads ISO 8859-1 or Windows-1252, and Unicode is poorly supported. Keep the message in ASCII and move the readable text into a field.

Back to articles

Related articles

Related tools