API Reference

The PerkPass API is JSON over HTTPS. Base URL: https://perkpass.co.uk. One request creates an installable Apple Wallet (.pkpass) and Google Wallet pass, returns a hosted share page, and lets you push updates to devices with a PUT.

Authentication

Pass your API key in the Authorization header. Find it in your dashboard. Only the prefix is stored — rotate to generate a new key.

Authorization: Bearer pk_live_<your_key>

Create a pass

POST/api/passes
curl -X POST https://perkpass.co.uk/api/passes \
  -H "Authorization: Bearer pk_live_<key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationName": "Bayroast Coffee",
    "logoText": "Bayroast",
    "barcodeValue": "MEMBER-12345",
    "barcodeFormat": "QR",
    "colorPreset": "dark",
    "headerFields": [{ "key": "points", "label": "POINTS", "value": "1,250" }],
    "primaryFields": [{ "key": "card", "label": "CARD", "value": "Coffee Rewards" }],
    "secondaryFields": [{ "key": "tier", "label": "TIER", "value": "Gold" }],
    "expirationDays": 365
  }'

Response

{
  "serialNumber": "9f3a1c…",
  "applePass": "<base64 .pkpass>",
  "googleSaveUrl": "https://pay.google.com/gp/v/save/…",
  "shareUrl": "https://perkpass.co.uk/p/9f3a1c…"
}

Add ?format=pkpass to stream the raw .pkpass binary instead of the JSON envelope.

Fetch a pass

GET/api/passes/:serial
curl https://perkpass.co.uk/api/passes/<serial> \
  -H "Authorization: Bearer pk_live_<key>"
{
  "serialNumber": "9f3a1c…",
  "passJson": { /* full stored pass body */ },
  "createdAt": "2026-06-01T12:00:00.000Z",
  "updatedAt": "2026-06-10T09:30:00.000Z",
  "shareUrl": "https://perkpass.co.uk/p/9f3a1c…",
  "googleSaveUrl": "https://pay.google.com/gp/v/save/…"
}

Update a pass

PUT/api/passes/:serial

Send the full new pass body. Set changeMessage on any field to fire a lock-screen banner when that field's value changes (%@ is replaced with the new value). Identical bodies are no-ops: no push, no quota cost, response is { unchanged: true }. Do not include serialNumber or authenticationToken in the body — server-owned, returns 400.

curl -X PUT https://perkpass.co.uk/api/passes/<serial> \
  -H "Authorization: Bearer pk_live_<key>" \
  -H "Content-Type: application/json" \
  -d '{
    "organizationName": "Bayroast Coffee",
    "barcodeValue": "MEMBER-12345",
    "barcodeFormat": "QR",
    "headerFields": [{
      "key": "points",
      "label": "POINTS",
      "value": "1,300",
      "changeMessage": "You hit %@ points. Free coffee unlocked!"
    }],
    "primaryFields": [{ "key": "card", "label": "CARD", "value": "Coffee Rewards" }]
  }'

Response

{
  "serialNumber": "9f3a1c…",
  "lastUpdated": "2026-06-14T10:12:00.000Z",
  "notifiedDevices": 3,
  "unchanged": false,
  "changedFields": ["headerFields.points"],
  "apple": { "pushed": 2 },
  "google": { "pushed": 1, "message": "You hit 1,300 points. Free coffee unlocked!" }
}

Binary endpoint

POST/api/pkpass

Same handler as /api/passes, but streams the raw application/vnd.apple.pkpass binary by default. Serial, Google save URL, and hosted share URL ride on response headers.

curl -X POST https://perkpass.co.uk/api/pkpass \
  -H "Authorization: Bearer pk_live_<key>" \
  -H "Content-Type: application/json" \
  --output member.pkpass \
  -d '{ "organizationName": "Acme", "barcodeValue": "ACME-1" }'

# Response headers:
#   Content-Type: application/vnd.apple.pkpass
#   X-Serial-Number: 9f3a1c…
#   X-Google-Save-Url: https://pay.google.com/gp/v/save/…
#   X-Pass-Url: https://perkpass.co.uk/p/9f3a1c…

Add ?format=json to get the JSON envelope from this endpoint instead.

Google Wallet redirect

GET/api/passes/:serial/google

Public (no auth). Returns a 302 redirect to the current Save to Google Wallet link for that serial. Use it as the stable href for an "Add to Google Wallet" button — the underlying JWT can rotate without breaking the link.

Hosted share page

GET/p/:serial

Device-aware install page. Auto-detects iOS, Android, and desktop and shows the right button (QR fallback on desktop). Use the URL returned in shareUrl.

Usage

GET/api/auth/usage
{
  "count": 4231,
  "limit": 100000,
  "remaining": 95769,
  "resetDate": "2026-07-01T00:00:00.000Z",
  "plan": "pro"
}

Pass fields

FieldTypeDescription
organizationNamestringIssuer name; shown as notification title. Falls back to logoText.
logoTextstringText next to the logo at the top of the pass.
descriptionstringAccessibility text (not visible). Defaults to logoText.
barcodeValuestringRequired. Value encoded in the barcode. Max 512 chars.
barcodeFormatQR | PDF417 | Aztec | Code128Default: QR.
colorPresetdark | blue | green | red | purple | orangeSolid preset color.
color / customColorhex stringPro only. Overrides the preset (e.g. "#0a2540").
primaryFieldsField[]Up to 2. Large value displayed front and center.
secondaryFieldsField[]Up to 4. Two-column layout under primary.
headerFieldsField[]Up to 3. Top-right of the pass (e.g. points).
backFieldsField[]Up to 20. Shown on the back of the pass.
Field.changeMessagestringLock-screen banner when this field's value changes. %@ = new value.
locationsLocation[]Up to 10 { latitude, longitude, altitude?, relevantText? } for geofencing.
expirationDate / expirationDaysISO string / 1–3650When the pass expires.
sharingProhibitedbooleanDefaults to true. Hides Apple Wallet share button.
logoURL / iconURL / thumbnailURL / stripURLhttps URL or data:image/png;base64,…Pro only. Custom imagery.

Pass styles

Every pass has an Apple Wallet style that controls its layout. Set it per template in Dashboard → Editor → Pass style (Apple), or override it per pass via the API with passStyle. Google Wallet automatically maps to its closest equivalent class (loyalty, offer, event, generic, boarding).

ValueUse forGoogle equivalent
storeCardLoyalty / membership / stamp cards (default)Loyalty
couponDiscounts, vouchers, promo offersOffer
eventTicketConcerts, classes, bookings (supports strip image)Event
boardingPassTravel / transit with origin → destinationBoarding
genericAnything else (IDs, gym cards, lockers)Generic
// POST /api/passes
{
  "passStyle": "eventTicket",
  "headerFields":   [{ "key": "event",   "label": "EVENT",   "value": "Summer Show" }],
  "primaryFields":  [{ "key": "name",    "label": "NAME",    "value": "Alex Doe" }],
  "secondaryFields":[{ "key": "seat",    "label": "SEAT",    "value": "A12" }],
  "auxiliaryFields":[{ "key": "doors",   "label": "DOORS",   "value": "19:00" }]
}

Changing styles re-arranges where fields render on the front of the card — you may want to retune labels/values per style. Existing passes keep the style they were issued with; only new passes pick up the change.

Push notifications

Every PUT that changes a field whose object includes changeMessage pushes a lock-screen banner to all registered devices on both Apple and Google Wallet. Use %@ as a placeholder for the new value.

Reserved notification field

Every pass ships with a reserved back field with key: "notification" and changeMessage: "%@". The Notifications page in the dashboard writes to it for every pass on your account at once; you can also write to it per-pass via the API:

// PUT body
{
  "backFields": [{
    "key": "notification",
    "label": "Notifications",
    "value": "Your order is ready for pickup.",
    "changeMessage": "%@"
  }]
}

Geofencing

Add up to 10 locations to surface the pass on the lock screen when the user is near a coordinate. Mirrored to both Apple Wallet and Google Wallet.

{
  "locations": [
    {
      "latitude": 37.7749,
      "longitude": -122.4194,
      "altitude": 16,
      "relevantText": "Welcome to our SF store!"
    }
  ]
}

Aliases & shortcuts

These optional aliases map onto the canonical fields so simpler payloads still work:

AliasMaps to
colorcustomColor (Pro)
titleprimaryFields[0].value
cardLabelprimaryFields[0].label
label / value (top-level)secondaryFields[0]
description (omitted)Defaults to logoText
PNG data URI (data:image/png;base64,…)logoURL / iconURL / thumbnailURL / stripURL

Errors

StatusMeaning
400Malformed JSON, or PUT body contained server-owned fields (serialNumber, authenticationToken).
401Missing or invalid API key.
403Pro-only field used on Free plan, or pass belongs to another account.
404Serial number not found.
422Validation failed — see `details` field.
429Monthly pass limit reached.
500Something went wrong on our side.
PerkPass

The wallet pass API for modern teams. Issue Apple & Google Wallet passes, push updates, and trigger lock‑screen messages from one request.

© 2026 PerkPass. Apple Wallet & Google Wallet are trademarks of their respective owners.Made for developers in the UK.