Why does my Apple Wallet pass stop updating after install?

An Apple Wallet developer's notes on why passes install fine but never refresh: the PassKit web service contract is usually where it breaks.

This is the question I get asked most often as an Apple Wallet developer: the .pkpass installs cleanly, the barcode scans, everyone signs off, and then the balance changes or the gate time moves and the pass in the user's Wallet still shows last week's number. Nobody gets a push, nothing errors, and the file you generate is correct. Generating the file rarely fails. What breaks after launch is almost always the PassKit web service contract that the device and Apple's push gateway both rely on.

An Apple Wallet pass installs cleanly and scans, then the balance changes and Wallet still shows last week's number, with no push and no error.
The pass installs. The barcode scans. Then the number goes stale and nothing tells you why.

The reason this catches people out is that a pass file and a live pass are two different products that happen to share a file extension.

Why does a Wallet pass stop updating after install?

A static pass is a signed zip. A pass that keeps updating is a small web service the device registers with and that Apple's push gateway pings, and Wallet only talks to it if that contract is implemented exactly. The pass carries a webServiceURL and an authenticationToken in its pass.json, and on install the device calls back to register itself against the pass.[1]1. The webServiceURL and authenticationToken keys in pass.json, and the device registration callback they trigger, are defined in Apple's PassKit Web Service Reference and the Wallet Developer Guide. If that registration handshake isn't stored correctly there is no device on file to push to, so a later update goes nowhere and never errors. That is the most common reason a pass that installed fine simply stops moving.

The registration call is easy to under-implement because it looks trivial. The device POSTs to your service with a device library identifier, the pass type identifier, the serial number, and a push token, and your job is to persist the relationship between that device and that pass. Most first implementations store the push token and move on. The part that bites later is that the same device re-registers the same pass across reinstalls and restores from backup, each time with a fresh push token, and your storage has to treat the latest registration as the live one instead of accumulating dead ones. A pass that worked in testing starts failing in the field precisely because real devices re-register and your service is still pushing to a token APNs retired months ago.

The push itself is the second trap. A Wallet update is an empty push sent over APNs against the pass type identifier as the topic, signed with the Pass Type ID certificate rather than the app's push certificate.[2]2. Wallet update notifications are empty pushes sent over APNs with the pass type identifier as the topic, signed with the Pass Type ID certificate, per Apple's "Updating a Pass" documentation in the Wallet Developer Guide. Get the topic or the signing cert wrong and APNs still returns a success status while Wallet quietly ignores it. And even a correct push only tells the device to ask for changes, which is where the third trap lives.

What does the PassKit update handshake actually look like?

There are three round trips, and a single broken link in any of them produces the identical silent symptom. On install the device registers itself with your web service. On a change you send an empty push through APNs. The device then comes back and asks what changed since the last time it looked, and only then downloads a fresh pass.

A sequence between the device running Wallet, APNs, and your web service. On install the device registers itself against the pass. On a change you send an empty push through APNs signed with the Pass Type ID certificate. The device then asks your web service what changed since last time, and stale modified-since bookkeeping reports nothing changed so the pass stays stale.
Three round trips. Any one of them failing looks exactly like the other two failing.

The third round trip is where most of the subtle bugs live, because it hinges on a piece of state that's easy to get almost right. When the device asks which of its passes have changed, your service answers with serial numbers and an opaque tag; the device hands that tag back on the next poll, and your service returns only the passes that changed after it. This is the same shape as an HTTP If-Modified-Since exchange, and it fails the same way. If your tag doesn't advance when the pass content changes, the device is told nothing changed and keeps the stale pass forever; if it advances when nothing meaningful changed, the device re-downloads on a loop that drains battery for no reason.

This is hard to catch because registration, the push topic, and the change tag are three independent systems that only have to agree at the exact moment a real pass ages in a real Wallet. Each passes its own unit test in isolation; the failure only appears at the join, under conditions a clean test device almost never reproduces. I've written before about how edge cases hide at the seams of a state machine rather than in any single state, and the PassKit update flow is one of the clearest cases of that I've shipped.

Why does a healthy-looking pass still fail to update?

The usual failure signature for a Wallet pass not updating is that everything looks healthy. APNs returns a success status, the pass JSON validates, the certificate is in date. The break is in the join between registration, change bookkeeping, and the push topic, where any single mismatch produces the same silent symptom and none of the pieces look broken on their own. You ship the happy path and only discover the gap once real passes age in real Wallets, where device migrations and reissued certificates exercise the registration and renewal paths a clean test device never touches.

Certificates are the slow-motion version of this. The Pass Type ID certificate that signs both the pass and the push expires, and when it does, every push you send fails at once for a reason that has nothing to do with the code you last touched. A team that treats the certificate as a one-time setup step rather than a recurring operational dependency discovers this about a year after launch, when the original cert quietly lapses.

One more thing makes this hard to catch in development: the iOS Simulator does not exercise the real push path. Passes behave differently in the Simulator than on a physical device, so a flow that looks complete on your Mac can be missing the entire update half, and you won't know until a real phone with a real push token is in the loop. Producing a signed pass takes an afternoon. Keeping live passes accurate and reachable by push for years is a service you have to run and maintain, and it goes subtly wrong in ways that only surface in production. I built the PureGym Apple Wallet pass and its Vapor backend by reverse engineering the flow, and I currently rank first on Google for "apple wallet consultant", so I have seen most of the ways this contract silently fails.

What about NFC passes, payments, and the App Review rules?

The moment a pass does more than show a barcode, the problem stops being purely technical and becomes a question of entitlements. Almost anyone can ship a barcode pass with a Pass Type ID, but an NFC pass that opens a gate or a payment pass that moves money sits behind Apple's enrolment and entitlement process, and the guidelines reserve the parts of Wallet that touch payments and access for specific programs rather than general developer accounts.[3]3. App Store Review Guidelines section 3.1 (Payments) and Apple's separate Wallet / Apple Pay and NFC entitlement programs govern which passes a standard developer account may ship versus which require enrolment. If your roadmap quietly assumes "we'll add tap-to-enter later," the right time to find out what that requires is before you've committed to a backend design that the entitlement process won't allow.

This matters because the update mechanism interacts with what the pass is allowed to carry: the same contract that refreshes a barcode also refreshes the fields on a transit or access pass, but who is allowed to sign for those fields changes the operational picture entirely. There's also a security dimension that's easy to wave away. The authenticationToken in the pass is a bearer credential, and your web service has to treat every registration and pass request as authenticated against it rather than serving updated passes to anyone who knows a serial number.[4]4. Apple's PassKit Web Service Reference specifies that registration, unregistration, and pass-fetch requests are authenticated with the pass's authenticationToken presented as an ApplePass authorization header. Getting the update path working is one milestone; getting it working without handing out other people's passes is where a security review of the backend earns its keep. If your pass crosses into NFC or hardware-backed access, that's adjacent to the BLE and NFC hardware work where the same questions about provisioning and trust come up.

How do I keep thousands of live passes accurate for years?

You treat the pass as the cheap part and the service behind it as a product with an operational budget. The service registers devices, tracks what each device last saw, signs pushes under a certificate that expires, and survives device migrations and reinstalls. Get that framing right at the start and most of the production failures are ones you've already designed around before they happen.

What you are running is a fan-out cache that has to stay coherent across devices you don't control and can't query. You never get to ask a device what version of a pass it currently holds; you can only push and then answer honestly when it comes back to ask what changed. That constraint is why the modified-since bookkeeping matters so much, and why "send a push and regenerate the pass" is necessary but not enough on its own. The work that keeps the cache honest is idempotent registration, a change tag that advances exactly when content changes, and a certificate rotation plan that doesn't strand the passes already in the wild.

If your pass installs but won't refresh, that diagnosis is the Apple Wallet and PassKit work I do as a PassKit developer. Send me your pass.json and a description of what isn't updating, and I can usually point at the broken link in the web service before I touch your server. The symptom is always the same; the cause almost never is, and working out which of the three round trips failed is most of what you're paying for.


  1. The webServiceURL and authenticationToken keys in pass.json, and the device registration callback they trigger, are defined in Apple's PassKit Web Service Reference and the Wallet Developer Guide. ↩︎

  2. Wallet update notifications are empty pushes sent over APNs with the pass type identifier as the topic, signed with the Pass Type ID certificate, per Apple's "Updating a Pass" documentation in the Wallet Developer Guide. ↩︎

  3. App Store Review Guidelines section 3.1 (Payments) and Apple's separate Wallet / Apple Pay and NFC entitlement programs govern which passes a standard developer account may ship versus which require enrolment. ↩︎

  4. Apple's PassKit Web Service Reference specifies that registration, unregistration, and pass-fetch requests are authenticated with the pass's authenticationToken presented as an ApplePass authorization header. ↩︎