Writing Labs Pass Serving Secrets
Space Computing Energy Tech Transport Science Dev Loyalty ↗ ◐ Dark mode ◎ Enable Alerts
Labs Pass · Guide

Apple Wallet passes
that actually update

Issuing a pass is easy. Making it change after it is sitting on someone's lock screen is where almost every implementation stops. This is the whole path — signing, the web service, and the push channel nobody documents properly.

Guide PassKit APNs Deno / Node

There is a lot of material on generating a .pkpass file. There is almost none on the part that matters: keeping it current. A loyalty card that shows a stale stamp count, an event ticket that cannot be moved to a new gate, a membership that cannot be revoked — these are the same failure, and it is a failure of infrastructure, not of design.

This guide covers the complete update path. It is written from the implementation that runs Labs Loyalty's own Wallet cards in production — signing, the web service, and push are all live there — and it deliberately spends most of its length on the three things that actually break: the web service contract, APNs over mutual-TLS, and a handful of undocumented behaviours that will cost you days if you meet them cold.

The one idea to take away

An Apple Wallet push carries no content. It is an empty background signal that means "something changed, go and ask." The device then calls your web service to fetch the new pass. Almost every wrong mental model of Wallet updates starts by assuming the push contains the update.

1. What a pass actually is

A .pkpass is a ZIP archive. Inside it:

The signature is what makes it a pass rather than a zip. Change one byte of any file without re-signing and the device rejects it silently.

The five pass types

boardingPass, coupon, eventTicket, storeCard, generic. The type determines the layout and which image slots exist. You cannot invent one, and you cannot change a pass's type after issue.

Gotcha — this one is genuinely undocumented

footerFields is not a real PassKit field group. The valid set is headerFields, primaryFields, secondaryFields, auxiliaryFields and backFields (plus additionalInfoFields, but only on poster event tickets). Popular libraries accept footerFields without complaint and silently drop it, so the field simply never appears — on any pass, ever, with no error anywhere. The confusion is understandable: Apple's own Human Interface Guidelines talk about a "footer" as a visual layout slot (the barcode/category line on some pass styles) — that's a real part of the design language, it just isn't a key you can write to in pass.json. If a field is mysteriously missing, unzip a real signed pass and read pass.json before you debug anything else.

2. Signing

You need three things from Apple: a Pass Type ID, a signing certificate for it, and the Apple WWDR intermediate certificate. The signature chains your certificate to Apple's root.

To make a pass updatable, pass.json must carry two extra keys:

{
  // ... your normal pass content ...
  "webServiceURL": "https://api.example.com",
  "authenticationToken": "<per-pass secret, 16+ chars>"
}
Gotcha — the classic first-week bug

Apple appends /v1/devices/... and /v1/passes/... to whatever you put in webServiceURL itself — it does not add a version prefix for you beyond that. Put /v1 in the value, as the example above used to, and every request lands on /v1/v1/devices/... unless your own routing happens to mount a second /v1 to match. The value should be the bare origin (optionally with a path prefix of your own choosing, e.g. https://api.example.com/passkit) — never a copy of the version segment Apple is about to add.

Two secrets are involved, not one, and they cover different calls. authenticationToken is the bearer credential above, sent as Authorization: ApplePass <token> on register, unregister and fetch-pass. The device's own deviceLibraryIdentifier — not a secret, just an opaque per-device id you chose when it registered — is what the "what changed?" endpoint below is keyed on. Treat the auth token as a real bearer credential: generate it randomly per pass, store its hash, and never reuse one across passes.

Certificates expire every 398 days

Not 365. When it lapses you cannot issue new passes and you cannot update existing ones — every pass already in circulation silently freezes, though it keeps displaying whatever it last showed (this is expiry, a soft failure — see the harder revocation case in §6). Apple emails the account as the date approaches, but don't make your renewal depend on an email landing in the right inbox: put the date in a calendar the day you create the certificate.

3. The web service

Once webServiceURL is present, the device starts talking to you. Five endpoints, and Apple's spec is exact about all of them. {deviceId} below is shorthand — the real path segment is deviceLibraryIdentifier, which is what you'll see in the actual spec and in any library's route table.

Method & pathWhat it means
POST /v1/devices/{deviceId}/registrations/{passTypeId}/{serial}Device is asking for updates. Body contains its APNs pushToken. Store it.
DELETE — same pathPass was removed. Delete the registration or you will push into the void forever.
GET /v1/devices/{deviceId}/registrations/{passTypeId}?passesUpdatedSince=<tag>"What changed?" Return the serials that changed plus a new lastUpdated tag.
GET /v1/passes/{passTypeId}/{serial}Serve a freshly signed .pkpass. This is the actual update.
POST /v1/logDevice-side error log, sent unprompted. Easy to skip in a short guide — but the spec has five endpoints, not four, and this is the one people forget exists.

Two behaviours worth knowing before you go looking for bugs:

4. The push channel — the part that breaks

This is where implementations die. Wallet push does not use a normal APNs auth key. It authenticates with your pass certificate as a TLS client certificate — mutual TLS, against api.push.apple.com.

The payload is empty. The apns-topic is your Pass Type ID, not a bundle ID. The push type is background.

// Deno — mutual-TLS client cert on the fetch client
const client = Deno.createHttpClient({
  cert: signerCertPem,   // your pass certificate, PEM
  key:  signerKeyPem,    // its private key, PEM
});

await fetch(`https://api.push.apple.com/3/device/${pushToken}`, {
  method: "POST",
  client,
  headers: {
    "apns-topic": PASS_TYPE_ID,      // NOT a bundle id
    "apns-push-type": "background",
    "apns-priority": "5",            // required: default (10) + background is a documented APNs error
  },
  body: "{}",                         // deliberately empty
});

Don't skip apns-priority: 5. APNs' default priority is 10, and 10 paired with a background push type is a documented rejection — the frustrating version of this bug returns a plain 200 from APNs while the device does nothing at all, so it looks like your push worked right up until you check whether the pass actually updated.

Why this is the hard part

Client-certificate support is the least-travelled path in most HTTP stacks. You will hit runtime-specific limitations here that have nothing to do with Wallet, and the failure mode is usually an opaque TLS error rather than a useful HTTP status. Get a single push working end-to-end before you build anything on top of it — this is the step to de-risk first, not last.

The full update sequence

  1. Your data changes.
  2. You look up every registration for that pass serial.
  3. You send an empty push to each device token.
  4. The device calls your passesUpdatedSince endpoint.
  5. You answer with the changed serials.
  6. The device fetches each pass fresh.
  7. You sign a new .pkpass at request time and return it.

Note step 7. You are not storing pass files and serving them — you are regenerating and re-signing on demand. Build your pass generation as a pure function of your data from the very beginning, or you will rewrite it later.

5. Gotchas worth the time they cost

Serve the right content type

application/vnd.apple.pkpass. Get it wrong and the device downloads a zip instead of offering to add a pass.

Images are not optional

A missing icon.png produces a pass that fails to install with no error message. The icon is what appears in notifications and the lock screen, so it is required even though nothing tells you so.

Test on a real device

The Simulator's Wallet behaviour diverges from a real phone, particularly around registration and push. A pass that installs and updates in the Simulator proves very little.

Verify by unzipping, not by trusting

Pull a real signed pass from your live endpoint, unzip it, and read pass.json and the image bytes. A 200 response tells you your server worked; it tells you nothing about whether the pass is correct. This is how the footerFields problem above gets found.

Gateway auth in front of your functions

If your web service sits behind a platform that verifies its own auth tokens by default, Apple's calls will be rejected before your code runs — Apple sends Authorization: ApplePass <token>, which is not your platform's scheme. You will see a gateway 401 and no logs at all. Deploy these endpoints with platform auth disabled and do the ApplePass check yourself.

6. The certificate question, before you build a business on it

If you are issuing passes for other people, the constraint is not technical. Apple's position is that the entity owning the certificate should be the entity issuing and redeeming the passes. Issuing on a client's behalf means holding a written agreement authorising it, and putting a clear disclaimer on the back of the pass stating you are authorised and where it can be redeemed. Apple can revoke certificates for violations.

You hold the certificateClient holds it
OnboardingImmediateClient needs their own developer account
BrandingPasses stack; shared lock-screen iconCorrect per-brand icon
Blast radiusOne revocation affects everyoneIsolated
RenewalsOne clockOne clock per client

Do not confuse this with certificate expiry above — they are different failure modes with very different blast radii. Expiry is the soft case: installed passes keep showing whatever they last displayed, you simply lose the ability to sign new passes or push updates until you renew. Revocation is not the same thing with a different name. Apple's own certificate documentation is explicit about it: once a certificate is revoked, passes signed with it "will no longer function properly" — not "stop updating," stop functioning. A revocation can break passes that are already sitting on someone's lock screen, not just freeze them in place. If you are issuing passes for other people, write the revocation risk into your terms as a real, hard failure — not the same soft "it'll just go stale" degradation the certificate-expiry callout above describes.

NFC passes are a separate application

A pass that emulates a card to a physical reader — door entry, transit, access control — needs an Apple Developer account specifically approved to issue NFC Pass Type Identifier Certificates. Apple reviews your business and use case, approval takes roughly two to four weeks, and you must supply a public encryption key for Value Added Services. Apply early; it runs in parallel with everything else.

Labs Pass

Everything above — signing, the five web service endpoints, and mutual-TLS push — is the update path running Labs Loyalty's own Wallet cards in production today. A packaged, MIT-licensed reference implementation is in progress; this page will link to it the day it ships, not before.

More from the Labs