Work with passes
Issue, read, list, update, stamp, notify and void passes over the API, and what each call changes on the holder's phone.
A pass is one customer's card, issued from a template. This page covers everything you can do to a pass over the API, from issuing it to taking it out of use, and says which calls reach the holder's phone.
Paid plansBefore you start#
- An API key with the scopes for the calls you make. The Manage passes preset covers issuing, reading, updating and notifying; stamp and points operations and exports need extra scopes. See Scopes.
- The ID of the template you issue from.
GET /templateslists them; see Work with templates.
Identify a pass#
Every call that acts on one pass needs to know which one. Send either:
id— Walletap's ID for the pass, returned when it was issued, orexternalIdandtemplateIdtogether — your own reference for the customer, set when you issued the pass, plus the template it came from.
Keep each externalId unique within a template. A lookup by externalId acts on
the first pass it finds with that value.
Issue passes#
curl -X POST https://api.walletap.com/pass \
-H "X-API-Key: $WALLETAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"passes": [
{
"templateId": "tmpl_789",
"externalId": "harbour-crm-1043",
"templateFields": { "accountName": "Luka Horvat", "balance": 120 }
}
],
"email": "luka.horvat@example.com",
"phone": "+385915550142",
"locale": "hr"
}'This request issues Luka's card and sends it to him by email and text message, in Croatian. See Delivery to issue without sending.
| Field | Notes |
|---|---|
passes | One to ten passes. Each needs a templateId |
passes[].externalId | Your own reference for the customer |
passes[].templateFields | Values for the fields of the pass type, plus validFrom and validUntil |
passes[].customFields | Values for fields you added to the design, keyed by field ID |
passes[].collectedStamps | Starting stamp count on a stamp card, 0 to 50 |
passes[].redemptionValue | What the pass's barcode carries. Defaults to externalId, then to the pass ID |
email, phone | Where the pass is delivered |
sendToEmail, sendToPhone | Turn delivery on or off for this request. See Delivery |
locale | Two-letter language code for the email, the text message and the add-to-wallet buttons. en by default |
The response holds the pass id, its externalId, the link to its pass page,
the wallet links in pass and the add-to-wallet button images in badge.
Issue your first pass walks through it.
The pass page at link follows the language of the holder's browser. When you
send link through your own channel, add ?locale= and the language code to
open the page in that language, as in https://pass.walletap.com/pass_456?locale=hr.
Field values#
A value appears on the pass only if its key matches the ID of a field in the
template's design. To see the IDs, fetch the template with
GET /templates/{id} and parse its pass string: every entry in header,
rows and linkRows has an id.
Fields that belong to the pass type go in templateFields:
| Template type | Fields |
|---|---|
loyalty | accountName, accountId, balance, rewards, currencyCode |
giftCard | balance, currencyCode, cardNumber, pin, eventNumber, balanceUpdateTime |
eventTicket | ticketHolderName, ticketType, ticketNumber, gate, section, row, seat, confirmationCode |
Fields you added to the design yourself go in customFields, keyed by their ID.
A field you added yourself is also read from templateFields, so a custom value
sent there still shows. A field of the pass type, such as accountName, is read
only from templateFields. Values are strings or numbers.
A name sent as memberName is moved to accountName or ticketHolderName when
the template uses one of those for the holder's name, and memberId to
accountId in the same way, so a generic integration does not need to know
which one a template uses.
Validity dates#
templateFields.validFrom and templateFields.validUntil take an ISO 8601 date
or date-time. validUntil is the pass's expiry in the wallet itself: once it
passes, Apple Wallet and Google Wallet move the pass to their expired passes.
That suits a ticket or a coupon.
For a membership that renews, do not set validUntil. Put the date in a field of
your own, such as a custom "Valid until" field, so the card shows it without the
wallet ever archiving the pass. See
pass types for the difference.
Delivery#
Issuing a pass delivers it:
- Email — when the request carries an
email, Walletap sends a message from "template name via Walletap" with a link to the pass page. Replies go to the account owner's address. - Text message — when the request carries a
phoneand the account's messaging is set up, Walletap texts the link over the account's channels.
email and phone are read from the top level of the request, or from the
first pass when the top level has none.
When you leave out sendToEmail and sendToPhone, the account's
Send new passes automatically setting decides, under
SettingsIntegrationsDelivery. Both are on unless someone
turned them off. Set either flag to false to skip that channel for one request.
A delivery that fails does not fail the request: the pass is issued either way.
Several passes at once#
One request can issue up to ten passes, from one template or several, for one
person — two tickets to Summer Jazz Night, say. They are delivered together, in
one email and one text, with one link to a page that holds all of them. The
response id then identifies that group rather than one pass, and GET /pass
accepts it.
A pass whose templateId is unknown, or belongs to another account, is left out.
If none are left, the request answers 403.
Read a pass#
curl "https://api.walletap.com/pass?externalId=harbour-crm-1042&templateId=tmpl_123" \
-H "X-API-Key: $WALLETAP_API_KEY"{
"id": "pass_456",
"externalId": "harbour-crm-1042",
"link": "https://pass.walletap.com/pass_456",
"pass": { "apple": "https://…", "google": "https://pay.google.com/gp/v/save/…" },
"badge": { "apple": "https://…/apple/en.png", "google": "https://…/google/en.png" },
"deviceCount": 1
}deviceCount is how many wallets hold the pass right now; 0 means nobody has
added it yet. Add locale to get the button images in another language, and
includeTemplate=true to add the template's name, colour and logo.
This call returns the pass's links and install state. For its field values, list or search the template's passes.
List a template's passes#
curl "https://api.walletap.com/passes?templateId=tmpl_123&limit=50" \
-H "X-API-Key: $WALLETAP_API_KEY"{
"passes": [
{
"id": "pass_456",
"templateId": "tmpl_123",
"externalId": "harbour-crm-1042",
"email": "maya.chen@example.com",
"templateFields": { "accountName": "Maya Chen" },
"collectedStamps": 3,
"isValid": true,
"deviceCount": 1,
"created": 1760180000000,
"updated": 1760180400000
}
],
"totalCount": 31
}| Parameter | Notes |
|---|---|
templateId | Required |
limit | Passes per page. Without it, every pass on the template comes back |
startAfter | The created value of the last pass on the previous page |
status | all (the default), installed or not_installed |
Passes come newest first. To page through all of them, keep status at all
and pass the last created value as startAfter until a page comes back short.
Find a pass by email or phone#
curl "https://api.walletap.com/passes/search?templateId=tmpl_123&searchQuery=maya.chen%40example.com" \
-H "X-API-Key: $WALLETAP_API_KEY"The response is an array of the template's passes whose email or phone number
equals searchQuery exactly. Emails are stored in lower case, so search in lower
case. URL-encode the value: @ becomes %40 and + becomes %2B.
Export a template's passes#
curl "https://api.walletap.com/passes/export?templateId=tmpl_123" \
-H "X-API-Key: $WALLETAP_API_KEY" \
-o coffee-card.csvReturns a CSV file with one row per pass: name, email, phone, the redemption
value and every field on the pass, in the same columns the dashboard's import
reads. status filters it the same way as
GET /passes. This call needs the members.export scope.
Update a pass#
Send the pass's identity and only the fields you want to change. Everything else stays as it is.
curl -X PATCH https://api.walletap.com/pass \
-H "X-API-Key: $WALLETAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "harbour-gift-0007",
"templateId": "tmpl_456",
"templateFields": { "balance": 25 }
}'{ "id": "pass_789", "externalId": "harbour-gift-0007" }The change is pushed to every wallet holding the pass within moments. Whether the phone also shows a notification depends on the field: Apple Wallet and Google Wallet alert the holder only for fields set up to announce their changes. To send a message of your own, notify the holder.
Changing email or phone on a pass stores the new value; it does not send the
pass again. Use resend for that.
Add and remove stamps#
On a template with stamps, stampOperation changes the count:
curl -X PATCH https://api.walletap.com/pass \
-H "X-API-Key: $WALLETAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "pass_456",
"stampOperation": {
"operation": "add",
"count": 1,
"idempotencyKey": "till-2-receipt-88412"
}
}'operation | Does |
|---|---|
add | Adds count stamps. Stops at the card's size |
remove | Removes count stamps, 1 if left out. Stops at zero |
removeAll | Empties the card |
set | Sets the count to count, up to the card's size |
claimTier | Marks the reward tier at threshold stamps as claimed |
count is required for add and set, and threshold for claimTier.
idempotencyKey— send a unique value per real-world event, such as a receipt number. If a timed-out request is retried with the same key, the stamp is counted once. Walletap remembers the last 20 keys on each card.intent: "redeem"— with an operation that empties the card, records that the reward was handed over, so it counts as a redemption in loyalty analytics rather than a correction.
The response carries the new stampCollection, with totalCollected,
totalStamps, isComplete and claimedTiers. A refused tier claim answers
400 with Threshold not reached, Tier already claimed or
Unknown tier threshold.
stampOperation needs the scanning.stamp scope. A key with only passes.edit
can state the count instead, with "collectedStamps": 7 at the top level. That
sets it exactly, from 0 to 50, even past the card's size, and is the way to sync
a count your own system keeps.
Change points and balances#
balanceOperation changes the balance field of a loyalty card or gift card:
curl -X PATCH https://api.walletap.com/pass \
-H "X-API-Key: $WALLETAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "harbour-crm-1043",
"templateId": "tmpl_789",
"balanceOperation": { "operation": "add", "amount": 15 }
}'{
"id": "pass_321",
"externalId": "harbour-crm-1043",
"pointsBalance": 135,
"templateFields": { "balance": 135 }
}operation | Does |
|---|---|
add | Adds amount |
subtract | Subtracts amount, stopping at zero |
set | Sets the balance to amount |
amount is a number of zero or more, or a string holding one. With "isTotal": true,
add and subtract set the balance to amount instead.
balanceOperation needs the scanning.redeem scope. A key with only
passes.edit can set templateFields.balance directly instead. Send operations
for one pass one at a time: two that arrive at the same moment can both start
from the same balance.
Expire, void and reactivate#
- Change the expiry — send a new
templateFields.validUntil. Moving it into the future also reactivates a pass that had been voided or had expired. - Void a pass — send
"isValid": false, optionally with aninvalidatedReasonofused,refunded,exchangedormanual. The wallet moves the pass to expired passes and every scanner refuses it. - Reactivate — send
"isValid": true.
curl -X PATCH https://api.walletap.com/pass \
-H "X-API-Key: $WALLETAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "id": "pass_654", "isValid": false, "invalidatedReason": "refunded" }'Voiding is how a pass is taken out of use. The pass and its history stay in the account, so you can reactivate it later.
Notify holders#
One holder#
curl -X POST https://api.walletap.com/pass/pass_456/notify \
-H "X-API-Key: $WALLETAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "One stamp to go",
"content": "Your next coffee at Harbour Coffee is on us."
}'The message appears on the pass and as a notification on the holder's phone.
validFrom and validUntil (ISO 8601) set how long it stays on the pass: from
now, for 24 hours, if you leave them out.
The response says "success": true when the message went out. For a pass in
Apple Wallet, "success": false comes with a code:
no_registered_devices— no iPhone holds the pass right now. The message is on the pass and shows once it is added again.unchanged_message— the same message is already on the pass. Apple Wallet alerts only on a change, so send different wording to notify again.
Google Wallet raises at most three notifications per pass in 24 hours.
Everyone on a template#
curl -X POST https://api.walletap.com/template/tmpl_123/notification \
-H "X-API-Key: $WALLETAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "New on the menu",
"content": "Pumpkin spice is back at both Harbour Coffee locations."
}'Sends one message to every holder of the template's passes. Google Wallet holders get it straight away; Apple Wallet holders within a few minutes. To schedule messages or target part of your members, use campaigns in the dashboard.
Resend a pass#
Sends an existing pass's link again, without issuing a new pass — when someone deleted the text or changed phones.
curl -X POST https://api.walletap.com/pass/pass_456/resend \
-H "X-API-Key: $WALLETAP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "phone": "+385915550142", "channels": ["SMS"] }'| Field | Notes |
|---|---|
phone, email | Where to send. Default to the contact details stored on the pass |
sendToPhone | true by default |
sendToEmail | false by default |
channels | Which of SMS, WHATSAPP and VIBER to use for this send, in order. Defaults to the account's channels |
locale | Language of the message |
The response is { "success": true, "id": "pass_456", "link": "…" }. As with
issuing, text messages go out over the account's
messaging channels.
If something goes wrong#
403 "You don't have permission to access this resource or resource does not
exist" on POST /pass — none of the templateId values belong to the account
the key is for. Check the ID with GET /templates.
404 "Resource not found" on PATCH /pass, or "Pass not found" on
GET /pass — no pass matches. With externalId, check that templateId is the
template the pass was issued from.
401 "You are not authorized for this action" on GET /pass — the pass
belongs to another account.
400 "Pass field values must be a string or number…" — a field value is a
boolean, an object or null. Send "true" as a string, or split the object into
separate fields.
400 "Invalid valid until string! Must be ISO 8601…" — send an ISO 8601
date-time, such as 2026-12-31T23:59:59Z. A bare date like 2026-12-31 means
midnight UTC at the start of that day.
400 "Identify the pass to update…" — send id, or both externalId and
templateId.
403 "Missing permission: scanning.stamp" — the key cannot run stamp
operations. Use a key with that scope, or state the count with
collectedStamps.
404 "Google Wallet object not found…" from notify — the pass's Google
Wallet copy was not created when it was issued. Update the pass with
PATCH /pass, which rebuilds it, then send the message again.
400 "No phone number available to resend the pass to." — the pass has no
phone number stored. Send phone in the request, or resend by email with
"sendToPhone": false, "sendToEmail": true.
402 with "code": "upgrade_required" — the account cannot issue more
passes on its current plan. The reason field says why; see
Errors.
The pass issued, but a field is blank — the key in templateFields or
customFields does not match a field ID in the template, or a field of the pass
type was sent in customFields. See Field values.