Open dashboardwalletap.com

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.

Web

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 plans

Before 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 /templates lists 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, or
  • externalId and templateId together — 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#

POSThttps://api.walletap.com/pass
Shell
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.

FieldNotes
passesOne to ten passes. Each needs a templateId
passes[].externalIdYour own reference for the customer
passes[].templateFieldsValues for the fields of the pass type, plus validFrom and validUntil
passes[].customFieldsValues for fields you added to the design, keyed by field ID
passes[].collectedStampsStarting stamp count on a stamp card, 0 to 50
passes[].redemptionValueWhat the pass's barcode carries. Defaults to externalId, then to the pass ID
email, phoneWhere the pass is delivered
sendToEmail, sendToPhoneTurn delivery on or off for this request. See Delivery
localeTwo-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 typeFields
loyaltyaccountName, accountId, balance, rewards, currencyCode
giftCardbalance, currencyCode, cardNumber, pin, eventNumber, balanceUpdateTime
eventTicketticketHolderName, 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 phone and 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#

GEThttps://api.walletap.com/pass
Shell
curl "https://api.walletap.com/pass?externalId=harbour-crm-1042&templateId=tmpl_123" \
  -H "X-API-Key: $WALLETAP_API_KEY"
JSON
{
  "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#

GEThttps://api.walletap.com/passes
Shell
curl "https://api.walletap.com/passes?templateId=tmpl_123&limit=50" \
  -H "X-API-Key: $WALLETAP_API_KEY"
JSON
{
  "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
}
ParameterNotes
templateIdRequired
limitPasses per page. Without it, every pass on the template comes back
startAfterThe created value of the last pass on the previous page
statusall (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#

GEThttps://api.walletap.com/passes/search
Shell
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#

GEThttps://api.walletap.com/passes/export
Shell
curl "https://api.walletap.com/passes/export?templateId=tmpl_123" \
  -H "X-API-Key: $WALLETAP_API_KEY" \
  -o coffee-card.csv

Returns 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#

PATCHhttps://api.walletap.com/pass

Send the pass's identity and only the fields you want to change. Everything else stays as it is.

Shell
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 }
  }'
JSON
{ "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:

Shell
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"
    }
  }'
operationDoes
addAdds count stamps. Stops at the card's size
removeRemoves count stamps, 1 if left out. Stops at zero
removeAllEmpties the card
setSets the count to count, up to the card's size
claimTierMarks 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:

Shell
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 }
  }'
JSON
{
  "id": "pass_321",
  "externalId": "harbour-crm-1043",
  "pointsBalance": 135,
  "templateFields": { "balance": 135 }
}
operationDoes
addAdds amount
subtractSubtracts amount, stopping at zero
setSets 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 an invalidatedReason of used, refunded, exchanged or manual. The wallet moves the pass to expired passes and every scanner refuses it.
  • Reactivate — send "isValid": true.
Shell
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#
POSThttps://api.walletap.com/pass/{passId}/notify
Shell
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#
POSThttps://api.walletap.com/template/{templateId}/notification
Shell
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#

POSThttps://api.walletap.com/pass/{passId}/resend

Sends an existing pass's link again, without issuing a new pass — when someone deleted the text or changed phones.

Shell
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"] }'
FieldNotes
phone, emailWhere to send. Default to the contact details stored on the pass
sendToPhonetrue by default
sendToEmailfalse by default
channelsWhich of SMS, WHATSAPP and VIBER to use for this send, in order. Defaults to the account's channels
localeLanguage 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.

Next steps#