Skip to main content

guides/pass-types.md

# Pass Types

This guide is a short reference for the five pass types `wallet_passes`
supports, the platform-specific resources each one maps to, and the
`PassData` fields that are meaningful per type. The library exposes pass
type as a single atom on `PassData.pass_type` and threads it through both
the Apple `.pkpass` builder and the Google Wallet API client.

Forward links:

- [Getting Started](getting-started.md) for a full first-pass walkthrough.
- [Apple Wallet](apple-wallet.md) for the Apple style-key behaviour.
- [Google Wallet](google-wallet.md) for class/object semantics.

## Overview

A "pass type" describes the shape of the pass: an event ticket renders
differently from a boarding pass, and a loyalty card differently again.
Both Apple and Google model this, but they expose it in different shapes
of their API. This library normalises both behind a single atom on
`PassData`:

```elixir
%WalletPasses.PassData{pass_type: :event_ticket, ...}
```

The default is `:event_ticket`. The five supported atoms are
`:event_ticket`, `:boarding_pass`, `:store_card`, `:coupon`, and
`:generic` (note: `:store_card`, not `:loyalty` — see the table below).

## Concepts

### Apple: a top-level "style key" inside `pass.json`

Apple's `pass.json` carries exactly one of `eventTicket`, `boardingPass`,
`storeCard`, `coupon`, or `generic` as a top-level key. Whichever key is
present determines how iOS lays out the pass; the value is the
"pass-structure dictionary" that holds the field arrays (`headerFields`,
`primaryFields`, `secondaryFields`, `auxiliaryFields`, `backFields`) and,
for boarding passes only, the `transitType`. This library picks the
correct top-level key from `pass_data.pass_type`.

### Google: a different resource per type

Google Wallet's REST API exposes *five different resource families*. Each
pass type has its own class endpoint, its own object endpoint, and its
own JWT payload key for the Save-to-Wallet link. There is no single
"pass" endpoint; you must hit the right resource for the type you want.
This library resolves all three from `pass_data.pass_type` via
`WalletPasses.PassType` so callers don't have to think about it.

## Type Mapping

| `pass_type`       | Apple style key | Google class       | Google object       | Save-URL JWT key      | Class ID suffix |
|-------------------|-----------------|--------------------|---------------------|------------------------|-----------------|
| `:event_ticket`   | `eventTicket`   | `eventTicketClass` | `eventTicketObject` | `eventTicketObjects`   | `event_class`   |
| `:boarding_pass`  | `boardingPass`  | `flightClass`      | `flightObject`      | `flightObjects`        | `flight_class`  |
| `:store_card`     | `storeCard`     | `loyaltyClass`     | `loyaltyObject`     | `loyaltyObjects`       | `loyalty_class` |
| `:coupon`         | `coupon`        | `offerClass`       | `offerObject`       | `offerObjects`         | `offer_class`   |
| `:generic`        | `generic`       | `genericClass`     | `genericObject`     | `genericObjects`       | `generic_class` |

Two naming asymmetries to note:

- Apple calls a loyalty pass a "store card" (`storeCard`); Google calls
  it "loyalty" (`loyaltyClass`/`loyaltyObject`). This library uses the
  atom `:store_card` — pick whichever name you prefer, but the atom is
  `:store_card`.
- Apple calls flight/transit passes "boarding pass"; Google calls the
  same resource `flightClass`/`flightObject`. The library accepts
  `:boarding_pass` for both.

## Field Applicability

Most `PassData` fields apply to every pass type. The field arrays
(`header_fields`, `primary_fields`, `secondary_fields`,
`auxiliary_fields`, `back_fields`) are universal — they always populate
the corresponding Apple pass-structure section, and on Google they
flatten into `textModulesData` entries (Google doesn't have separate
"primary/secondary" zones in the same way).

The fields below are either type-conditional or get mapped to different
Google object fields depending on type:

| `PassData` field    | `:event_ticket`            | `:boarding_pass`          | `:store_card`            | `:coupon`                 | `:generic`                                        |
|---------------------|----------------------------|---------------------------|--------------------------|---------------------------|---------------------------------------------------|
| `holder_name`       | Google: `ticketHolderName` | Google: `passengerName`   | Google: `accountName`    | not mapped                | Google: `header.defaultValue` (top-line text)     |
| `transit_type`      | ignored                    | required (defaults `:air`) | ignored                  | ignored                   | ignored                                           |
| `event_name`        | suggested for class title  | suggested for class title | suggested for class title | suggested for class title | suggested for class title                         |

A few notes on the above:

- **`holder_name` mapping.** On Apple, `holder_name` isn't emitted
  directly into `pass.json` — surface it through your fields (e.g. a
  primary or secondary field) if you want it visible. On Google, the
  library writes it into a type-appropriate Google object property as
  shown.
- **`event_name` is a `PassData` field, but Apple doesn't read it.**
  It's intended as data your `PassDataProvider` carries through to the
  Google class builder, where it maps to the type-specific class title
  field (see "Class-shape differences" below). On Apple, set the
  visible title through `Apple.Visual.logo_text` and `description`.
- **NFC fields, location fields, dates, and barcode fields are
  type-independent.** They apply identically to every pass type. See
  the [NFC & Smart Tap](nfc.md) guide for NFC specifics.

## `transit_type` for `:boarding_pass`

When `pass_type: :boarding_pass`, the Apple builder writes a
`transitType` key into the pass-structure dictionary. The atom on
`PassData.transit_type` maps to Apple's `PKTransitType*` enum:

| `transit_type` atom | Apple `transitType` value |
|---------------------|---------------------------|
| `:air`              | `PKTransitTypeAir`        |
| `:boat`             | `PKTransitTypeBoat`       |
| `:bus`              | `PKTransitTypeBus`        |
| `:train`            | `PKTransitTypeTrain`      |
| `:generic`          | `PKTransitTypeGeneric`    |

`nil` defaults to `:air`. The field is ignored for every pass type other
than `:boarding_pass` — setting it on a `:store_card` won't produce
`transitType` in the output. Google's `flightClass`/`flightObject`
resources don't take an equivalent enum; the library just emits the
flight resource shape and Google infers transit context from the
resource type itself.

```elixir
%WalletPasses.PassData{
  serial_number: "BP-001",
  pass_type: :boarding_pass,
  transit_type: :train,
  # ...
}
```

## Class-Shape Differences

Google's class resource has a "title" field with a different name per
type. The library reads `class_config.event_name` and writes it into
the right field for the pass type you're creating:

| `pass_type`       | Class title field | Localized sibling          |
|-------------------|-------------------|----------------------------|
| `:event_ticket`   | `eventName`       | (already `LocalizedString`) |
| `:boarding_pass`  | (no class title field — flight details carry the identity) | — |
| `:store_card`     | `programName`     | `localizedProgramName`     |
| `:coupon`         | `title`           | `localizedTitle`           |
| `:generic`        | (no class title field) | —                     |

Two patterns to internalise:

- `:event_ticket` carries its title as a native `LocalizedString` field
  (`eventName`). Translations populate `translatedValues` directly.
- `:store_card` and `:coupon` carry the title as a *plain string* with a
  separate localized sibling. The library always writes the plain field
  and adds the sibling (`localizedProgramName`, `localizedTitle`) only
  when at least one translation matches. See the
  [Localization](localization.md) guide's "plain + localized sibling"
  section for the full pattern.

The same `event_name` value drives the class title regardless of type —
the library picks the destination field for you:

```elixir
# Drives "eventName" on an event-ticket class.
WalletPasses.google_save_url(pass_data, google_visual,
  class_config: %{
    id: "summer_2026",
    issuer_name: "Festival Co",
    event_name: "Summer Music Festival",
    pass_type: :event_ticket
  }
)

# Drives "programName" on a loyalty class.
WalletPasses.google_save_url(pass_data, google_visual,
  class_config: %{
    id: "rewards_v1",
    issuer_name: "Coffee Co",
    event_name: "Rewards Program",
    pass_type: :store_card
  }
)

# Drives "title" on an offer class.
WalletPasses.google_save_url(pass_data, google_visual,
  class_config: %{
    id: "summer_promo",
    issuer_name: "Summer Co",
    event_name: "20% Off Everything",
    pass_type: :coupon
  }
)
```

`:boarding_pass` and `:generic` class objects don't take a single
"title" field — the library leaves `event_name` out of the class JSON
for those types. Configure identity through type-specific fields on
your own (flight details for boarding passes; field arrays for
generic).

## API Reference

`WalletPasses.PassType` is a thin lookup module. Every function takes a
single pass-type atom and returns a string.

| Function                       | Returns (for `:event_ticket`) | Used by                                    |
|--------------------------------|-------------------------------|--------------------------------------------|
| `apple_style_key/1`            | `"eventTicket"`               | Apple pass.json top-level structure key    |
| `google_object_type/1`         | `"eventTicketObject"`         | Google object resource path                |
| `google_class_type/1`          | `"eventTicketClass"`          | Google class resource path                 |
| `google_save_objects_key/1`    | `"eventTicketObjects"`        | Save-URL JWT payload key                   |
| `google_class_suffix/1`        | `"event_class"`               | Default class ID suffix when none provided |
| `all/0`                        | `[:event_ticket, :boarding_pass, :store_card, :coupon, :generic]` | Validation / enumeration |

```elixir
iex> WalletPasses.PassType.apple_style_key(:boarding_pass)
"boardingPass"

iex> WalletPasses.PassType.google_class_type(:store_card)
"loyaltyClass"

iex> WalletPasses.PassType.google_class_suffix(:coupon)
"offer_class"

iex> WalletPasses.PassType.all()
[:event_ticket, :boarding_pass, :store_card, :coupon, :generic]
```

You generally don't need to call these directly — they're invoked by the
builders. They're public so you can switch on them in your own
`PassDataProvider` or wire them into custom telemetry without
hard-coding string literals.