Guides
Templates and placeholders
Design a pass once, then issue it to many holders, each with their own values.
Most passes of a kind look the same and differ only in a few values: a name, a member number, a balance. A template is that design, with placeholders where each pass has its own values. You change the design in one place, and decide when its passes get the change.
Placeholders
A placeholder is a name in double braces, anywhere in the pass's text:
{ "key": "points", "label": "Points", "value": "{{points}}" }Its name is a lowercase letter, then lowercase letters, digits or _. Two are reserved for the holder: {{holder.name}} and {{holder.email}}, filled from the pass's holder.
They can go in names and descriptions, any field's label, value or changeMessage, the barcode and the text under it, a location's relevantText, a featured action's url, externalId, and expiresAt (on its own: a date).
A pass is never issued with a placeholder left in it. Anything in {{…}} that isn't a placeholder is refused, never shown as text, so a typo can't reach a holder.
Create a template
Create a template with its design, and a rule for any placeholder that isn't required text:
curl "https://api.walletpassapi.com/v1/templates" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Idempotency-Key: gold-member-template" \
-H "Content-Type: application/json" \
-d '{
"name": "Gold member",
"slug": "gold-member",
"design": {
"organizationName": "Acme Coffee",
"logoText": "Acme",
"style": "loyalty",
"primaryFields": [
{
"key": "points",
"label": "Points",
"value": "{{points}}"
}
],
"secondaryFields": [
{
"key": "member",
"label": "Member",
"value": "{{holder.name}}"
}
],
"barcodeValue": "{{member_id}}",
"barcodeFormat": "QR"
},
"placeholders": [
{
"key": "points",
"type": "number"
},
{
"key": "member_id",
"label": "Member ID"
}
],
"holderRequired": {
"name": true
}
}'const res = await fetch("https://api.walletpassapi.com/v1/templates", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WALLET_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "gold-member-template",
},
body: JSON.stringify({
"name": "Gold member",
"slug": "gold-member",
"design": {
"organizationName": "Acme Coffee",
"logoText": "Acme",
"style": "loyalty",
"primaryFields": [
{
"key": "points",
"label": "Points",
"value": "{{points}}"
}
],
"secondaryFields": [
{
"key": "member",
"label": "Member",
"value": "{{holder.name}}"
}
],
"barcodeValue": "{{member_id}}",
"barcodeFormat": "QR"
},
"placeholders": [
{
"key": "points",
"type": "number"
},
{
"key": "member_id",
"label": "Member ID"
}
],
"holderRequired": {
"name": true
}
}),
});
const data = await res.json();import os
import requests
res = requests.post(
"https://api.walletpassapi.com/v1/templates",
headers={"Authorization": f"Bearer {os.environ['WALLET_API_KEY']}", "Idempotency-Key": "gold-member-template"},
json={
"name": "Gold member",
"slug": "gold-member",
"design": {
"organizationName": "Acme Coffee",
"logoText": "Acme",
"style": "loyalty",
"primaryFields": [
{
"key": "points",
"label": "Points",
"value": "{{points}}",
},
],
"secondaryFields": [
{
"key": "member",
"label": "Member",
"value": "{{holder.name}}",
},
],
"barcodeValue": "{{member_id}}",
"barcodeFormat": "QR",
},
"placeholders": [
{
"key": "points",
"type": "number",
},
{
"key": "member_id",
"label": "Member ID",
},
],
"holderRequired": {
"name": True,
},
},
)
data = res.json()<?php
$ch = curl_init("https://api.walletpassapi.com/v1/templates");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("WALLET_API_KEY"),
"Content-Type: application/json",
"Idempotency-Key: gold-member-template",
],
CURLOPT_POSTFIELDS => json_encode([
"name" => "Gold member",
"slug" => "gold-member",
"design" => [
"organizationName" => "Acme Coffee",
"logoText" => "Acme",
"style" => "loyalty",
"primaryFields" => [
[
"key" => "points",
"label" => "Points",
"value" => "{{points}}",
],
],
"secondaryFields" => [
[
"key" => "member",
"label" => "Member",
"value" => "{{holder.name}}",
],
],
"barcodeValue" => "{{member_id}}",
"barcodeFormat" => "QR",
],
"placeholders" => [
[
"key" => "points",
"type" => "number",
],
[
"key" => "member_id",
"label" => "Member ID",
],
],
"holderRequired" => [
"name" => true,
],
]),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);Each rule sets a placeholder's type (text, number, or date as YYYY-MM-DD), whether it's required, and a default. The design is checked as a pass with sample values, so a mistake is refused now, not when a pass is made from it.
Templates can also be designed in the dashboard's editor, with a live preview. Either way, the API uses them by their slug.
Make passes from it
Create a pass with the template's slug instead of fields, and the pass's own values:
curl "https://api.walletpassapi.com/v1/passes" \
-H "Authorization: Bearer $WALLET_API_KEY" \
-H "Idempotency-Key: member-1042" \
-H "Content-Type: application/json" \
-d '{
"template": "gold-member",
"data": {
"points": 120,
"member_id": "MEMBER-1042"
},
"holder": {
"name": "Priya Shah",
"email": "priya@example.com"
},
"externalId": "crm-1042"
}'const res = await fetch("https://api.walletpassapi.com/v1/passes", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.WALLET_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "member-1042",
},
body: JSON.stringify({
"template": "gold-member",
"data": {
"points": 120,
"member_id": "MEMBER-1042"
},
"holder": {
"name": "Priya Shah",
"email": "priya@example.com"
},
"externalId": "crm-1042"
}),
});
const data = await res.json();import os
import requests
res = requests.post(
"https://api.walletpassapi.com/v1/passes",
headers={"Authorization": f"Bearer {os.environ['WALLET_API_KEY']}", "Idempotency-Key": "member-1042"},
json={
"template": "gold-member",
"data": {
"points": 120,
"member_id": "MEMBER-1042",
},
"holder": {
"name": "Priya Shah",
"email": "priya@example.com",
},
"externalId": "crm-1042",
},
)
data = res.json()<?php
$ch = curl_init("https://api.walletpassapi.com/v1/passes");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("WALLET_API_KEY"),
"Content-Type: application/json",
"Idempotency-Key: member-1042",
],
CURLOPT_POSTFIELDS => json_encode([
"template" => "gold-member",
"data" => [
"points" => 120,
"member_id" => "MEMBER-1042",
],
"holder" => [
"name" => "Priya Shah",
"email" => "priya@example.com",
],
"externalId" => "crm-1042",
]),
CURLOPT_RETURNTRANSFER => true,
]);
$data = json_decode(curl_exec($ch), true);A required placeholder without a value is refused with placeholder_missing, which lists every one that's missing. To change a pass's values later, send only data (or holder) to Update a pass: they're merged over the pass's own.
The holder's name and email are kept with the pass, so you can find a person's passes by searching for them. They're only on the pass where the design shows them, and never in webhooks.
For many passes at once, from a spreadsheet, use Import on the dashboard's Passes tab.
Change the design
Save the template's changes with the whole template, as it should now be. Its version goes up, and you choose what happens to the passes already made from it:
"updatePasses": "none"(the default): they keep their design for now. They're listed as outdated, and you can update them whenever you're ready."updatePasses": "all": they get the new design now, in the background, and installed passes refresh. One operation a pass.
If the new design adds a required placeholder without a default, the passes made before it have no value for it. Give them one in fill, such as { "tier": "Gold" }; otherwise the save is refused with placeholder_fill_required.
To see what an update would cost first, get the template: preview.outdated is how many passes it would update, against preview.operations.remaining. To find the outdated passes, list passes with template=gold-member&templateOutdated=true.
Renaming a template, or changing its install page heading, doesn't change its design: use Rename a template. Deleting it leaves its passes as they are, no longer linked to it.
Your organization's templates
An organization's owners and admins can make templates for all its workspaces, or chosen ones, in the dashboard. A workspace they're shared with sees them in List templates as org: and their slug, such as org:gold-member, and makes passes from them the same way:
{ "template": "org:gold-member", "data": { "points": 120, "member_id": "MEMBER-1042" } }In the workspace they can only be read: they're changed in the organization, and a change reaches every workspace's passes. Duplicate one in the dashboard to make a workspace's own copy.