Passmint
TemplatesDocsPricingBlog
Log inGet started
Passmint

Apple and Google Wallet passes from one API. Built for people who ship.

Product
  • Pass Designer
  • Developers
  • Distribution
  • Analytics
  • Templates
  • Pricing
Developers
  • Documentation
  • API reference
  • Node SDK
  • Webhooks
Company
  • Changelog
  • About
  • Contact
  • Support
  • Security
  • Terms
  • Privacy
Social
  • GitHub
  • X

© 2026 Passmint. Built for indie makers.

Apple Wallet & Google Wallet, one API.

Teardown · byte by byte

Your boarding pass is a zip file. Let's take it apart.

One boarding pass, opened file by file - the JSON, the images, the checksums, the signature, and the two lines that make it update itself.

Sample download coming with the signed build - every value on this page is still real.

Passmint Air
LONDON
LHR
→
SAN FRANCISCO
SFO
Boarding
07:45
Seat
12F
↓Start scrolling

The pass in your Apple Wallet - your boarding pass, your gym card, that coffee loyalty card - is a zip file. Inside it: one JSON file that describes everything you see, a folder of PNGs, a manifest of checksums, and a cryptographic signature from Apple's chain. That's the whole format.

No app, no runtime, no code of any kind. Wallet reads the JSON, lays out the card, and treats the signature as the only proof it needs. This page takes one apart - the boarding pass on screen, rendered from the very files we're about to open. Every byte gets explained.

01

It's a zip

There is no magic in the container. Take any .pkpass - this sample, or a boarding pass saved from an airline email - rename it to .zip, and double-click. It opens. The extension is a courtesy; the bytes are a standard ZIP archive, and unzip -l sample.pkpass works on it untouched.

Seven files. No code, no HTML, no tracking pixel - Wallet renders everything from declarative JSON. pass.json is the whole story: two kilobytes of JSON describing every pixel Wallet will draw. The images come next - icon.png, icon@2x.png, and icon@3x.png are the same 29-point icon shipped at three pixel densities so every screen from an SE to a Pro Max gets a crisp copy, and logo.png is the mark drawn beside the header text.

The last two files exist only for trust: manifest.json, a checksum inventory, and signature, Apple's cryptographic seal. Chapters 05 and 06 take those apart, hash by hash. Richer passes bundle more - a strip.png behind the fields, a thumbnail, .lproj folders of localized strings - but these seven files are everything an installable pass contains.

{
  "formatVersion": 1,
  "passTypeIdentifier": "pass.com.passmint.anatomy",
  "teamIdentifier": "PASSMINT26",
  "serialNumber": "ANATOMY-2026-0001",
  "organizationName": "Passmint Air",
  "description": "Boarding pass - PM314 LHR to SFO",
  "logoText": "Passmint Air",
  "foregroundColor": "rgb(245, 245, 245)",
  "backgroundColor": "rgb(14, 16, 22)",
  "labelColor": "rgb(150, 114, 255)",
  "relevantDate": "2026-07-03T08:15:00+01:00",
  "locations": [
    {
      "latitude": 51.4723,
      "longitude": -0.489,
      "relevantText": "Heathrow Terminal 5"
    }
  ],
  "webServiceURL": "https://api.passmint.com/apple-ws/",
  "authenticationToken": "sample-token-published-on-purpose",
  "barcodes": [
    {
      "format": "PKBarcodeFormatAztec",
      "message": "M1LOVELACE/ADA        EABC123 LHRSFOPM 0314 184Y012F0042 100",
      "messageEncoding": "iso-8859-1"
    }
  ],
  "boardingPass": {
    "transitType": "PKTransitTypeAir",
    "headerFields": [
      {
        "key": "gate",
        "label": "GATE",
        "value": "B36"
      }
    ],
    "primaryFields": [
      {
        "key": "origin",
        "label": "LONDON",
        "value": "LHR"
      },
      {
        "key": "destination",
        "label": "SAN FRANCISCO",
        "value": "SFO"
      }
    ],
    "secondaryFields": [
      {
        "key": "passenger",
        "label": "PASSENGER",
        "value": "Ada Lovelace"
      },
      {
        "key": "flight",
        "label": "FLIGHT",
        "value": "PM314"
      }
    ],
    "auxiliaryFields": [
      {
        "key": "boarding",
        "label": "BOARDING",
        "value": "07:45"
      },
      {
        "key": "seat",
        "label": "SEAT",
        "value": "12F"
      },
      {
        "key": "class",
        "label": "CLASS",
        "value": "Economy"
      }
    ],
    "backFields": [
      {
        "key": "about",
        "label": "About this pass",
        "value": "This is the sample pass from passmint.com/anatomy. Every file inside it is explained, hash by hash, on that page."
      }
    ]
  }
}

full walkthrough below ↓

One sharp edge before you build your own: the icon is not decoration. Wallet refuses a pass without an icon - the one hard requirement people hit first. Every other image - logo, strip, thumbnail, background - is contextual and skippable, but a missing icon.png means the pass silently fails to add, with no error to tell you why.

02

pass.json, annotated

Open pass.json and you are looking at the entire pass - all 87 lines of it, in the panel beside you. As you read, the camera zooms into each block as we reach it.

Open pass.jsonand you are looking at the entire pass - all 87 lines of it, annotated in full at the end of this chapter. Let's walk it block by block.

It starts with identity. formatVersion is always 1 - the format has not needed a 2 since 2012. passTypeIdentifier and teamIdentifier are the pair that matters: together they name who issued this pass, and Wallet checks them against the certificate that signs it. If the JSON claims pass.com.passmint.anatomy but the certificate was issued to anyone else, the pass is rejected before it renders. serialNumber distinguishes this pass from its siblings - unique within the pass type, and the key the update protocol uses later.

Then the face. organizationName appears in notifications, description is the accessibility string, logoText is printed large in the header. Three colors - background, foreground, label - drive the entire visual design. There is no CSS and no layout language; you choose three colors and Wallet draws the rest.

relevantDate and locationsare the quiet superpower. You didn't set an alarm for the gate - the pass did. relevantDate plus a location is all lock-screen surfacing takes, and the matching happens on-device. iOS watches the clock and the geofence itself; the issuer never learns where you are. A pass can carry up to ten locations - enough for every neighborhood branch of a coffee chain, or both ends of a flight. (iOS 18 renamed the date key to plural relevantDates; the singular still works, and this sample uses it for maximum compatibility.)

Next, two lines that look boring and are anything but. Remember webServiceURL and authenticationToken- they're the two lines that make a pass a living object instead of a screenshot. We'll come back to them. (And yes, this sample's token is published deliberately. It is inert - the serial number it pairs with exists in no database, so it authenticates exactly nothing.)

The barcodes array declares what the scanner sees: a format - Aztec here - a message string, and its text encoding. It is an array because scanners disagree; Wallet renders the first format the device supports. And the encoding is iso-8859-1rather than UTF-8 as a concession to scanner fleets older than the smartphone. That is the whole block; what's inside the message is chapter 04.

Everything else lives under a key named for the pass style - boardingPass in this sample. transitType picks the icon in the header notch, and five field groups carry the data: headerFields stay visible even when passes stack, primaryFields get the hero slot, then secondaryFields, auxiliaryFields, and backFields - the detail rows behind the ⓘ button. You place data into groups; the style decides where each group renders. Which brings us to chapter 03.

Identity

passTypeIdentifier + teamIdentifier are the pass's identity - they must match the certificate that signs it, or Wallet refuses the pass.

Passmint Air
LONDON
LHR
→
SAN FRANCISCO
SFO
Boarding
07:45
Seat
12F

03

Five faces, one format

Wallet knows five pass styles, and the choice is a single key in pass.json: boardingPass, eventTicket, storeCard, coupon, or generic. Same fields, same colors - flip the key and the layout transforms. Try it - the pills beside you (or below) re-render the same data into each face.

Passmint Air
LONDON
LHR
→
SAN FRANCISCO
SFO
Boarding
07:45
Seat
12F

Transit notch, header route, footer band - fields spread edge to edge.

The boarding pass is the most opinionated: it gets the notched header with a transitType icon - air, train, bus, boat, generic - and spreads its fields edge to edge for a glance at the gate. Event tickets hand the primary field a hero slot. Store cards and coupons share a strip-image layout built for balances and offers. Generic is the honest fallback - thumbnail right, fields left - for everything that isn't travel, tickets, or retail. Choosing the style is most of the design work; the rest is three colors and good labels.

04

The barcode is just a string

The Aztec square at the bottom of the pass encodes exactly one thing: the message string from the barcodes array. No wrapper, no encryption, no Apple format inside - the scanner at the gate reads the literal characters you put in pass.json. The barcode is a string with good PR. Wallet can draw it four ways - QR, Aztec, PDF417, or Code 128 - and an optional altText prints the human-readable fallback underneath for the day the scanner gives up.

What goes in the string is the issuer's business. A coffee card might encode a customer ID; a concert ticket, a signed token. The airline world settled on Aztec and on IATA's Bar Coded Boarding Pass standard - BCBP - a fixed-width record that every airline scanner on earth can parse, and this sample's payload is real BCBP. Fixed-width means position is meaning: character 0 declares the format, character 1 counts the legs, and the next twenty are the passenger name, space-padded to width - and so on down to the check-in sequence number. No delimiters, no JSON - 1980s-grade efficiency that still runs the world's airports. Click a segment to decode it.

FieldValueNote
MM = multi-leg capable BCBP
1one flight leg encoded
LOVELACE/ADAsurname/given, space-padded to 20
Eelectronic ticket
ABC123the code that unlocks the whole booking on most airline sites
LHRIATA origin
SFOIATA destination
PMairline code, space-padded
0314zero-padded flight 314
184day 184 of the year = July 3rd
YY = economy
012Frow 12, seat F
004242nd passenger to check in
11 = checked in
00hex length of the conditional field that follows - 00: nothing does
Everything highlighted above is printed on every boarding pass you've ever posted to Instagram - including the booking reference that can open your whole reservation on the airline's site. Blur the barcode too, not just the name.

05

manifest.json - the tamper seal

manifest.json is an inventory of everything else in the archive: one line per file, the filename and the SHA-1 digest of its exact bytes. Every file except two. The manifest cannot list itself - a file cannot contain its own digest - and it cannot list the signature, because the signature is computed over the finished manifest and exists only after the manifest is frozen.

{
  "pass.json": "e86c04e81eb3337b662db75c243960b2c2a83cb9",
  "icon.png": "16ad81b593b9acc728b7003131e95f0d627b3243",
  "icon@2x.png": "561cba35b1e1483c815e7a7f10fe66f3279d356d",
  "icon@3x.png": "b5cd79571fd74b2886fbbffa258e377ed489f83f",
  "logo.png": "bd5f0aeb2dd02ee3695d6b257d4bfdcee879960c"
}

These are the sample's real digests - the manifest you're looking at matches the file in the archive, hash for hash. Wallet recomputes every digest when you add the pass. Change one pixel of logo.png and its hash no longer matches - Wallet throws the pass out before it ever renders. Same for a single character of pass.json. The manifest freezes the archive: nothing inside can drift without detection.

SHA-1, in 2026? Deliberately fine. SHA-1 is broken for collision resistance, but the manifest is not the security boundary - it is a table of contents. The manifest itself is covered by the CMS signaturein the next chapter, so integrity comes from the certificate chain, not the digest alone. Swapping a file into an already-issued pass would take a second preimage - which SHA-1 still resists - and a brand-new manifest would need Apple's chain to sign it.

06

The signature - Apple's wax seal

The last file has no extension and does all the work. signature is a PKCS#7/CMS detached signature computed over manifest.json- detached meaning the file carries only the signature and the signer's certificates, not the data it signs.

And because the manifest fixes the hash of every other file, signing the manifest transitively seals the entire archive. One signed file, and nothing in the zip can change. It is ordinary DER-encoded CMS, too - no private format. openssl pkcs7 -inform DER -in signature -print_certs names every certificate in the chain.

Pass Type ID certificateyours - signs the manifest
↓ issued by
Apple WWDR intermediate
↓ issued by
Apple Root CAtrusted by every iPhone

When you add a pass, Wallet verifies the signature, walks that chain to the root baked into iOS, and then runs the check that catches most hand-rolled implementations: the identifiers embedded in your certificate must equal the passTypeIdentifier and teamIdentifier in pass.json. A valid signature from the wrong certificate is still a rejected pass - the identity pair from chapter 02 is enforced here, cryptographically. No entity other than the holder of your private key can produce a pass that claims to be yours, which is the entire reason a zip file is allowed to sit next to your credit cards.

This is the part nobody warns you about: getting that Pass Type ID certificate means a CSR, the Apple Developer portal, a .cer download, and a private key you must never lose - the certificate dance is most of the reason 'add wallet passes' sits in backlogs for quarters. And it never fully ends: certificates expire, and an expired signing certificate means new passes stop minting. Passmint owns this dance - issuance, signing, renewal - so an API call is the whole integration.

07

It's alive

Those two lines from chapter 02 - webServiceURL and authenticationToken - turn this sealed archive into something that updates itself. The protocol is small enough to hold in your head. When you add the pass, your iPhone registers itself with the issuer: it calls webServiceURL, presents the authenticationToken, and hands over a push token. The issuer now has an address for that pass on that phone.

When something changes - gate, seat, delay - the issuer sends a push through APNs to Wallet. Here is the elegant part: the push carries no payload at all; the pass re-downloads itself. APNs is just a doorbell. The device wakes, asks the web service what changed, fetches a fresh .pkpass - new manifest, new signature, the full seal again - and swaps it in place on your lock screen, no airline app required. The re-fetch is polite about it: the device first asks the web service which serial numbers changed since its last check, then downloads only those. One trap for implementers: there is no sandbox APNs for passes. Pushes are production-only from day one.

Your server
→
Passmint
→
APNs
→
Your iPhone

Feel it happen on your own lock screen → passmint.com/demo/apple-wallet.

08

The gate

That is the whole format: a zip, one JSON file for the design, one for the checksums, four PNGs, a signature. Nothing in it is hard to understand - the difficulty was never the format. It is the ceremony around it: certificates, signing, the update service, push registration. The format is knowable, the ceremony is the moat, and both are automatable.

And this is where all of it was heading: a scanner at a gate, one Aztec square, half a second. Keep scrolling - Ada's pass has a flight to catch. Every layer you just took apart gets reassembled, sealed, read back - and stamped - in the only moment that matters.

And this is where all of it was heading: a scanner at a gate, one Aztec square, half a second. Every layer you just took apart gets reassembled, sealed, and read back in the only moment that matters. Ada makes her flight.

You now know exactly what every 'Add to Apple Wallet' button on the internet has to produce - go mint one.

manifest.json
signature
icon.png · 1x 2x 3x
logoText · colors

Passmint Air

primaryFields

London

LHR

⟶

San Francisco

SFO

secondary · auxiliary

Passenger

Ada Lovelace

Flight

PM314

Boarding

07:45

Seat

12F

barcodes[0].message
pass.jsonthe whole file
{
  "formatVersion": 1,
  "passTypeIdentifier": "pass.com.passmint.anatomy",
  "teamIdentifier": "PASSMINT26",
  "serialNumber": "ANATOMY-2026-0001",
  "organizationName": "Passmint Air",
  "description": "Boarding pass - PM314 LHR to SFO",
  "logoText": "Passmint Air",
  "foregroundColor": "rgb(245, 245, 245)",
  "backgroundColor": "rgb(14, 16, 22)",
  "labelColor": "rgb(150, 114, 255)",
  "relevantDate": "2026-07-03T08:15:00+01:00",
  "locations": [
    {
      "latitude": 51.4723,
      "longitude": -0.489,
      "relevantText": "Heathrow Terminal 5"
    }
  ],
  "webServiceURL": "https://api.passmint.com/apple-ws/",
  "authenticationToken": "sample-token-published-on-purpose",
  "barcodes": [
    {
      "format": "PKBarcodeFormatAztec",
      "message": "M1LOVELACE/ADA        EABC123 LHRSFOPM 0314 184Y012F0042 100",
      "messageEncoding": "iso-8859-1"
    }
  ],
  "boardingPass": {
    "transitType": "PKTransitTypeAir",
    "headerFields": [
      {
        "key": "gate",
        "label": "GATE",
        "value": "B36"
      }
    ],
    "primaryFields": [
      {
        "key": "origin",
        "label": "LONDON",
        "value": "LHR"
      },
      {
        "key": "destination",
        "label": "SAN FRANCISCO",
        "value": "SFO"
      }
    ],
    "secondaryFields": [
      {
        "key": "passenger",
        "label": "PASSENGER",
        "value": "Ada Lovelace"
      },
      {
        "key": "flight",
        "label": "FLIGHT",
        "value": "PM314"
      }
    ],
    "auxiliaryFields": [
      {
        "key": "boarding",
        "label": "BOARDING",
        "value": "07:45"
      },
      {
        "key": "seat",
        "label": "SEAT",
        "value": "12F"
      },
      {
        "key": "class",
        "label": "CLASS",
        "value": "Economy"
      }
    ],
    "backFields": [
      {
        "key": "about",
        "label": "About this pass",
        "value": "This is the sample pass from passmint.com/anatomy. Every file inside it is explained, hash by hash, on that page."
      }
    ]
  }
}

87 lines. Every pixel Wallet draws comes from this file - keep scrolling to zoom into each block.

Passmint Air
LONDON
LHR
→
SAN FRANCISCO
SFO
Boarding
07:45
Seat
12F

M1LOVELACE/ADA EABC123 LHRSFOPM 0314 184Y012F0042 100

60 characters · zero secrets kept

manifest.jsonthe tamper seal
{
  "pass.json": "e86c04e81eb3337b662db75c243960b2c2a83cb9",
  "icon.png": "16ad81b593b9acc728b7003131e95f0d627b3243",
  "icon@2x.png": "561cba35b1e1483c815e7a7f10fe66f3279d356d",
  "icon@3x.png": "b5cd79571fd74b2886fbbffa258e377ed489f83f",
  "logo.png": "bd5f0aeb2dd02ee3695d6b257d4bfdcee879960c"
}

SHA-1 of every file except itself and the signature. Change a pixel, break a hash, lose the pass.

Pass Type ID certificatepass.com.passmint.anatomy
↓
Apple WWDRintermediate
↓
Apple Root CAtrust anchor

One detached signature over manifest.json covers every file

Your serversomething changed
↓
Passmintre-sign · store
↓
APNsempty wake-up push
↓
iPhonere-fetches the pass

The push carries no payload - the pass re-downloads itself

Passmint Air
LONDON
LHR
→
SAN FRANCISCO
SFO
Boarding
07:45
Seat
12F
Boarded
Gate B36Reading…Cleared - go through

✓ Seat 12F · Have a good flight, Ada

Every pass on this page is minted by the same API you'd ship with - signed, delivered, and ready for live updates.

Start with the API →Watch one update live