Passtastic API — Wallet Loyalty for Any System
Passtastic
Entdecken
Produkt
Preise
Anwendungsfälle
Hilfe
Entwickler
Noch kein Konto? Kostenlos registrieren
Developer API

Wallet loyalty, wired into your own systems

Enroll customers, award stamps and points, and read balances from your POS, CRM, or app. Passtastic keeps their Apple/Google Wallet pass updated in real time — you call a REST API and listen for webhooks, we handle the wallet plumbing.

Quickstart

Four calls: get a key, enroll a customer, award something, and listen for what happens next.

1. Get an API key

API keys are created by an org admin from Account → API & Webhooks. The raw key is shown once at creation — store it in your own secrets manager, not in source control.

2. Enroll a customer

cardId is the loyalty program (BusinessCard) to enroll them on. externalCustomerId is your own identifier (CRM contact id, user id, etc.) — Passtastic uses it to resolve the customer on every later call.

bash
curl -X POST https://api.passtastic.io/api/v1/customers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalCustomerId": "crm_10293",
    "cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
    "name": "Alex Rivera",
    "email": "alex@example.com"
  }'
json
{
  "passUserId": "66a1e2f3c9d4b5a6f7081920",
  "externalCustomerId": "crm_10293",
  "installUrl": "https://passtastic.io/get-pass/64f1c2a9b8e4a2d1c0a1b2c3?code=8K3F2Q",
  "status": "pending_install"
}

Send them installUrl — opening it on a phone adds the card to Apple or Google Wallet. Nothing to build here; the wallet pass is served by Passtastic.

3. Award stamps or points

bash
curl -X POST https://api.passtastic.io/api/v1/customers/crm_10293/earn \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "points",
    "from": "spend",
    "spend": { "amount": 24.50, "currency": "EUR" },
    "note": "Order #4821"
  }'
json
{ "balance": { "points": 245 }, "transactionId": "66a1e2f3c9d4b5a6f7081920" }

4. Subscribe to a webhook

So your system finds out about redemptions or level changes that happen from the merchant's scanner, not just the calls you make yourself.

bash
curl -X POST https://api.passtastic.io/api/v1/webhooks \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-system.com/hooks/passtastic",
    "events": ["balance.updated", "reward.redeemed"]
  }'

Authentication & keys

Every request is authenticated with a static API key — no OAuth flow, no token refresh.

http
X-Api-Key: YOUR_API_KEY

Keys are created and revoked by an org admin from Account → API & Webhooks. A key is scoped to the org it was created in — there is no cross-org access. Passtastic keys are full-access across all four scopes below; there is currently no scope picker in the UI.

ScopeGrants
customers:readGET a customer's status/balance.
customers:writeEnroll customers.
loyalty:writeEarn, redeem, and adjust balances.
webhooks:manageCreate, list, update, delete webhook endpoints; view and replay deliveries.

A key missing a required scope gets a 403 with a body like { "message": "Missing scope(s): loyalty:write" }.

Idempotency

POST /v1/customers, .../earn, .../redeem, and .../adjust all accept an optional Idempotency-Key header. Retry the same request with the same key (per org + endpoint) and you get back the first response instead of double-applying the write — safe to retry on a network timeout.

http
Idempotency-Key: order-4821-earn

Resolving a customer

The {ref} path segment on earn/redeem/adjust/GET status accepts either your own externalCustomerId directly, or Passtastic's internal id prefixed with pu_ (e.g. pu_66a1e2f3c9d4b5a6f7081920).

Recipes

The four calls most integrations make first.

Earn points from a sale

Pass the raw sale — Passtastic applies the card's configured accrual rate. Use from: "spend" for a currency amount, or from: "items" when your POS tracks earn-eligible items by key (e.g. one stamp per coffee, regardless of price). Omit from and pass a flat amount to add a specific number of points or stamps directly.

bash
# Item-based (e.g. "one stamp per coffee")
curl -X POST https://api.passtastic.io/api/v1/customers/crm_10293/earn \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "stamps",
    "from": "items",
    "items": [{ "key": "coffee", "quantity": 1 }]
  }'

Sync a customer by external ID

Enroll is idempotent per (org, externalCustomerId) — call it from every CRM sync or sign-in without worrying about duplicates. A repeat call for the same externalCustomerId returns the existing enrollment (same passUserId/installUrl) instead of creating a second pass.

bash
# Safe to call again with the same externalCustomerId — returns the
# existing enrollment instead of erroring or duplicating the pass.
curl -X POST https://api.passtastic.io/api/v1/customers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "externalCustomerId": "crm_10293", "cardId": "64f1c2a9b8e4a2d1c0a1b2c3" }'

Read a customer's balance

bash
curl https://api.passtastic.io/api/v1/customers/crm_10293 \
  -H "X-Api-Key: YOUR_API_KEY"
json
{
  "externalCustomerId": "crm_10293",
  "passUserId": "66a1e2f3c9d4b5a6f7081920",
  "cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
  "cardType": "point_card",
  "balance": { "points": 245 },
  "installed": true,
  "installUrl": "https://passtastic.io/get-pass/64f1c2a9b8e4a2d1c0a1b2c3"
}

balance shape depends on the card: { stamps } for stamp cards, { points } for point cards, { points, level } for level cards.

Receive and verify webhooks

Every delivery is a POST with an X-Passtastic-Signature header. Verify it against the raw, unparsed request body before trusting the payload — see the full signature format in Webhooks below.

javascript
// Node / Express — mount with a raw-body parser so rawBody is the exact
// bytes Passtastic sent (JSON.stringify'd server-side, not re-serialized).
const crypto = require('crypto')

function verifyPasstasticSignature(header, rawBody, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')))
  const timestamp = Number(parts.t)
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')

  const a = Buffer.from(parts.v1 || '', 'hex')
  const b = Buffer.from(expected, 'hex')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

app.post('/hooks/passtastic', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.header('X-Passtastic-Signature') || ''
  const rawBody = req.body.toString('utf8')

  if (!verifyPasstasticSignature(signature, rawBody, process.env.PASSTASTIC_WEBHOOK_SECRET)) {
    return res.status(401).send('invalid signature')
  }

  const event = JSON.parse(rawBody)
  // event.type: 'customer.enrolled' | 'balance.updated' | 'reward.redeemed' | 'level.changed'
  console.log(event.type, event.data)
  res.sendStatus(200)
})
python
# Python / Flask
import hashlib
import hmac
import time

def verify_passtastic_signature(header, raw_body, secret, tolerance_sec=300):
    parts = dict(p.split('=', 1) for p in header.split(','))
    timestamp = int(parts.get('t', 0))
    if not timestamp or abs(time.time() - timestamp) > tolerance_sec:
        return False

    expected = hmac.new(
        secret.encode('utf-8'),
        f'{timestamp}.{raw_body}'.encode('utf-8'),
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(parts.get('v1', ''), expected)


@app.route('/hooks/passtastic', methods=['POST'])
def passtastic_webhook():
    signature = request.headers.get('X-Passtastic-Signature', '')
    raw_body = request.get_data(as_text=True)

    if not verify_passtastic_signature(signature, raw_body, PASSTASTIC_WEBHOOK_SECRET):
        return 'invalid signature', 401

    event = request.get_json()
    # event['type']: 'customer.enrolled' | 'balance.updated' | 'reward.redeemed' | 'level.changed'
    print(event['type'], event['data'])
    return '', 200

Webhooks

Subscribe once, and Passtastic pushes an event every time a customer's balance changes — whether it happened through your API call or a scan at the counter.

Events

EventFired whendata fields
customer.enrolledA new customer is created via POST /v1/customers (not on a repeat/idempotent enroll).externalCustomerId, passUserId, cardId
balance.updatedA stamp or points balance changes from earn, redeem, or adjust.externalCustomerId, passUserId, cardId, cardType, balance, change
reward.redeemedA card's fixed reward is redeemed (type: "reward").externalCustomerId, passUserId, cardId, cardType, balance, change
level.changedA level card's tier changes from a balance correction.externalCustomerId, passUserId, cardId, balance, change

Subscribe to specific events, or pass ["*"] for all of them.

Payload envelope

json
{
  "id": "66a3f8b1c2d4e5f6a7b8c9d0",
  "type": "balance.updated",
  "createdAt": "2026-07-14T10:32:05.114Z",
  "orgId": "64e2b1a0c9d8e7f6a5b4c3d2",
  "data": {
    "externalCustomerId": "crm_10293",
    "passUserId": "66a1e2f3c9d4b5a6f7081920",
    "cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
    "cardType": "point_card",
    "balance": 245,
    "change": 25
  }
}

Note: data.balance here is a single number (the card's primary counter — points or stamps), not the { points, stamps, level } object returned by GET /v1/customers/{ref}.

Signature

Every delivery carries an X-Passtastic-Signature header:

http
X-Passtastic-Signature: t=1752483125,v1=5f3c9a1e7b2d4c6f8a0b1c3d5e7f9a1b3c5d7e9f1a3b5c7d9e1f3a5b7c9d1e3f

t is a Unix timestamp (seconds). v1 is hex(HMAC-SHA256(signingSecret, timestamp + "." + rawBody)) — the timestamp, a literal dot, and the exact raw request body, signed with the secret shown once when you created the webhook. Reject anything where the timestamp is more than 5 minutes old (replay protection) — the JS/Python snippets above do this for you.

Delivery & retries

Passtastic expects a 2xx within 5 seconds. A non-2xx or timeout is retried up to 3 attempts total with a short backoff between them. Every attempt — success or failure — is recorded and visible (with the response code) in Account → API & Webhooks → Webhooks, where you can also replay any individual delivery by hand.

Endpoint reference

Base URL https://api.passtastic.io. Every path below is prefixed with /api (e.g. https://api.passtastic.io/api/v1/customers).

Customers & loyalty

MethodEndpointScopeDescription
POST/v1/customerscustomers:writeEnroll a customer on a card. Idempotent per externalCustomerId — calling again returns the existing enrollment.
GET/v1/customers/{ref}customers:readRead a customer's card, balance, and wallet-install state.
POST/v1/customers/{ref}/earnloyalty:writeAward stamps or points — flat amount, spend-based, or item-based.
POST/v1/customers/{ref}/redeemloyalty:writeRedeem the card's fixed reward, or deduct an arbitrary points amount.
POST/v1/customers/{ref}/adjustloyalty:writeApply a signed balance correction, or set an absolute points value.

Webhooks

MethodEndpointScopeDescription
POST/v1/webhookswebhooks:manageRegister an endpoint. The signing secret is returned once, in this response only.
GET/v1/webhookswebhooks:manageList your webhook endpoints (secrets are never included).
GET/v1/webhooks/{id}webhooks:manageRead a single webhook endpoint.
PATCH/v1/webhooks/{id}webhooks:manageUpdate the URL, subscribed events, or status.
DELETE/v1/webhooks/{id}webhooks:manageRemove a webhook endpoint.
GET/v1/webhooks/{id}/deliverieswebhooks:manageRecent delivery attempts for one endpoint (status, response code, timestamp).
POST/v1/webhooks/deliveries/{deliveryId}/replaywebhooks:manageRe-send a specific delivery to its original endpoint.

Common errors

StatusBody / cause
401Missing or invalid X-Api-Key.
403Missing scope(s): <scope> — the key isn't authorized for this call.
403CARD_NOT_IN_ORG — the cardId doesn't belong to your org.
404CARD_NOT_FOUND / CUSTOMER_NOT_FOUND — check the cardId/{ref}.
400UNSUPPORTED_FOR_CARD_TYPE — e.g. redeeming a level card's fixed reward, or setting points on a stamp card.
400Field validation errors (missing/malformed body fields) — standard Nest validation-error shape.

Build with AI

This whole reference is written so an AI coding agent can integrate Passtastic end-to-end from it — paste the docs, get a key, ship the integration.

Copy the full reference

One file: every endpoint, request/response examples, the webhook signature verification code, and the recipes above — self-contained, no crawling required. Paste it into Claude, Cursor, Codex, or any other coding agent as context before you ask it to integrate.

Copies the full API reference (llms-full.txt) to your clipboard.

Starter prompt

Paste this into your agent to kick off the integration:

text
Integrate Passtastic loyalty using this API: https://passtastic.io/llms-full.txt — get a key from Settings → API & Webhooks

Agent-ready files