A Simple Development Guide to Apple Wallet Passes
12/08/2026
Apple was founded with the mindset of its products existing as a closed system. That is, intentionally abstracting and preventing understanding of the system in exchange for tighter coupling between hardware and software, thereby achieving a "sleeker" feeling product. This decision has persisted across all of Apple's offerings, from the first Macintosh to the newest iPhone. Apple's implementation of wallet passes named PKPass or "PassKit Pass" follow this principle, resulting in an, admittedly, seamless user flow to install them.
As part of my work with the IT team at my university's Computer Science club, I was tasked with building a prototype membership system that used mobile wallet passes with QR codes to scan and redeem points for attending club-run events. Despite an admirable attempt on Apple's part at documentation, Apple's smooth wallet flow came at the cost of abstracting many details, many of which were not captured by documentation. I've compiled my findings into an article, including information about a PKPass' structure, creation and common issues to help future developers or potentially future club members that will succeed me. Rather than a comprehensive guide, I suggest using this piece as supplementary reading to cover blind spots in Apple's docs.
!!Disclaimer: Generating passes is NOT free!!: Passes require a paid Apple Developer account in order to register the information needed to issue them.
What is a PkPass + Structure
Simply put, a PkPass is a ZIP archive containing JSON data and image assets that are signed using certificates owned by you and Apple.
More specifically, many .pkpass files will contain:
pass.json[1]: Mandatory. Defines core metadata such as information about the issuing organisation, the card's layout and style, and pass-specific information such as its serial numbermanifest.json: Mandatory but often generated programmatically. Stores SHA-1 hashes of every file (excluding itself and the signature) in the pass to prevent tampering.signature: Mandatory but often generated programmatically. A digital signature proving the manifest belongs to the real organisation and not an impostor. Signed using the organisation's signing certificate.logo.png[2]: Brand logo shown in the top corner of the pass.icon.png[2]: Small icon used for notifications and in settings.thumbnail.png[2]: Secondary image displayed next to fields in specific pass styles such as boarding passes.strip.png[2]: The main graphic used for specific pass styles such as event tickets or store cards.background.png[2]: Optional background image.Localisation information: Passes can be localised for different languages, though this will require apass.stringsfile for each language to store translated messages.
1. Apple encourages the use of its Pass Designer tool for creating passes. To automate the creation process, however, we opted for creating a skeleton JSON pass and programmatically adding styling and user data.
{
"webServiceURL": "URL_TO_PASS_SERVICE",
"authenticationToken": "STATIC_B64_SECRET",
"formatVersion" : 1,
"passTypeIdentifier" : "pass.com.cissa.membership",
"serialNumber" : "/////////",
"teamIdentifier" : "SECRET_APPLE_ORGANISATION_IDENTIFIER",
"organizationName" : "Computing and Information Systems Students Association",
"description" : "CISSA Membership Card",
"foregroundColor" : "#FFFFFF",
"backgroundColor" : "#000000",
"labelColor" : "#FFFFFF",
"storeCard" : {}
}{
"webServiceURL": "URL_TO_PASS_SERVICE",
"authenticationToken": "STATIC_B64_SECRET",
"formatVersion" : 1,
"passTypeIdentifier" : "pass.com.cissa.membership",
"serialNumber": "POPULATED_SERIAL_NUMBER",
"teamIdentifier" : "SECRET_APPLE_ORGANISATION_IDENTIFIER",
"organizationName": "Computing and Information Systems Students Association",
"description": "CISSA Membership Card",
"foregroundColor": "#FFFFFF",
"backgroundColor": "#000000",
"labelColor": "#FFFFFF",
"storeCard": {
"headerFields": [
{
"key": "validThrough",
"label": "VALID",
"value": "2025",
"textAlignment": "PKTextAlignmentLeft"
}
],
"primaryFields": [],
"secondaryFields": [
{
"key": "name",
"label": "Name",
"value": "John Ling",
"textAlignment": "PKTextAlignmentLeft"
}
],
"auxiliaryFields": [],
"backFields": [],
"additionalInfoFields": []
}
}In addition, an important pair of attributes is the webServiceURL and authenticationToken. Simply put, this allows for passes
to be updated or deactivated without the user's interaction which can be useful for distributing patches across the entire system.
Some setup does need to be done on the server's side, namely creating an API to handle the aforementioned operations. More details can be found
in this guide from Apple.
2. Assets should have 1-3x resolution variants (i.e logo.png, logo@2x.png and logo@3x.png)
Generating Certificates
I've glossed over certificates until now since the process is fairly straightforward:
- After getting your Apple Developer account, you'll visit the Apple Developer portal and navigate to "Identifier" 1a. Create a Pass Type ID used to represent our identity as pass authors.
- Download Apple's certificate (as of writing in 2026 it is WWDR G4) from here
- Afterwards, the next steps will be to create a Certificate Signing Request (CSR)
3a. Generate a private signing key using
openssl genrsa -out <your-key-name>.key 20483b. Create the CSR usingopenssl req -new -key <your-key-name>.key -out request.certSigningRequest3c. Enter the following information. Blanks can be skipped as can everything after "Email Address"
Country Name (2-letter code) [AU]: AU
State or Province Name [Some-State]: VIC
Locality Name []:
Organization Name [Internet Widgits Pty Ltd]: Apple Inc.
Organizational Unit Name []: Apple Worldwide Developer Relations
Common Name []: Apple Worldwide Developer Relations Certification Authority
Email Address []: YOUR_EMAIL_HERE- Upload the
request.certSigningRequestfile to Apple's portal. You'll receive your signing certificate in return. Keep both the key and certificate safe. - Convert both your new signing certificate and Apple's WWDR certificate into
.pemfiles withopenssl x509 -inform DER -outform PEM -in <cert name>.cer -out <cert name>.pem
Generating Passes Programmatically
The algorithm to generate a pass is as follows:
- Store
pass.jsonand assets under a folder formatted asNAME.pass - Populate
manifest.jsonwith SHA-1 hashes for each asset. - Sign the manifest using your signing certificate and key via the
opensslcommand - Zip the folder into a pass file.
Though this would be done through the terminal, a more programmatic approach would be to use libraries
that automate this process. In our case, we used a Node library called passkit-generator.
// ...
const newPass = await PKPass.from({
model: path.join(process.cwd(), "src/app/api/membership/passes/base.pass"),
certificates: {
wwdr: {{APPLE_CERT_HERE}},
signerCert: {{SIGNING_CERT_HERE}},
signerKey: {{SIGNING_KEY_HERE}}
},
},
{
// pass.json data
});
newPass.headerFields.push({
"key": "validThrough",
"label": "Valid Through",
"value": "2026",
"textAlignment": "PKTextAlignmentLeft"
});
newPass.secondaryFields.push({
"key": "name",
"label": "Name",
"value": data.fullName,
"textAlignment": "PKTextAlignmentLeft"
});
newPass.setBarcodes({
message: JSON.stringify({
id: data.studentId,
}),
format: "PKBarcodeFormatQR",
messageEncoding: "iso-8859-1",
altText: ""
});
// ...Generates a minimal pass with a QR code.
Distribution + Debugging Failures
Compared to creation, getting the distribution just right was far more difficult.
For starters, it's worthwhile to note that passes can be installed using binary data in object urls or via direct API responses. However, for both methods, these conditions need to be met:
- Everything needs to be done in Safari over HTTPS. No other browsers will work.
- Both need a MIME type of "application/vnd.apple.pkpass"
Setting the MIME type in the API response is trivial, as is setting up HTTPS for a local dev server.
Getting object URLS right can be slightly tricky. I suggest converting the created pass into bytes then sending it back
to the frontend to be extracted using await response.blob(). Afterwards, we can generate an object URL and populate the DOM.
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${studentId}.pkpass`;Common Debugging Issues
Wallet passes can be cryptic in that if something goes wrong, you will be presented with a generic error message rather than anything insightful.
Common issues include:
- Expired signing certificates or keys.
- Outdated WWDR certificates: As of writing, Apple mandates the use of their G4 certificate.
- Missing assets
- Malformed
pass.json
In my case, I lost a lot of time because our signing cert silently expired. Speaking from experience, it is worth keeping track of the expiration dates of your signing certs and keys.
PKPass validator websites are incredibly helpful in ensuring wallet passes are valid and, by extension, installable. Although, keep in mind that some of these validators may not be properly up to date and may miss or falsely report errors. Using multiple of these services to get different "opinions" is highly recommended.
Conclusion
Hopefully, this guide has helped you understand both the theory and application of Apple's wallet pass implementation. While I can't publish the exact code I wrote for the club's system, what has been provided should give the a good reference for generating passes programmatically.