Apple Wallet Integration for Loyalty Cards in Mobile Apps
PassKit is the framework that manages .pkpass files on the device. A loyalty card in Apple Wallet is a signed JSON archive containing images, metadata, and optional NFC data. The main entry point in code is PKAddPassesViewController. Without a properly formatted and signed archive, users will receive an "Invalid Pass" error when attempting to add the card.
.pkpass Structure and Signing
The archive contains:
-
pass.json— card type, colors, and fields -
icon.png,logo.png,strip.png— graphic assets (required in double density@2x) -
manifest.json— SHA1 hashes of all files -
signature— PKCS#7 signature of the manifest using the Pass Type ID certificate
The most common mistake when building the archive manually is an incorrect manifest.json. The hash must match the actual file content exactly. A single extra character in pass.json will cause Apple Wallet to reject the package without a clear message.
Minimal pass.json for a loyalty card:
{
"formatVersion": 1,
"passTypeIdentifier": "pass.com.yourcompany.loyalty",
"serialNumber": "USER-12345",
"teamIdentifier": "ABCDE12345",
"organizationName": "YourCompany",
"description": "YourCompany Loyalty Card",
"logoText": "YourCompany",
"foregroundColor": "rgb(255,255,255)",
"backgroundColor": "rgb(30,90,200)",
"storeCard": {
"primaryFields": [
{ "key": "balance", "label": "Points", "value": "1 240" }
],
"secondaryFields": [
{ "key": "tier", "label": "Level", "value": "Gold" }
],
"barcode": {
"message": "USER-12345",
"format": "PKBarcodeFormatQR",
"messageEncoding": "iso-8859-1"
}
}
}
The storeCard field identifies this as a loyalty card pass type. Alternatives: boardingPass, coupon, eventTicket, generic.
Server-Side Signing
Three entities from Apple Developer Portal are required:
-
Pass Type ID certificate — issued for a specific
passTypeIdentifier - WWDR Intermediate Certificate — downloaded separately from Apple's website
- Certificate private key
The signature is generated via OpenSSL:
openssl smime -binary -sign \
-signer pass_certificate.pem \
-inkey pass_key.pem \
-certfile wwdr.pem \
-in manifest.json \
-out signature \
-outform DER -nodetach
Ready-made libraries are available: passbook for Node.js, passkit-generator for TypeScript, wallet-php for PHP.
iOS: Adding a Pass in the App
import PassKit
func addLoyaltyCard(passData: Data) {
guard let pass = try? PKPass(data: passData) else {
showError("Could not read the pass")
return
}
let passLibrary = PKPassLibrary()
if passLibrary.containsPass(pass) {
// Card already added — offer to update
passLibrary.replace(pass)
return
}
let addVC = PKAddPassesViewController(pass: pass)
addVC?.delegate = self
present(addVC!, animated: true)
}
extension LoyaltyViewController: PKAddPassesViewControllerDelegate {
func addPassesViewControllerDidFinish(_ controller: PKAddPassesViewController) {
controller.dismiss(animated: true)
checkPassStatus()
}
}
PKPassLibrary().containsPass(_:) checks by the combination of passTypeIdentifier + serialNumber. If the pass already exists, PKAddPassesViewController will show an "Update" dialog instead of "Add".
Updating Card Data (Push Notifications)
Apple Wallet supports server-side updates via Web Service URL. Add these to pass.json:
"webServiceURL": "https://api.yourcompany.com/wallet",
"authenticationToken": "vxwxd7J8AlNNFPS8k0a0FfUFtq0ewzFdc"
Wallet will periodically query GET /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}, receive a list of updated serialNumber values, and download new pass versions.
Timeline
2–3 days for server-side pass generation and signing, PKAddPassesViewController implementation, and push-update configuration. Pricing is calculated individually.







