Passtastic API — Wallet Loyalty for Any System
Passtastic
Explorar
Produto
Preços
Casos de Uso
Ajuda
Desenvolvedores
Ainda não tem conta? Cadastre-se grátis
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