Online store
Enroll a shopper on checkout, award points when their order is paid, and keep their wallet pass in sync with refunds — four calls into your existing order flow.
Enroll at checkout or sign-up
Call this from your checkout's order-confirmation step, or your account sign-up flow. It is safe to call on every order — a returning shopper is matched by email rather than duplicated (see result/matchedBy below).
Send firstName/lastName, or a single name — we split it at the first space.
curl -X POST https://api.passtastic.io/api/v1/customers \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalCustomerId": "shop_customer_9001",
"cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
"firstName": "Jamie",
"lastName": "Cole",
"email": "jamie@example.com"
}'{
"passUserId": "66a1e2f3c9d4b5a6f7081920",
"externalCustomerId": "shop_customer_9001",
"cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
"result": "created",
"matchedBy": null,
"linked": true,
"installUrl": "https://passtastic.io/get-pass/64f1c2a9b8e4a2d1c0a1b2c3?code=8K3F2Q",
"status": "pending_install"
}Put installUrl in the order-confirmation email — opening it on a phone adds the card to Apple or Google Wallet. Nothing to build on your side; the pass itself is served by Passtastic.
Let Passtastic email the card
Don't want to build that email yourself? Set sendCardEmail: true on the same enroll call and Passtastic sends "Your {business} card — add it to your wallet in one tap", with Apple Wallet and Google Wallet buttons, right after the customer is created.
It only fires on result: "created" — never on "matched". That is what makes it safe to leave wired into a flow that calls enroll on every order: a returning shopper is matched, not re-emailed, even though your integration enrolls them again on every purchase.
curl -X POST https://api.passtastic.io/api/v1/customers \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalCustomerId": "shop_customer_9001",
"cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
"firstName": "Jamie",
"lastName": "Cole",
"email": "jamie@example.com",
"sendCardEmail": true
}'{
"passUserId": "66a1e2f3c9d4b5a6f7081920",
"externalCustomerId": "shop_customer_9001",
"cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
"result": "created",
"matchedBy": null,
"linked": true,
"installUrl": "https://passtastic.io/get-pass/64f1c2a9b8e4a2d1c0a1b2c3?code=8K3F2Q",
"status": "pending_install",
"cardEmail": "sent"
}Send notifyEmail instead when the address to email differs from email — it is used only to send this one message, never stored on the customer and never used for matching, and it still carries the same unsubscribe link every card email does. Store-credit cards are supported; a Stripe-sold paid card is not, and comes back as cardEmailReason: "card_not_supported" instead of failing the enrollment. See CRM and customer data for the full cardEmail/cardEmailReason list, including "opted_out" and "recently_sent".
No-code tools (Zapier, Make, etc.): skip the separate "send an email" step after your enroll action — set sendCardEmail to true right on the enroll step itself. It also accepts the string "true"/"false" in any casing, since that's what these tools send instead of a real boolean — and it works the same with a test key, which sends a real email rather than a sandboxed one.
Earn when an order is paid
Trigger this from your payment-succeeded webhook, not from checkout submission — an order can still fail payment or be abandoned between the two. Use from: "spend" for the order total, and set Idempotency-Key to the order id so a retried payment webhook never double-awards the same order.
curl -X POST https://api.passtastic.io/api/v1/customers/email:jamie@example.com/earn \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-58210" \
-d '{
"type": "points",
"from": "spend",
"spend": { "amount": 64.00, "currency": "EUR" },
"note": "Order #58210"
}'Refunds
A refund is a negative balanceCorrection on adjust — use the order id for Idempotency-Key (a distinct key from the earn call above, since they're different writes) so a retried refund webhook can never withdraw twice. balanceCorrection must be non-zero — a partial refund is the actual points/stamps that order earned, negated, not the full order value.
curl -X POST https://api.passtastic.io/api/v1/customers/email:jamie@example.com/adjust \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-58210-refund" \
-d '{
"balanceCorrection": -64,
"note": "Refund for order #58210"
}'Show the balance in the account page
Read the same customer back to show their stamps, points, or level next to their order history.
curl https://api.passtastic.io/api/v1/customers/email:jamie@example.com \
-H "X-Api-Key: YOUR_API_KEY"{
"externalCustomerId": "shop_customer_9001",
"passUserId": "66a1e2f3c9d4b5a6f7081920",
"cardId": "64f1c2a9b8e4a2d1c0a1b2c3",
"name": "Jamie Cole",
"firstName": "Jamie",
"lastName": "Cole",
"cardType": "point_card",
"balance": { "points": 245 },
"installed": true,
"installUrl": "https://passtastic.io/get-pass/64f1c2a9b8e4a2d1c0a1b2c3"
}