Home Platform Integrations Customers Pricing Blog
Sign in Request Access
Engineering 12 min read

Loyalty Programs in Apple Wallet and Google Wallet: A Technical Guide

PassKit, Google Wallet Objects API, and real-time push updates — a practical walkthrough for engineering teams adding mobile wallet passes to their loyalty platform.

Mobile wallet passes for loyalty programs have moved from "nice-to-have" to an expected feature for any retail chain with a meaningful mobile user base. The pitch is intuitive — your loyalty card lives in the member's iPhone or Android Wallet, displays their current balance, and updates in real time when they earn or redeem. No app install required. No friction at the register.

The implementation, however, involves two separate platform integrations with different models, different developer tooling, and different update mechanisms. This guide covers both: Apple Wallet (PassKit) and Google Wallet (Wallet Objects API).

Apple Wallet: PassKit Architecture

Apple Wallet passes are signed JSON files wrapped in a .pkpass bundle. The pass bundle contains a pass.json manifest describing the pass fields and display structure, one or more images (logo, strip, thumbnail), and a manifest file plus a signature generated with your Apple Pass Type ID certificate.

For a loyalty card pass, the relevant pass type is storeCard. A minimal pass.json for a loyalty program looks like:

{
  "passTypeIdentifier": "pass.com.yourchain.loyalty",
  "serialNumber": "mbr_00482917_loyalty",
  "teamIdentifier": "YOUR_TEAM_ID",
  "webServiceURL": "https://api.yourchain.com/wallet/",
  "authenticationToken": "vxwxd7J8AlNNFPS8k0a0FfUFtq0ewzV",
  "organizationName": "Your Chain",
  "description": "Your Chain Loyalty Card",
  "formatVersion": 1,
  "foregroundColor": "rgb(255, 255, 255)",
  "backgroundColor": "rgb(28, 43, 58)",
  "labelColor": "rgb(168, 188, 201)",
  "storeCard": {
    "primaryFields": [
      {
        "key": "points_balance",
        "label": "Points Balance",
        "value": "1,240",
        "changeMessage": "You earned %@ points on your last visit."
      }
    ],
    "secondaryFields": [
      {
        "key": "member_tier",
        "label": "Tier",
        "value": "Gold"
      },
      {
        "key": "member_id",
        "label": "Member ID",
        "value": "00482917"
      }
    ],
    "auxiliaryFields": [
      {
        "key": "points_to_next_tier",
        "label": "To Platinum",
        "value": "760 pts"
      }
    ],
    "barcode": {
      "message": "mbr_00482917",
      "format": "PKBarcodeFormatQR",
      "messageEncoding": "iso-8859-1"
    }
  }
}

Real-Time Pass Updates via Push

The real-time update capability is what makes a wallet pass meaningfully different from a static loyalty card image. When a member earns or redeems points, you want their pass to update within seconds, not hours.

Apple's push update mechanism requires registering your pass with Apple's Push Notification service for passes (APNs passes). The flow is: your loyalty platform fires a push notification to APNs, APNs wakes the pass on the device, the Wallet app makes a GET request to your webServiceURL endpoint to fetch the updated pass.json, and the pass displays the new values.

Your web service must implement three endpoints per the PassKit Web Service Reference:

  • POST /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} — called when a device registers to receive updates for a pass
  • DELETE /v1/devices/{deviceLibraryIdentifier}/registrations/{passTypeIdentifier}/{serialNumber} — called when a pass is removed from Wallet
  • GET /v1/passes/{passTypeIdentifier}/{serialNumber} — called by the device when it receives a push notification to fetch the updated pass

The update latency from your loyalty platform sending the APNs push to the member seeing the updated balance on their phone is typically 5–15 seconds under normal conditions. This is adequate for post-transaction balance updates; it doesn't support real-time balance display at the register during a transaction.

Google Wallet: Objects API Architecture

Google Wallet uses a different model that's worth understanding before you assume it works the same way as PassKit. Rather than a signed bundle sent to the device, Google Wallet objects live in Google's cloud and are accessed by the device through the Wallet app.

You create a Loyalty Class (the program template — your chain's branding, program name, tier structure) and Loyalty Objects (individual member records linked to the class). Members add the object to their Wallet via a "Save to Google Wallet" button that deep-links to their specific object.

Updating a member's pass is a PATCH or PUT request against the Loyalty Object endpoint:

PATCH https://walletobjects.googleapis.com/walletobjects/v1/loyaltyObject/{resourceId}

{
  "loyaltyPoints": {
    "balance": {
      "string": "1,240 pts"
    },
    "label": "Points"
  },
  "secondaryLoyaltyPoints": {
    "balance": {
      "string": "Gold"
    },
    "label": "Tier"
  }
}

The key architectural difference: Google Wallet objects are server-side. You don't push an update and wait for the device to fetch it — you update the object in Google's API, and the Wallet app reflects the change the next time it syncs. Push notifications to trigger immediate device updates are available through the sendMessage endpoint for Wallet Objects, which triggers a notification that prompts the user to open their Wallet.

Barcode Format Considerations

The barcode on the loyalty pass is the primary identification mechanism at the POS. Format choice matters:

  • QR Code: Higher data density, better scan reliability with phone screens. Preferred for most retail contexts. Use for member IDs longer than 12 characters or if you want to encode additional data in the barcode.
  • Code 128 (linear barcode): Compatible with a wider range of legacy barcode scanners. Required if your POS laser scanner doesn't support 2D barcode scanning. Most POS hardware newer than 2018 supports QR, but confirm before committing to format.
  • PDF417: An alternative 2D format that some grocery POS systems scan more reliably than QR due to scanner angle constraints.

The barcode message must encode a value your POS loyalty connector can resolve to a member record. The simplest reliable approach is the member ID in your loyalty platform's canonical format. Don't encode additional data in the barcode unless your POS integration layer is designed to parse it — a barcode scanner that reads extra fields it doesn't expect will either strip them silently or fail the scan.

Tier Badge Display

Displaying the member's current tier visually on the pass — not just as a text field — requires using the strip image or thumbnail image slot in Apple PassKit. The strip image is the background band on the pass; if you generate tier-specific strip images (Bronze/Silver/Gold/Platinum with distinct color treatments), the pass visually signals tier status at a glance.

This means your pass provisioning service needs to be able to generate or select the correct strip image at the time the pass is created or updated. If you're managing this with static images per tier, you need four image variants stored and a lookup at provisioning time. If a member upgrades to Gold, the pass update flow must replace the strip image, not just update the text fields — which requires re-signing the entire pass bundle (Apple) or updating the class reference (Google).

We're not saying dynamic tier images are required — text-only tier display works and is simpler to implement. We're saying that text-only tier display is less differentiated from a member experience standpoint. The visual identity of the tier on the pass is part of what makes it feel like a real status symbol rather than a data entry form.

Provisioning Flow Integration

Pass provisioning — the step where a member gets their pass added to their Wallet — typically happens through one of three entry points:

  1. Post-enrollment email: Member enrolls in the loyalty program, receives a welcome email with an "Add to Apple Wallet" or "Save to Google Wallet" button. Simplest flow, works for both platforms.
  2. In-app deep link: If the retailer has a mobile app, a CTA within the app triggers the wallet add flow. Requires the app to request the passes entitlement (iOS) or use the Google Pay API (Android).
  3. Post-transaction prompt: After an in-store purchase, the digital receipt or loyalty confirmation screen includes a wallet add prompt. Highest conversion moment — the member has just demonstrated engagement.

The provisioning URL for Apple Wallet is a direct link to the .pkpass file served from your web service. For Google Wallet, it's a deep link to the Wallet app containing an encoded JWT that Google's servers decode to identify which object to add. Both should be served over HTTPS and ideally behind your loyalty API so you can track provisioning events and attribute them to enrollment or engagement campaigns.

Operational Considerations

A few points that catch teams off guard:

Certificate rotation (Apple): Apple Pass Type ID certificates expire. A lapsed certificate means you can no longer sign new passes or push updates — all existing passes will stop updating. Set calendar reminders 90 days before certificate expiry. This sounds obvious; it's caught multiple production programs by surprise because the person who set up the certificate left the team.

Google Wallet API service account permissions: The Google API uses OAuth 2.0 service accounts. The service account must have the correct IAM permissions for the Wallet API. If you rotate service account keys (good practice) without updating your loyalty platform's credential store, all wallet updates will fail silently until someone notices that pass balances haven't been updating.

Testing on real devices: The PassKit simulator in Xcode and the Google Wallet test environment both have limitations. Pass push notifications in particular behave differently in test environments. Budget time for physical device testing with real APNs/Google push infrastructure before going live with pass updates.