WalletPassAPI

Apple Wallet's poster layout for loyalty and membership passes

iOS 27 brings the poster look to loyalty cards, memberships, coupons and gift cards. What it is, who sees it, and how to issue one with WalletPassAPI.

· 5 min read

A coffee shop's loyalty card three ways: Apple Wallet's poster layout on iOS 27 with a photo of a cappuccino, the classic store card on older iPhones, and Google Wallet.

With iOS 27 and watchOS 27, Apple added a new pass style: Poster Generic. It gives membership, loyalty, coupon, rewards and gift card passes the poster look that Apple first made for event tickets: full-height artwork, large modern type, and the details people need laid out by Wallet itself.

This guide covers what the layout is, what goes where, what people on older iPhones see, and how to issue a poster pass with WalletPassAPI.

What changes for your pass

A classic Apple Wallet pass is a card with a coloured background, a logo, rows of fields and a barcode. The poster layout puts your artwork first. In Apple's words, it's "designed to highlight prominent artwork, modern typography, and thoughtful customer notifications".

Apple's Creating a Poster Generic Pass lists what Wallet adds by itself on a poster pass: business information, the barcode, terms and conditions, store locations and other useful details. You provide the content. Wallet decides how it's presented.

It suits passes people keep and come back to:

Poster Generic and event ticket posters

Apple has two poster styles, and they work differently:

Event ticket posterPoster Generic
Made forSports and live performancesMemberships, loyalty, coupons, gift cards and more
ContentSemantic tags (event name, venue, seats and so on) are requiredOrdinary pass fields, with no event-specific requirements
In pass.jsoneventTicket with semantic tagsposterGeneric

Poster Generic builds on the event ticket's presentation, but you choose which content to surface. You don't have to describe your pass as an event.

What goes where

The posterGeneric layout uses the same kinds of fields as other passes:

It has no secondary or auxiliary rows, and Apple's example shows a square QR code.

Who sees it

The poster layout needs iOS 27 or watchOS 27. Older devices don't understand posterGeneric, so Apple's approach is to include a second, classic style in the same pass: storeCard, generic, eventTicket or coupon. Wallet uses the poster when it can and the classic style when it can't. Everyone gets a working pass from the same download.

On Apple Watch, Apple notes that the strip image, the thumbnail and the back of the pass aren't shown.

Google Wallet has no poster layout. A pass built for both wallets shows Google's own layout on Android, from the same content.

Poster passes can carry up to two featured actions: buttons such as "Call", "Go to location" or "View membership benefits" that open a link, a phone number or Maps. They're covered in Featured actions in Apple Wallet.

Issue a poster pass with WalletPassAPI

You don't write pass.json or keep two layouts in step. Give a pass a background image and it gets the poster layout on iOS 27. Its style (here loyalty, a store card) becomes the classic layout for older iPhones. Both are built, signed and kept up to date from one pass.

1. Upload the artwork

Upload the image as a background with Upload an image. It's resized to 1074 × 1344, a 4:5 portrait, so crop to that shape first. Your fields sit over the artwork, so choose an image that still reads well with text on it.

curl "https://api.walletpassapi.com/v1/assets?type=background" \
  -H "Authorization: Bearer $WALLET_API_KEY" \
  -H "Idempotency-Key: acme-poster-autumn" \
  -H "Content-Type: image/png" \
  --data-binary @poster.png
import { readFile } from "node:fs/promises";

const res = await fetch("https://api.walletpassapi.com/v1/assets?type=background", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WALLET_API_KEY}`,
    "Content-Type": "image/png",
    "Idempotency-Key": "acme-poster-autumn",
  },
  body: await readFile("poster.png"),
});
const data = await res.json();
import os

import requests

res = requests.post(
    "https://api.walletpassapi.com/v1/assets",
    headers={"Authorization": f"Bearer {os.environ['WALLET_API_KEY']}", "Idempotency-Key": "acme-poster-autumn", "Content-Type": "image/png"},
    params={
        "type": "background",
    },
    data=open("poster.png", "rb"),
)
data = res.json()
<?php

$ch = curl_init("https://api.walletpassapi.com/v1/assets?type=background");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("WALLET_API_KEY"),
        "Content-Type: image/png",
        "Idempotency-Key: acme-poster-autumn",
    ],
    CURLOPT_POSTFIELDS => file_get_contents("poster.png"),
    CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);

The answer's reference is what the pass uses, such as asset:asset_k3m9q2w7x4n8p1r6.

2. Create the pass

Put the reference in backgroundURL, and fill the fields as you would for any pass. The poster can show up to four primary fields.

curl "https://api.walletpassapi.com/v1/passes" \
  -H "Authorization: Bearer $WALLET_API_KEY" \
  -H "Idempotency-Key: member-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationName": "Acme Coffee",
    "logoText": "Acme",
    "description": "Acme Coffee loyalty card",
    "style": "loyalty",
    "backgroundURL": "asset:asset_k3m9q2w7x4n8p1r6",
    "headerFields": [
      {
        "key": "tier",
        "label": "Tier",
        "value": "Gold"
      }
    ],
    "primaryFields": [
      {
        "key": "points",
        "label": "Points",
        "value": "120",
        "changeMessage": "You now have %@ points"
      },
      {
        "key": "member",
        "label": "Member",
        "value": "Sam Taylor"
      }
    ],
    "footerFields": [
      {
        "key": "since",
        "label": "Member since",
        "value": "2024"
      }
    ],
    "backFields": [
      {
        "key": "terms",
        "label": "Terms",
        "value": "Points expire 12 months after they'\''re earned."
      }
    ],
    "barcodeValue": "MEMBER-1042",
    "barcodeFormat": "QR"
  }'
const res = await fetch("https://api.walletpassapi.com/v1/passes", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WALLET_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "member-1042",
  },
  body: JSON.stringify({
    "organizationName": "Acme Coffee",
    "logoText": "Acme",
    "description": "Acme Coffee loyalty card",
    "style": "loyalty",
    "backgroundURL": "asset:asset_k3m9q2w7x4n8p1r6",
    "headerFields": [
      {
        "key": "tier",
        "label": "Tier",
        "value": "Gold"
      }
    ],
    "primaryFields": [
      {
        "key": "points",
        "label": "Points",
        "value": "120",
        "changeMessage": "You now have %@ points"
      },
      {
        "key": "member",
        "label": "Member",
        "value": "Sam Taylor"
      }
    ],
    "footerFields": [
      {
        "key": "since",
        "label": "Member since",
        "value": "2024"
      }
    ],
    "backFields": [
      {
        "key": "terms",
        "label": "Terms",
        "value": "Points expire 12 months after they're earned."
      }
    ],
    "barcodeValue": "MEMBER-1042",
    "barcodeFormat": "QR"
  }),
});
const data = await res.json();
import os

import requests

res = requests.post(
    "https://api.walletpassapi.com/v1/passes",
    headers={"Authorization": f"Bearer {os.environ['WALLET_API_KEY']}", "Idempotency-Key": "member-1042"},
    json={
        "organizationName": "Acme Coffee",
        "logoText": "Acme",
        "description": "Acme Coffee loyalty card",
        "style": "loyalty",
        "backgroundURL": "asset:asset_k3m9q2w7x4n8p1r6",
        "headerFields": [
            {
                "key": "tier",
                "label": "Tier",
                "value": "Gold",
            },
        ],
        "primaryFields": [
            {
                "key": "points",
                "label": "Points",
                "value": "120",
                "changeMessage": "You now have %@ points",
            },
            {
                "key": "member",
                "label": "Member",
                "value": "Sam Taylor",
            },
        ],
        "footerFields": [
            {
                "key": "since",
                "label": "Member since",
                "value": "2024",
            },
        ],
        "backFields": [
            {
                "key": "terms",
                "label": "Terms",
                "value": "Points expire 12 months after they're earned.",
            },
        ],
        "barcodeValue": "MEMBER-1042",
        "barcodeFormat": "QR",
    },
)
data = res.json()
<?php

$ch = curl_init("https://api.walletpassapi.com/v1/passes");
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => "POST",
    CURLOPT_HTTPHEADER => [
        "Authorization: Bearer " . getenv("WALLET_API_KEY"),
        "Content-Type: application/json",
        "Idempotency-Key: member-1042",
    ],
    CURLOPT_POSTFIELDS => json_encode([
        "organizationName" => "Acme Coffee",
        "logoText" => "Acme",
        "description" => "Acme Coffee loyalty card",
        "style" => "loyalty",
        "backgroundURL" => "asset:asset_k3m9q2w7x4n8p1r6",
        "headerFields" => [
            [
                "key" => "tier",
                "label" => "Tier",
                "value" => "Gold",
            ],
        ],
        "primaryFields" => [
            [
                "key" => "points",
                "label" => "Points",
                "value" => "120",
                "changeMessage" => "You now have %@ points",
            ],
            [
                "key" => "member",
                "label" => "Member",
                "value" => "Sam Taylor",
            ],
        ],
        "footerFields" => [
            [
                "key" => "since",
                "label" => "Member since",
                "value" => "2024",
            ],
        ],
        "backFields" => [
            [
                "key" => "terms",
                "label" => "Terms",
                "value" => "Points expire 12 months after they're earned.",
            ],
        ],
        "barcodeValue" => "MEMBER-1042",
        "barcodeFormat" => "QR",
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);

The answer includes a shareUrl: send it to the holder. On an iPhone it opens Apple Wallet, on Android Google Wallet, and on a computer it shows a QR code to scan.

3. Keep it current

Send the pass again with Update a pass when something changes, such as the points. Installed passes refresh on holders' phones, and a field with a changeMessage shows a notification. You can swap the artwork the same way, by uploading a new background and sending the new reference. Updating and notifying covers this in full.

You can also design the pass in the dashboard's editor, which previews the poster and, with a switch, the older iPhone layout.

Checklist

Questions

Which iPhones show the poster layout?

iPhones on iOS 27 or later, and Apple Watch on watchOS 27. Older devices show the classic style that the same pass also carries.

Do I need a separate pass for older iPhones?

No. One pass holds both the poster and a classic style (store card, generic, event ticket or coupon), and Wallet uses the poster where it can. With WalletPassAPI, the pass's style is the classic one.

Does Google Wallet have a poster layout?

No. Google Wallet shows the same pass in its own layout, without the poster artwork.

How is Poster Generic different from the event ticket poster?

Event ticket posters are for sports and live performances, and need semantic tags describing the event. Poster Generic uses ordinary pass fields, so it suits memberships, loyalty cards, coupons and gift cards.

What size should the poster artwork be?

With WalletPassAPI, a 4:5 portrait image: it's resized to 1074 × 1344. Crop it to that shape before uploading, and choose artwork that reads well with text over it.

Further reading