Skip to main content
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.
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.

How it works

Both the program landing page and the partner API use the same generator (PassGenerationService). The only difference is delivery:
FlowEndpointDelivery
Landing page (in-browser)downloads the .pkpass directlystreams the file / redirects to Google
Partner APIPOST /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:
FieldTypeDescription
customer_idstringRequired. Customer KSUID (the LoyaltyUser).
platformstringRequired. apple or google.
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"}'
Response (201)
{
  "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:
FieldTypeDescription
customer_idstringRequired. Customer KSUID.
coupon_idstringRequired. Issued coupon KSUID (a RewardRedemption, see Coupon Lifecycle).
platformstringRequired. 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).
{
  "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": {}
}
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.

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
FieldTypeRequiredDescription
customer_idstring (KSUID)YesThe LoyaltyUser.id (same one you use everywhere else).
typestring enumYesOne 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.
messagestring (≤500 chars)YesThe text the customer reads in the wallet pass back-field. Plain text only; emojis fine.
Response — 202 Accepted
FieldTypeDescription
notification_idstring (KSUID)Async dispatch ID. Track it in logs; there’s no GET endpoint to poll status (the fanout is best-effort).
statusstringAlways "queued" on 202 — the Celery worker takes it from here.
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"
  }'
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"]
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}`);

When to use each type

typeUse whenExample message
points_updatePoints balance changed (earned, spent, manual adjust). Paired with the relevant operation (/points/add/, redeem, etc.)"Você ganhou 1500 pontos! Saldo: 1520"
coupon_availableNew 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_updateGeneric 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

StatusTypeWhen
202Queued successfully. The async fanout decides per-platform delivery.
404not-foundcustomer_id doesn’t exist in your company
422validation-errorInvalid type (not in the enum), missing/empty message, or message > 500 chars
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/ 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).

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.
Wallet endpoints require a read-write API key (they create passes). Read-only keys get 403 on the create endpoints.