API Documentation
Complete reference for the WalletWallet API. Generate signed Apple Wallet passes with a single HTTP request.
https://api.walletwallet.dev What each field does
Edit any field and see the pass update live
Text next to the logo (top-left)
No header fields
No primary fields
No secondary fields
No back fields
Overrides color preset
Wide banner image. Use secondary fields for readable text when using this option.
Overview
The WalletWallet API lets you generate signed Apple Wallet (.pkpass) files programmatically. Send a POST request with your pass data and receive a ready-to-distribute pass file.
All requests use JSON bodies and return JSON responses, except the pass creation endpoint which returns a binary .pkpass file on success.
Authentication
All API requests require a valid API key. Include it in the Authorization header using the Bearer scheme:
Authorization: Bearer ww_live_<your_key>
API keys follow the format ww_live_ followed by 32 hexadecimal characters. You can get one instantly from the signup page.
/api/auth/usage
Returns your current monthly usage statistics. Requires authentication.
Headers
| Header | Value |
|---|---|
| Authorization | Bearer ww_live_<your_key> |
Response
{
"count": 150,
"limit": 1000,
"remaining": 850,
"resetDate": "2026-03-01",
"plan": "free"
} curl https://api.walletwallet.dev/api/auth/usage \
-H "Authorization: Bearer ww_live_<your_key>" /api/pkpass
Generates a signed Apple Wallet pass (.pkpass file). This is the primary endpoint. Requires authentication.
Headers
| Header | Value |
|---|---|
| Content-Type | application/json |
| Authorization | Bearer ww_live_<your_key> |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| barcodeValue | string | Yes | Data encoded in the barcode. Max 512 characters. |
| barcodeFormat | string | Yes | QR PDF417 Aztec Code128 |
| logoText | string | No | Text next to the logo (top-left of pass). |
| description | string | No | Accessibility text (not visible). Defaults to logoText. |
| organizationName | string | No | Issuing organization shown in Wallet under the pass title. Max 64 characters. Falls back to the account default when omitted. |
| primaryFields | array | No | Main content fields. Array of {label, value, changeMessage?} objects. changeMessage is the lock-screen banner template fired when this field's value changes — see Update Pass. |
| secondaryFields | array | No | Fields below primary. Array of {label, value, changeMessage?} objects. |
| headerFields | array | No | Top-right header area. Array of {label, value, changeMessage?} objects. |
| backFields | array | No | Back of pass. Array of {label, value, changeMessage?} objects. |
| locations | array | No | Up to 10 geofences that surface the pass on the lock screen when the device is nearby. Array of {latitude, longitude, altitude?, relevantText?} objects. Latitude is -90…90, longitude -180…180, relevantText ≤ 128 chars. |
| colorPreset | string | No |
Color theme:
dark blue green red purple orange.
Defaults to dark.
|
| expirationDays | number | No |
Pass expires after this many days. Common presets:
30,
90,
365.
Any integer between
1 and
3650
is accepted.
|
| color Pro | string | No | Custom hex background color, e.g. #1e40af. Overrides colorPreset. |
| logoURL Pro | string | No | Custom logo image. Must use HTTPS — HTTP URLs are rejected. Also accepts PNG data URIs (data:image/png;base64,...). Private/internal addresses are not allowed. |
| title Legacy | string | No | Legacy shortcut. Sets primaryFields[0].value and logoText if those aren't set. |
| cardLabel Legacy | string | No | Legacy shortcut. Sets primaryFields[0].label. Defaults to CARD. |
| label Legacy | string | No | Legacy shortcut. Sets secondaryFields[0].label. |
| value Legacy | string | No | Legacy shortcut. Sets secondaryFields[0].value. |
| thumbnailURL Pro | string | No | Image shown top-right of the pass. HTTPS URL or PNG data URI. |
| stripURL Pro | string | No | Wide banner image behind the primary field. Switches pass to store card layout. HTTPS URL or PNG data URI. |
| iconURL Pro | string | No | Replaces the default icon.png shown in iOS lock-screen notifications. Distinct from logoURL, which renders on the pass face. HTTPS URL or PNG data URI. |
At least one of logoText, primaryFields, or title must be provided.
Response
application/vnd.apple.pkpass file. Save as .pkpass.
Response Headers
| Header | Description |
|---|---|
| Content-Type | application/vnd.apple.pkpass |
| Content-Disposition | Suggested filename, e.g. attachment; filename="card.pkpass" |
| X-Serial-Number | Server-generated serial for the new pass. Save this if you plan to send updates — you'll pass it to PUT /api/pkpass/<serial>. The same value is also baked into pass.json. CORS-exposed. |
curl -X POST https://api.walletwallet.dev/api/pkpass \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ww_live_<your_key>" \
-d '{
"barcodeValue": "MEMBER-12345",
"barcodeFormat": "QR",
"logoText": "Membership Card"
}' \
-o membership.pkpass curl -X POST https://api.walletwallet.dev/api/pkpass \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ww_live_<your_key>" \
-d '{
"barcodeValue": "LOYALTY-98765",
"barcodeFormat": "QR",
"logoText": "Bayroast Coffee",
"description": "Loyalty card for Bayroast Coffee",
"primaryFields": [{"label": "CARD", "value": "Coffee Rewards"}],
"secondaryFields": [{"label": "TIER", "value": "Gold Status"}],
"headerFields": [{"label": "BALANCE", "value": "$25.00"}],
"colorPreset": "green",
"expirationDays": 365
}' \
-o rewards.pkpass curl -X POST https://api.walletwallet.dev/api/pkpass \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ww_live_<your_key>" \
-d '{
"barcodeValue": "VIP-001",
"barcodeFormat": "QR",
"logoText": "VIP Access",
"primaryFields": [{"label": "PASS", "value": "VIP Access"}],
"color": "#8B4513",
"logoURL": "https://example.com/logo.png"
}' \
-o vip.pkpass /api/pkpass/<serial>
Updates a previously-issued pass. Devices that have it installed receive an Apple Push Notification within seconds; Wallet refreshes the pass in place with a lock-screen banner. Requires authentication and ownership of the serial.
How updates reach the device
The serial number you got back from POST /api/pkpass (either in the X-Serial-Number header or inside pass.json) is your update handle. PUT a new body with that serial — every device with the pass installed gets pushed and pulls the new content. The actual lock-screen text comes from any field's changeMessage; without one, iOS shows the default "Pass Updated".
Headers
| Header | Value |
|---|---|
| Content-Type | application/json |
| Authorization | Bearer ww_live_<your_key> |
Request Body
Same shape as POST /api/pkpass: send the full pass spec. The server replaces the stored body, recomputes a content hash, and fans out push notifications to registered devices.
- The URL path's
<serial>identifies the pass — do not includeserialNumberin the body. authenticationTokenis server-owned and immutable once issued; including it in the body returns 400.- To surface a custom lock-screen banner on update, add
changeMessageon the field whose value is changing (e.g."You earned %@ points"). iOS substitutes%@with the new value. - An identical body returns
{ unchanged: true }with no push and no quota impact — safe to retry.
Response
Body change accepted. APNs fan-out runs in the background.
{
"serialNumber": "8f4c3a2e-...",
"lastUpdated": 1778538208273,
"notifiedDevices": 3,
"unchanged": false
} If the body is byte-equivalent to the stored one, no push fires and no usage counts:
{
"serialNumber": "8f4c3a2e-...",
"lastUpdated": 1778538208273,
"notifiedDevices": 0,
"unchanged": true
} serialNumber / authenticationToken) curl -X PUT https://api.walletwallet.dev/api/pkpass/<serial> \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ww_live_<your_key>" \
-d '{
"barcodeValue": "LOYALTY-98765",
"barcodeFormat": "QR",
"logoText": "Bayroast Coffee",
"primaryFields": [{"label": "CARD", "value": "Coffee Rewards"}],
"secondaryFields": [
{
"label": "POINTS",
"value": "250",
"changeMessage": "You now have %@ points"
}
],
"colorPreset": "green"
}' Barcode Formats
| Format | Type | Best For |
|---|---|---|
| QR | 2D square | General purpose, high data capacity, most common |
| PDF417 | 2D stacked | Boarding passes, ID cards, government documents |
| Aztec | 2D square | Transit tickets, compact spaces, no quiet zone needed |
| Code128 | 1D linear | Retail, inventory, shipping labels |
Color Presets
Available on all plans. Use the colorPreset field.
Pro plan: Use the color field with any hex value (e.g. #1e40af) to set a fully custom background color.
Rate Limits
| Plan | Passes / Month | Custom Color | Custom Logo | Price |
|---|---|---|---|---|
| Free | 1,000 | No | No | $0 |
| Pro | 100,000 | Yes | Yes | $19/mo |
Usage resets on the 1st of each month (UTC). You can check your current usage at any time via the /api/auth/usage endpoint.
POST always counts. PUT only counts when the body actually changes — an unchanged PUT is free, no push fires, no quota moves. Devices polling the Wallet web service do not count either.
When you exceed your limit, the API returns a 429 response with the reset date:
{
"error": "Rate limit exceeded",
"resetDate": "2026-03-01",
"message": "Monthly limit reached. Resets on 2026-03-01"
} Errors
All error responses return JSON with an error field:
{
"error": "Error message describing the issue"
} HTTP Status Codes
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Bad request — invalid input, malformed JSON, or validation failure |
| 401 | Unauthorized — missing or invalid API key |
| 404 | Not found — endpoint does not exist |
| 405 | Method not allowed — wrong HTTP method |
| 429 | Rate limit exceeded — monthly quota used up |
| 500 | Internal server error |
Common Validation Errors
| Cause | Error Message |
|---|---|
| Missing required field | barcodeValue is required |
| Invalid barcode format | barcodeFormat must be one of: QR, PDF417, Aztec, Code128 |
| Title too long | title must be 64 characters or less |
| Custom color on free plan | color is only available on the Pro plan |
| Invalid expiration | expirationDays must be between 1 and 3650 |
Code Examples
const response = await fetch('https://api.walletwallet.dev/api/pkpass', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ww_live_<your_key>'
},
body: JSON.stringify({
barcodeValue: 'TICKET-789',
barcodeFormat: 'QR',
logoText: 'Event Ticket',
primaryFields: [{ label: 'EVENT', value: 'Concert' }],
secondaryFields: [{ label: 'Seat', value: 'A-23' }]
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error);
}
// Browser: trigger download
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'ticket.pkpass';
a.click();
// Node.js: save to file
// const fs = require('fs');
// const buffer = Buffer.from(await response.arrayBuffer());
// fs.writeFileSync('ticket.pkpass', buffer); import requests
response = requests.post(
'https://api.walletwallet.dev/api/pkpass',
headers={
'Content-Type': 'application/json',
'Authorization': 'Bearer ww_live_<your_key>'
},
json={
'barcodeValue': 'ORDER-456',
'barcodeFormat': 'Code128',
'logoText': 'Order Pickup',
'primaryFields': [{'label': 'ORDER', 'value': 'Pickup'}],
'secondaryFields': [{'label': 'Order #', 'value': '456'}]
}
)
response.raise_for_status()
with open('order.pkpass', 'wb') as f:
f.write(response.content) require 'net/http'
require 'json'
require 'uri'
uri = URI('https://api.walletwallet.dev/api/pkpass')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request['Authorization'] = 'Bearer ww_live_<your_key>'
request.body = {
barcodeValue: 'MEMBER-001',
barcodeFormat: 'QR',
logoText: 'Gym Membership',
primaryFields: [{ label: 'MEMBER', value: 'Premium' }]
}.to_json
response = http.request(request)
File.binwrite('membership.pkpass', response.body) $ch = curl_init('https://api.walletwallet.dev/api/pkpass');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ww_live_<your_key>'
],
CURLOPT_POSTFIELDS => json_encode([
'barcodeValue' => 'COUPON-50OFF',
'barcodeFormat' => 'QR',
'logoText' => 'Discount Coupon',
'primaryFields' => [['label' => 'COUPON', 'value' => '50% Off']]
])
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
file_put_contents('coupon.pkpass', $response);
} package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"os"
)
func main() {
body, _ := json.Marshal(map[string]interface{}{
"barcodeValue": "PASS-999",
"barcodeFormat": "QR",
"logoText": "Access Pass",
})
req, _ := http.NewRequest("POST", "https://api.walletwallet.dev/api/pkpass", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer ww_live_<your_key>")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
out, _ := os.Create("access.pkpass")
defer out.Close()
io.Copy(out, resp.Body)
} Testing Your Pass
- Save the API response to a
.pkpassfile - macOS: Double-click the file to preview in Finder
- iOS: AirDrop or email the file to your device
- The pass will prompt to add to Apple Wallet
Tip: Passes are signed with Apple certificates, so they work immediately on any iOS device — no additional configuration needed.
Free plan includes 1,000 passes/month