> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zupy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Wallet Passes

> Generate Apple Wallet / Google Wallet passes for a customer's loyalty card or coupon via the Zupy API

Zupy can deliver a customer's **loyalty card** or **coupon** as an Apple Wallet / Google Wallet
pass. As a partner you only tell Zupy **which customer** and **which platform** — Zupy generates
the pass from the merchant's existing card/coupon design and template.

<Note>
  **Generation is reference-only.** The integration cannot personalize the pass (no custom fields,
  colors, or templates). Zupy builds it from the data and wallet template the merchant already
  configured — exactly the same pass the customer would get from the program landing page.
</Note>

## How it works

Both the program landing page and the partner API use the **same** generator
(`PassGenerationService`). The only difference is delivery:

| Flow                      | Endpoint                              | Delivery                                      |
| ------------------------- | ------------------------------------- | --------------------------------------------- |
| Landing page (in-browser) | downloads the `.pkpass` directly      | streams the file / redirects to Google        |
| **Partner API**           | `POST /api/v2/wallet/passes/loyalty/` | returns a `pass_url` you hand to the customer |

The `pass_url` is the same Apple `.pkpass` URL / Google Wallet save link the LoyaltyCard carries.
The customer opens it to add the card to their wallet.

## Generate a loyalty card pass

```
POST /api/v2/wallet/passes/loyalty/
```

**Request body:**

| Field         | Type   | Description                                     |
| ------------- | ------ | ----------------------------------------------- |
| `customer_id` | string | **Required.** Customer KSUID (the LoyaltyUser). |
| `platform`    | string | **Required.** `apple` or `google`.              |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.zupy.com/api/v2/wallet/passes/loyalty/" \
    -H "X-API-Key: zupy_pk_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{"customer_id": "2awTHloSJX7kGGprFerOOsvABcd", "platform": "apple"}'
  ```
</CodeGroup>

```json Response (201) theme={null}
{
  "data": {
    "id": "2cxSLnqYLZ0nJJsuIhuSSvyFGij",
    "pass_url": "https://api.zupy.com/wallet/apple/2cxSLnqYLZ0nJJsuIhuSSvyFGij.pkpass",
    "platform": "apple",
    "created_at": "2026-05-25T18:00:00Z"
  },
  "meta": {}
}
```

Deliver `pass_url` to the customer (link, button, QR). Opening it adds the card to their wallet.

## Generate a coupon pass

```
POST /api/v2/wallet/passes/coupons/
```

Same idea, plus the issued coupon to wrap:

| Field         | Type   | Description                                                                                                 |
| ------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| `customer_id` | string | **Required.** Customer KSUID.                                                                               |
| `coupon_id`   | string | **Required.** Issued coupon KSUID (a `RewardRedemption`, see [Coupon Lifecycle](/guides/coupon-lifecycle)). |
| `platform`    | string | **Required.** `apple` or `google`.                                                                          |

## Check pass status

```
GET /api/v2/wallet/passes/{pass_id}/status/
```

Returns whether the pass was added/removed by the customer (useful for engagement metrics).

## List a customer's passes

```
GET /api/v2/wallet/passes/?customer_id={id}
```

## Know if the customer installed the pass (and on which platform)

Before firing a push notification, you usually want to know whether the customer actually added
the pass to their wallet — and on which platform — so you can act intelligently (e.g. show an
"Add to Wallet" prompt instead of pushing into the void, or send an SMS fallback when no wallet
install exists).

```
GET /api/v2/customers/{customer_id}/wallet-devices/
```

The response separates **Apple Wallet** (per-device list, because PassKit registers each iPhone
explicitly) from **Google Wallet** (flat pass list, because Google syncs across devices
server-side and we have no per-device telemetry).

```json theme={null}
{
  "data": {
    "customer_id": "2abc...",
    "summary": {
      "apple":  { "installed": true,  "device_count": 1, "pass_count": 1 },
      "google": { "installed": false,                    "pass_count": 0 }
    },
    "apple_devices": [
      {
        "device_library_identifier": "5defd6b5d63fd0c3ed16690bbbce9709",
        "platform_hint": "ios",
        "locale": "pt-BR",
        "first_registered_at": "2026-05-26T07:13:25Z",
        "last_registered_at": "2026-05-26T07:13:25Z",
        "registered_passes": [
          {
            "serial_number": "40d96d49-ad9e-4b65-9675-6333d39ffcfb",
            "pass_type_identifier": "pass.com.zupy.clube.public",
            "pass_type": "coupon",
            "reward_name": "Vale Sobremesa",
            "last_push_sent": "2026-05-26T07:42:07Z"
          }
        ]
      }
    ],
    "google_passes": []
  },
  "meta": {}
}
```

<Tip>
  `summary.apple.installed` + `summary.google.installed` is the fastest signal: at least one
  side `true` means a `POST /api/v2/wallet/notifications/` call will reach the customer.
</Tip>

## Send a push notification to the wallet pass

Push an update + message into the customer's wallet pass. The notification **fans out
automatically to all platforms** the customer has the pass installed on — Apple (APNs silent
push → iPhone re-downloads the pkpass with the new back-field message) and Google (server-side
PATCH on the Wallet object → surfaces in the user's Google Wallet feed).

```
POST /api/v2/wallet/notifications/
```

**Request body**

| Field         | Type                | Required | Description                                                                                                                                                                       |
| ------------- | ------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customer_id` | string (KSUID)      | Yes      | The `LoyaltyUser.id` (same one you use everywhere else).                                                                                                                          |
| `type`        | string enum         | Yes      | One of `points_update`, `coupon_available`, or `program_update`. Controls how the message is categorized in the customer's wallet feed; the actual text is always your `message`. |
| `message`     | string (≤500 chars) | Yes      | The text the customer reads in the wallet pass back-field. Plain text only; emojis fine.                                                                                          |

**Response — 202 Accepted**

| Field             | Type           | Description                                                                                              |
| ----------------- | -------------- | -------------------------------------------------------------------------------------------------------- |
| `notification_id` | string (KSUID) | Async dispatch ID. Track it in logs; there's no GET endpoint to poll status (the fanout is best-effort). |
| `status`          | string         | Always `"queued"` on 202 — the Celery worker takes it from here.                                         |

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.zupy.com/api/v2/wallet/notifications/" \
    -H "X-API-Key: zupy_pk_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "customer_id": "2awTHloSJX7kGGprFerOOsvABcd",
      "type": "points_update",
      "message": "Você ganhou 1500 pontos! Saldo: 1520"
    }'
  ```

  ```python Python theme={null}
  response = requests.post(
      f"{BASE_URL}/wallet/notifications/",
      json={
          "customer_id": customer_id,
          "type": "points_update",
          "message": f"Você ganhou {amount} pontos! Saldo: {new_balance}",
      },
      headers=HEADERS,
  )
  notification_id = response.json()["data"]["notification_id"]
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch(`${BASE_URL}/wallet/notifications/`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({
      customer_id: customerId,
      type: "points_update",
      message: `Você ganhou ${amount} pontos! Saldo: ${newBalance}`,
    }),
  });
  const { data } = await response.json();
  console.log(`Queued: ${data.notification_id}`);
  ```
</CodeGroup>

### When to use each `type`

| `type`             | Use when                                                                                                                   | Example message                                 |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| `points_update`    | Points balance changed (earned, spent, manual adjust). Paired with the relevant operation (`/points/add/`, `redeem`, etc.) | `"Você ganhou 1500 pontos! Saldo: 1520"`        |
| `coupon_available` | New coupon was just issued for the customer — let them open the wallet pass and see it                                     | `"Seu cupom Sobremesa Grátis está disponível!"` |
| `program_update`   | Generic program-wide news (campaign, tier change, expiry warning) that isn't tied to a specific transaction                | `"Nova campanha — descontos esta semana"`       |

The choice doesn't change delivery behavior — it's metadata that helps the customer's wallet UI group similar notifications. Pick the one that best matches the trigger.

### Notification errors

| Status | Type               | When                                                                                |
| ------ | ------------------ | ----------------------------------------------------------------------------------- |
| 202    | —                  | Queued successfully. The async fanout decides per-platform delivery.                |
| 404    | `not-found`        | `customer_id` doesn't exist in your company                                         |
| 422    | `validation-error` | Invalid `type` (not in the enum), missing/empty `message`, or `message > 500` chars |

<Warning>
  This is a **best-effort fire-and-forget** dispatch. A 202 means we accepted the request, not that the push reached the customer's device. Reasons it may silently drop later:

  * The customer has no wallet pass installed (verify with [`GET /customers/{id}/wallet-devices/`](#know-if-the-customer-installed-the-pass-and-on-which-platform) first if you need a guarantee).
  * The customer uninstalled the pass after generation (Apple returns `BadDeviceToken` → registration is purged).
  * Rate-limit guard: max 10 notifications per customer per hour (Redis-tracked, returns 202 but logs the throttle).
</Warning>

### What the customer actually sees

* **iPhone (Apple Wallet)**: silent APNs push (no banner) → Wallet quietly re-downloads the pkpass → the back-field `"Última Mensagem"` updates to your `message`. The pass icon may pulse briefly on the lock screen depending on iOS settings.
* **Android (Google Wallet)**: server-side `objects.patch` → notification appears in the Google Wallet feed with the merchant's class branding. May trigger a system notification on the device depending on the user's Wallet notification settings.

Typical latency: **5-15 seconds** to APNs, **30-60 seconds** to Google. If the iPhone shows the old message after a push, the customer can pull-to-refresh on the pass — that forces a non-conditional `GET /v1/passes/.../` and bypasses any stale cache.

<Note>
  Wallet endpoints require a **read-write** API key (they create passes). Read-only keys get
  `403` on the create endpoints.
</Note>
