Back to blog
guide
3 months ago

Apple Wallet Pass Series (Part 3): Setting up Updates

Learn how to implement dynamic updates for Apple Wallet passes using Node.js and web services

15 min read
Apple Wallet Pass Series (Part 3): Setting up Updates

Apple Wallet Pass Series (Part 3): Setting up Updates

In the previous parts of this series, we navigated the labyrinth of certificate generation and crafted our first digital Apple Wallet pass. Now, it's time to take things a step further by enabling our passes to update dynamically. This means that once a user has added your pass to their wallet, you can push updates to it without any manual intervention from the user. Whether it's updating flight times, changing event details, or tweaking adding points to their loyalty card, dynamic updates enable you to make your passes relevant and useful.

But how do we make this magic happen? Let's dive into the world of web services, push notifications, and a bit of server-side wizardry.

Understanding Pass Updates

Before we get our hands dirty with code, let's understand how pass updates work as its not really an intuitive process.

  1. User Installs the Pass: The user adds your pass to their Apple Wallet.

  2. Device Registers for Updates: If your apple wallet has a webServiceURL, then the device registers itself with your server to receive updates for that pass. This event is triggered when the pass is added to the wallet for the first time.

  3. Pass Information Changes: Something changes on your end—perhaps a flight is delayed or a special offer is updated, this isn't immediatly reflected on the card, you must inititate the following process.

  4. Server Sends Push Notification: Your server sends a push notification to the user's device, indicating that the pass has been updated.

  5. Device Fetches Updated Pass: The device receives the notification and contacts your server to fetch the updated pass.

  6. Pass is Updated in Wallet: The updated pass replaces the old one in the user's wallet.

In essence, updating passes is a collaborative dance between the device, your server, and Apple's push notification service.

Modifying Pass Generation

To start, we need to modify how we generated our pass in the previous post examine the code below:

const pass = await PKPass.from({
  model: path.join(__dirname, "passModels", "BoardingPass.pass"),
  certificates: {
    wwdr,
    signerCert,
    signerKey,
    // signerKeyPassphrase, // Include if your key is passphrase protected
  },
}, {
  serialNumber:serialNumber, 
  description: "Boarding Pass for Flight 815",
});

In this boarding pass we have the data being added to the pass as the serialNumber and the description. We need to add to the pass on creation a new property called the webServiceURL. This tells the pass what is the base url it should be calling when updating, registering and unregistering the pass. So, a modified version of the above code would look something like this. You may also want to setup the authenticaiton token to prevent your pass being updated by any other malicous servers.

const pass = await PKPass.from({
  model: path.join(__dirname, "passModels", "BoardingPass.pass"),
  certificates: {
    wwdr,
    signerCert,
    signerKey,
    // signerKeyPassphrase, // Include if your key is passphrase protected
  },
}, {
  serialNumber:serialNumber, 
  description: "Boarding Pass for Flight 815",
  webServiceURL: "https://yourbackend.com/api",
  authenticationToken: "somerandomlongstring",
});

Setting Up the Web Service

To enable pass updates, we'll need to set up a web service at our webServiceURL that handles:

  • Registering and Unregistering Devices: Keeping track of which devices have your pass installed.
  • Providing Updated Passes: Serving the latest version of the pass when requested.

According to Apple's Wallet Web Service Reference, we'll need to implement the following endpoints:

  1. Register a Device to Receive Push Notifications:
POST /v1/devices/<deviceLibraryIdentifier>/registrations/<passTypeIdentifier>/<serialNumber>
  1. Unregister a Device:
DELETE /v1/devices/<deviceLibraryIdentifier>/registrations/<passTypeIdentifier>/<serialNumber>
  1. Get the Serial Numbers for Passes Associated with a Device:
GET /v1/devices/<deviceLibraryIdentifier>/registrations/<passTypeIdentifier>?passesUpdatedSince=<tag>
  1. Get the Latest Version of a Pass:
GET /v1/passes/<passTypeIdentifier>/<serialNumber>

Endpoint Implementations

Device Registration

When a user adds your pass to their wallet, their device will attempt to register with your server for updates.

router.post(
  "/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber",
  async (req, res) => {
    const { deviceLibraryIdentifier, passTypeIdentifier, serialNumber } = req.params;
    const authHeader = req.headers.authorization;
    const { pushToken } = req.body;
 
    // Replace with your actual authentication token on your backend
    const AUTH_TOKEN = "ApplePass YOUR_AUTHENTICATION_TOKEN";
 
    // Verify the authentication token
    if (authHeader !== AUTH_TOKEN) {
      return res.status(401).send("Unauthorized");
    }
 
    try {
      // Store device and pass registration in MongoDB
      const collection = db.collection("registrations");
      await collection.updateOne(
        { deviceLibraryIdentifier },
        {
          $set: {
            deviceLibraryIdentifier,
            pushToken,
            serialNumber,
            passTypeIdentifier,
          }
        },
        { upsert: true }
      );
 
      res.sendStatus(201);
    } catch (error) {
      console.error("Error registering device:", error);
      res.sendStatus(500);
    }
  }
);

Device Unregistration

When a user removes your pass from their wallet, their device will notify your server to unregister from updates.

router.delete(
  "/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber",
  async (req, res) => {
    const { deviceLibraryIdentifier } = req.params;
    const authHeader = req.headers.authorization;
 
    const AUTH_TOKEN = "ApplePass YOUR_AUTHENTICATION_TOKEN";
 
    if (authHeader !== AUTH_TOKEN) {
      return res.status(401).send("Unauthorized");
    }
 
    try {
      const collection = db.collection("registrations");
      const result = await collection.deleteOne({ deviceLibraryIdentifier });
 
      if (result.deletedCount === 0) {
        return res.status(404).send("No matching registration found");
      }
 
      res.sendStatus(200);
    } catch (error) {
      console.error("Error unregistering device:", error);
      res.sendStatus(500);
    }
  }
);

Get Updated Passes

When you send a push notification indicating that passes have been updated, devices will call this endpoint to get the serial numbers of the passes that need updating.

router.get(
  "/v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier",
  async (req, res) => {
    const { deviceLibraryIdentifier } = req.params;
    const passesUpdatedSince = req.query.passesUpdatedSince;
 
    try {
      const collection = db.collection("registrations");
      const registration = await collection.findOne({ deviceLibraryIdentifier });
 
      if (!registration) {
        return res.status(404).send("No matching passes found for this device");
      }
 
      const { serialNumber, lastUpdated } = registration;
 
      // Check if the pass has been updated since the provided tag
      if (passesUpdatedSince && lastUpdated <= passesUpdatedSince) {
        // No updates
        return res.status(204).send();
      }
 
      res.status(200).json({
        serialNumbers: [serialNumber],
        lastUpdated: Date.now().toString(),
      });
    } catch (error) {
      console.error("Error fetching updated passes:", error);
      res.sendStatus(500);
    }
  }
);

Get Latest Pass Version

When a device knows which passes have been updated, it will call this endpoint to fetch the latest version.

router.get(
  "/v1/passes/:passTypeIdentifier/:serialNumber",
  async (req, res) => {
    const { serialNumber } = req.params;
    const authHeader = req.headers.authorization;
 
    const AUTH_TOKEN = "ApplePass YOUR_AUTHENTICATION_TOKEN";
 
    if (authHeader !== AUTH_TOKEN) {
      return res.status(401).send("Unauthorized");
    }
 
    try {
      // Generate the updated pass
      const passBuffer = await generatePassBuffer(serialNumber);
 
      res.setHeader("Content-Type", "application/vnd.apple.pkpass");
      res.send(passBuffer);
    } catch (error) {
      console.error("Error generating pass:", error);
      res.status(500).send("Error generating pass");
    }
  }
);

Pass Generation Helper

In the /v1/passes/:passTypeIdentifier/:serialNumber endpoint, we called a function generatePassBuffer(serialNumber). Let's implement this function using the passkit-generator library we used in the previous part.

const { PKPass } = require("passkit-generator");
const path = require("path");
 
const generatePassBuffer = async (serialNumber) => {
  // Load your pass model and certificates
  const pass = await PKPass.from({
    model: path.join(__dirname, "passModels", "YourPassModel.pass"),
    certificates: {
      wwdr: await fs.readFile(
        path.join(__dirname, "certs", "wwdr.pem"),
        "utf8"
      ),
      signerCert: await fs.readFile(
        path.join(__dirname, "certs", "signerCert.pem"),
        "utf8"
      ),
      signerKey: await fs.readFile(
        path.join(__dirname, "certs", "signerKey.key"),
        "utf8"
      ),
    },
  });
 
  // Update the pass data as needed
  pass.serialNumber = serialNumber;
  pass.barcode = {
    format: "PKBarcodeFormatQR",
    message: `Updated pass for serial number ${serialNumber}`,
    messageEncoding: "iso-8859-1",
  };
 
  // Return the pass buffer
  return await pass.getAsBuffer();
};

This is the preamble to allow passes to update at all, but this does not enable us to update them from the servers end. In essence, this allows a pass to update if the user manually updates it but doesn't allow the server to trigger a pass update on the iPhone. To trigger a manual update and test if this is working for you or not go to your apple wallet app on the phone the pass is installed on, then click the pass then the three dots on the top right hand corner, then pass details and on this page that shows the details swipe down and you will see a loader pop up and it should query the above endpoints and update the pass.

Apple Wallet
iOS
Node.js

Part of series: Creating Custom Apple Wallet Cards: A Developer's Guide

View all posts in this series →