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
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
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
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
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
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.
Usage
{
"count": 4231,
"limit": 100000,
"remaining": 95769,
"resetDate": "2026-07-01T00:00:00.000Z",
"plan": "pro"
}Pass fields
| Field | Type | Description |
|---|---|---|
| organizationName | string | Issuer name; shown as notification title. Falls back to logoText. |
| logoText | string | Text next to the logo at the top of the pass. |
| description | string | Accessibility text (not visible). Defaults to logoText. |
| barcodeValue | string | Required. Value encoded in the barcode. Max 512 chars. |
| barcodeFormat | QR | PDF417 | Aztec | Code128 | Default: QR. |
| colorPreset | dark | blue | green | red | purple | orange | Solid preset color. |
| color / customColor | hex string | Pro only. Overrides the preset (e.g. "#0a2540"). |
| primaryFields | Field[] | Up to 2. Large value displayed front and center. |
| secondaryFields | Field[] | Up to 4. Two-column layout under primary. |
| headerFields | Field[] | Up to 3. Top-right of the pass (e.g. points). |
| backFields | Field[] | Up to 20. Shown on the back of the pass. |
| Field.changeMessage | string | Lock-screen banner when this field's value changes. %@ = new value. |
| locations | Location[] | Up to 10 { latitude, longitude, altitude?, relevantText? } for geofencing. |
| expirationDate / expirationDays | ISO string / 1–3650 | When the pass expires. |
| sharingProhibited | boolean | Defaults to true. Hides Apple Wallet share button. |
| logoURL / iconURL / thumbnailURL / stripURL | https 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).
| Value | Use for | Google equivalent |
|---|---|---|
| storeCard | Loyalty / membership / stamp cards (default) | Loyalty |
| coupon | Discounts, vouchers, promo offers | Offer |
| eventTicket | Concerts, classes, bookings (supports strip image) | Event |
| boardingPass | Travel / transit with origin → destination | Boarding |
| generic | Anything 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:
| Alias | Maps to |
|---|---|
| color | customColor (Pro) |
| title | primaryFields[0].value |
| cardLabel | primaryFields[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
| Status | Meaning |
|---|---|
| 400 | Malformed JSON, or PUT body contained server-owned fields (serialNumber, authenticationToken). |
| 401 | Missing or invalid API key. |
| 403 | Pro-only field used on Free plan, or pass belongs to another account. |
| 404 | Serial number not found. |
| 422 | Validation failed — see `details` field. |
| 429 | Monthly pass limit reached. |
| 500 | Something went wrong on our side. |