Implementing Push Updates for Wallet Pass in Mobile Application
Apple Wallet Pass isn't a static card. Loyalty program, flight ticket, coupon — data changes in real time, user should see current info without reinstalling app. Exactly why Apple implemented server push-update mechanism for PassKit.
How Update Mechanism Works
Architecturally: your server registers device via PassKit Web Service API, stores deviceLibraryIdentifier + pushToken pair, and on data change sends push via APNs. iOS "wakes up", makes GET request to server for updated .pkpass file, card updates without user involvement.
Implementation splits into server and client parts — client nearly zero: PassKit itself handles registration cycle if server implements protocol correctly.
PassKit Web Service Server Protocol
Server must provide four endpoints:
-
POST /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}— device registration -
DELETE /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}— deregistration -
GET /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}?passesUpdatedSince={tag}— list of updated passes -
GET /v1/passes/{passTypeIdentifier}/{serialNumber}— download current.pkpass
Most common error — wrong HTTP status. Apple PassKit very sensitive: 200 with empty body on DELETE → iOS breaks deregistration. Need 204 No Content. GET with no changes — strictly 204, not 200 [].
// Example response structure for GET /registrations
{
"serialNumbers": ["ABC123", "DEF456"],
"lastUpdated": "1711234567"
}
lastUpdated field — UNIX timestamp as string. iOS passes it back in passesUpdatedSince on next request. Wrong timestamp format — device constantly requests all passes, ignoring incremental logic.
APNs Push for Update
Wallet push non-standard. Minimal payload:
{
"aps": {}
}
Exactly like this — empty aps. No alert, badge, sound. iOS on receiving such push silently goes to server for updates. Send via APNs with apns-topic equal to passTypeIdentifier (format: pass.com.yourcompany.appname), not bundleIdentifier.
PassKit certificate separate — Pass Type ID Certificate from Apple Developer Portal, not usual APN cert. Confused regularly, result: APNs accepts request but push doesn't deliver.
# Example sending via httpx (Python, APNs HTTP/2)
headers = {
"apns-topic": "pass.com.example.loyalty",
"apns-push-type": "background",
"apns-priority": "5",
"authorization": f"bearer {jwt_token}"
}
payload = json.dumps({"aps": {}})
response = await client.post(
f"https://api.push.apple.com/3/device/{push_token}",
content=payload,
headers=headers
)
apns-priority: 5 — mandatory for background push. Priority 10 for Wallet doesn't work as expected.
.pkpass Signing
Each .pkpass — ZIP archive with manifest.json (SHA-1 hashes of all files) and signature (PKCS#7 detached signature). On update must recalculate manifest and recreate signature. Using old signature with new data → iOS silently ignores file.
Signature generation via openssl:
openssl smime -binary -sign \
-certfile AppleWWDRCA.pem \
-signer passcertificate.pem \
-inkey passkey.pem \
-in manifest.json \
-out signature \
-outform DER
Apple's signpass tool convenient for testing, but production better to implement signing natively on server — without external binaries.
Testing Update Cycle
PassKit Companion App (macOS) simulates registration and checks endpoints directly. Server debug — enable WKWebsiteDataStore logging on iOS simulator won't work, but Charles Proxy on real device intercepts PassKit requests completely.
Typical debug cycle: add pass via app → check POST registration in Charles → push manually via curl → confirm iOS fetches updated pass → check card UI.
Process
Audit current server infrastructure or implement from scratch: Pass Type ID, server endpoints, pushToken storage.
Set up APNs with Pass Type ID Certificate, implement push send on data change.
Generate and sign .pkpass on server, set up incremental updates.
Test full cycle: registration → data change → push → card update.
Timeline Estimates
Existing infrastructure with just PassKit Web Service addition — 3–5 days. Full implementation including pass file generation and server backend — 1–2 weeks depending on update logic complexity.







