Creating a web-service for Apple-Wallet passes with Node.js

Julius Schmid
5 min readAug 25, 2023

--

In this article I’ll show you how to implement your web-service (for .pkpass files in Node.js with Express).

Background

The purpose of the web service is to dynamically update .pkpass files once they have been added to a device.

Important: This is not a tutorial on how to create a .pkpass file with node.js. This is only about implementing the web service for a .pkpass. If you need a tutorial to create one I can recommend this tutorial.

Here is the official documentation on the Apple Developer page.

Explanation

I have summarized in my (extraordinarily beautiful ;) sketch how the web-service works.

A sketch of the functionality of the web service

Don’t worry if this feels like a lot at once, when you see the code you’ll be surprised how simple it actually is.

To be fair there are a few white-lies here to keep it simple, however we’ll look at everything in detail in a moment.

I will explain you the two most important ones (register a passport for automatic updates and send an updated passport to the client). After these two you should understand the principle and be able to implement the other endpoints yourself.

Preparation

Before you start, setup a node.js server (i use Firebase Functions).

Also think about how you want to save data when it comes from an endpoint to your server. All this saving is necessary so that you can select which pass or, more precisely, which pass on which device should be updated via your server later on.

Also note that I don’t save the data in a sql database as specified by the Apple developer documentation, but in the firebase firestore database. I would strongly recommend saving to a sql database though. The following is the Apple documentation’s indication of what data you should store in what table.

Device table

Contains the devices that contain updatable passes. Information for a device includes the device library identifier and the push token that your server uses to send update notifications.

Pass table

Contains the updatable passes. Information for a pass includes the pass type identifier, serial number, and a last-update tag. You define the contents of this tag and use it to track when you last updated a pass. The table can also include other data that you require to generate an updated pass.

Registration table

Contains the relationships between passes and devices. Use this table to find the devices registered for a pass, and to find all the registered passes for a device. Both relationships are many-to-many.

Registering a Pass for (automatic) updates (Pass get’s added to Apple-Wallet)

The following endpoint is called on your web-service url after adding the passport to the Apple Wallet. Calling the endpoint only worked for me on my real iPhone, not on the iOS simulator. This also didn’t work when I enabled the “Allow HTTP services” feature or other features in the developer settings.

The endpoint:

https://yourwebserviceurl/v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber}

The endpoint is called as a POST request with 3 path parameters:

  1. ‘deviceLibraryIdentifier’: A unique ID to identify and authenticate a device.
  2. ‘passTypeIdentifier’: Your passTypeIdentifier as specified when creating your passport.
  3. ‘serialNumber’: The serialNumber as specified when creating your passport.

As HTTP header the so called ‘authorization’ value is returned. You use this to verify that the request really comes from your passport. The value is: ‘ApplePass {the authenticationToken you assigned to the passport when you created it}’.

In the HTTP body you get back the ‘pushToken’. This is a long token that you will use to update passes.

So what you should do is:

  1. Create a new entry in your table for the .pkpass with the data: the passTypeIdentifier, the serialNumber and a last-updated tag. You have to set the last-updated tag to the current time every time you change it.
  2. Create a new entry in the Device Table. In this entry you should have 2 columns: the deviceLibraryIdentifier and the aforementioned pushToken that you get in the HTTP body of the request.

If the passport was already registered you should return a 200 status code, if it was not registered yet and everything was successful return a 201 status code and if something failed or the request is not authorized return a 401 status code.

const app = express();

app.use(express.json());
app.post('/:version/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber', async (req, res) => {
// Getting all parameters, headers and body data.
const { version, deviceLibraryIdentifier, passTypeIdentifier, serialNumber } = req.params;
const authHeader = req.headers.authorization;
const pushToken = req.body.pushToken;

// Creating references for our Firebase Firestore storage.
const devicesRef = admin.firestore().doc(`devices/${deviceLibraryIdentifier}`);
const passesRef = admin.firestore().doc(`passes/${serialNumber}`);
const registrationRef = admin.firestore().doc(`registration/${deviceLibraryIdentifier}`);

// Checking if request is authorized. Replace with your own token.
if (authHeader !== 'ApplePass {your Authorization Token}') {
res.status(401).send({
result: authHeader,
});
}

// Saving to Firebase Firestore altough you can save it with sql- and other DBs.
try {
const deviceSnapshot = await devicesRef.get();
const passSnapshot = await passesRef.get();
const timeStamp = admin.firestore.FieldValue.serverTimestamp();

if (!deviceSnapshot.exists) {
const deviceData = {
deviceLibraryIdentifier,
pushToken
};
await devicesRef.set(deviceData);

}
if (!passSnapshot.exists) {
const passData = {
serialNumber,
passTypeIdentifier,
timeStamp
}
await passesRef.set(passData);
}

await registrationRef.set({
passSerialNumber: serialNumber
}, { merge: true });

res.sendStatus(201);

} catch (error) {
console.error("Ein Fehler ist aufgetreten:", error);
res.sendStatus(500);
}
});

Updating a pass

The following endpoint creates, signs a updated version of your pass from your server to the device it is called from. This happens either automatically at certain intervals (if the user has the automatic updates function on for your pass) or via a push notification you can send or when the user makes the typical iOS gesture to update: pull down on your pass.

https://yourwebserviceurl/v1/passes/{passTypeIdentifier}/{serialNumber}

The endpoint is called as a GET request with 2 path parameters:

  1. ‘passTypeIdentifier’: Your passTypeIdentifier as specified when creating your passport.
  2. ‘serialNumber’: The serialNumber as specified when creating your passport.

As HTTP header you get the ‘authorization’ value as above. In your implementation you can then check if this matches the one you specified when creating the passport to make sure that the call comes from your passports.

If the passport is successfully created, signed and returned return a 200 status code. For everything else (failed or not authorized) a 401 status code.

app.get('/:version/passes/:passTypeIdentifier/:serialNumber', async (req, res) => {

// Your logic to create and sign a pass.

// Send your new generated Pass
res.set('Content-Type', 'application/vnd.apple.pkpass');
res.status(200).send(newPass);

});

And, if you use Firebase functions, don’t forget to make the endpoints available at the end.

exports.app = functions.https.onRequest(app);

That was my little article about the web service for Apple Wallet passes. I hope I could save you some time and nerves. If you have any questions don’t hesitate to contact me here.

--

--