Generating and Updating Wallet Pass (pkpass) on the Server for Mobile Apps
A .pkpass file is a ZIP archive containing JSON description, images, and signature. Apple Wallet only accepts passes signed with a certificate from the Apple Developer Program. This is a server-side operation: the certificate is stored on the backend, the client receives the ready .pkpass file and opens it.
pkpass Structure
pass.pkpass/
├── pass.json # pass description
├── manifest.json # SHA1 hashes of all files
├── signature # PKCS#7 signature of manifest.json
├── icon.png # 29x29 pt
├── [email protected] # 58x58 pt
├── [email protected] # 87x87 pt
├── logo.png # up to 160x50 pt
└── background.png # optional, boardingPass only
pass.json: Key Fields
{
"formatVersion": 1,
"passTypeIdentifier": "pass.com.yourcompany.membercard",
"serialNumber": "user-12345-2024",
"teamIdentifier": "ABCD1234EF",
"organizationName": "Your Company",
"description": "Member Card",
"foregroundColor": "rgb(255, 255, 255)",
"backgroundColor": "rgb(15, 82, 186)",
"storeCard": {
"primaryFields": [
{
"key": "member_name",
"label": "Member",
"value": "Ivan Petrov"
}
],
"secondaryFields": [
{
"key": "points",
"label": "Points",
"value": "1 250",
"changeMessage": "Points updated: %@"
}
],
"auxiliaryFields": [
{
"key": "tier",
"label": "Status",
"value": "Gold"
}
],
"backFields": [
{
"key": "terms",
"label": "Terms",
"value": "Points valid 12 months from accrual."
}
]
},
"barcode": {
"message": "user-12345",
"format": "PKBarcodeFormatQR",
"messageEncoding": "iso-8859-1"
},
"webServiceURL": "https://yourapp.com/wallet/",
"authenticationToken": "vxwxd7J8AlNNFPS8k0a0FfUFtq0ewzV"
}
The webServiceURL + authenticationToken fields provide the push-update mechanism. Apple Wallet will register the device on your server and request updated pass when changes occur.
Server-Side Generation in Python
import hashlib
import json
import zipfile
import io
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.serialization import pkcs12
from cryptography.hazmat.backends import default_backend
from cryptography import x509
from cryptography.hazmat.primitives.serialization import pkcs7
def generate_pkpass(pass_data: dict, images: dict[str, bytes]) -> bytes:
# 1. Serialize pass.json
pass_json = json.dumps(pass_data, ensure_ascii=False).encode('utf-8')
# 2. Build manifest — SHA1 of all files
manifest = {}
files = {"pass.json": pass_json, **images}
for filename, content in files.items():
manifest[filename] = hashlib.sha1(content).hexdigest()
manifest_json = json.dumps(manifest).encode('utf-8')
# 3. Sign manifest with PKCS#7 detached signature
with open("pass-cert.p12", "rb") as f:
p12_data = f.read()
private_key, certificate, chain = pkcs12.load_key_and_certificates(
p12_data, b"p12_password", default_backend()
)
# Load Apple WWDR intermediate certificate
with open("AppleWWDRCA.cer", "rb") as f:
wwdr_cert = x509.load_der_x509_certificate(f.read(), default_backend())
signature = pkcs7.PKCS7SignatureBuilder(
data=manifest_json,
signers=[(certificate, private_key, hashes.SHA256())]
).add_certificate(wwdr_cert).sign(
encoding=serialization.Encoding.DER,
options=[pkcs7.PKCS7Options.DetachedSignature]
)
# 4. Pack into ZIP
buffer = io.BytesIO()
with zipfile.ZipFile(buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
zf.writestr("pass.json", pass_json)
zf.writestr("manifest.json", manifest_json)
zf.writestr("signature", signature)
for filename, content in images.items():
zf.writestr(filename, content)
return buffer.getvalue()
The Pass Type ID certificate is created in Apple Developer Portal → Certificates, Identifiers & Profiles → Identifiers → Pass Type IDs.
Download Endpoint
@app.get("/wallet/pass/{user_id}")
async def download_pass(user_id: str, token: str = Query(...)):
user = db.get_user(user_id)
if not verify_token(user, token):
raise HTTPException(403)
pass_data = build_pass_json(user)
images = load_pass_images()
pkpass_bytes = generate_pkpass(pass_data, images)
return Response(
content=pkpass_bytes,
media_type="application/vnd.apple.pkpass",
headers={"Content-Disposition": f"attachment; filename={user_id}.pkpass"}
)
iOS: Opening pkpass
import PassKit
func downloadAndAddPass(url: URL) {
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data, error == nil else { return }
do {
let pass = try PKPass(data: data)
DispatchQueue.main.async {
let vc = PKAddPassesViewController(pass: pass)!
self.present(vc, animated: true)
}
} catch {
print("Invalid pass: \(error)")
}
}.resume()
}
Push Updates via APNs
When data changes (points issued, status changed), the server:
- Gets the device's
pushTokenfrom the database (Apple Wallet registered it when the pass was added) - Sends an empty push via APNs to the
passkitproduction endpoint - Wallet makes GET
/wallet/v1/passes/{passTypeIdentifier}/{serialNumber}with Bearer token
The response is an updated .pkpass. Wallet applies changes and displays changeMessage if set in pass.json.
Timeline
2–3 days. Server-side generation with signature, download endpoint, iOS integration — 2 days. Push updates via APNs — additionally 0.5–1 day. Pricing is calculated individually.







