Skip to main content
This flow puts a pass in the customer’s wallet from your own touchpoint: a page, app screen, or email. Your backend orchestrates it, and the API key never leaves your server. The simplest version, and the one to build: store Stell’s pass id on your customer record when the pass is issued, and let your own data answer whether this customer already has a pass. Issue once, with context. Create new pass carries programId, walletType (APPLE_WALLET or GOOGLE_WALLET), your externalId, customer details for personalization, and the customer’s current loyaltyState, such as pointsBalance and currentTierId. The pass is correct the moment it lands in the wallet. An optional payload sets what the pass carries for in-store scans; see In-store. Returns 201 with the pass id — store it. A returning customer skips the create. If you already hold a pass id, go straight to the link endpoint. That covers the customer who taps the button twice, reinstalls the wallet app, or moves to a new phone: same pass, new link. Links work for passes in PREACTIVE, ACTIVE, and INACTIVE, so a customer who deleted the pass can re-add the one they already have. Two link endpoints. Generate Apple Wallet installation link returns a .pkpass download URL valid for roughly 15 minutes. Generate Google Wallet save link returns a save link valid for roughly an hour. Each response carries the exact deadline in expiresAt. Request links at click time, and never cache them. Each endpoint only serves a pass issued for its own wallet type. Device handling is yours. The API returns links; it does not detect the customer’s device or browser. Show the right button for the device (there is no Google Wallet app on iOS, and Apple Wallet passes can’t open on Android), and mind the desktop case: the Apple link serves a .pkpass file that only Safari on macOS opens in Wallet — in any other desktop browser it lands in the downloads folder as a dead end. Stell’s hosted experiences (enrollment flow and wallet button widget) solve this by showing the link as a QR code the customer scans with their iPhone; if you build your own flow, bring your own equivalent or reserve the Apple button for devices that can use it. The Google save link opens in any browser. Activation is confirmed for you. When the customer adds the pass, it flips from PREACTIVE to ACTIVE and a signed PASS_STATUS_CHANGED webhook delivery tells your backend. Write it back to your customer record from there.

One pass per customer

The API does not deduplicate. POST /v1/passes creates a new pass every time it is called. There is no uniqueness constraint on externalId and no idempotency key — two calls for the same customer produce two passes with two different ids. Both are real and both scan, but only one of them receives the updates you send, so the customer’s points can silently stop moving on the pass in their wallet. Keeping one pass per customer is your backend’s job. Store the pass id and guard the create. One id on the customer record, written the moment the pass is issued, is the whole mechanism. Every later call — issuing a link, updating points, voiding — addresses that id. Guard the create against concurrent clicks the same way you would any other insert: a unique constraint or a lock on your side of the customer record, not a retry against Stell. “One pass” means one per program, per wallet type. A customer enrolled in two programs holds two passes. A customer who wants the pass on both an iPhone and an Android device also holds two, because a pass is bound to the walletType it was issued with. Store the id per wallet type if you support both, and keep their loyaltyState in sync by updating each one. Reissue rather than duplicate. When a customer loses access, don’t issue a second pass. Request a fresh link for the existing id. Issue a replacement only after you void the old pass, so exactly one live pass per customer remains.

If you don’t store the pass id

You can run the flow without keeping the id, at the cost of a lookup on every click. This is the fallback, not the default — the lookup is a read of Stell’s data to reconstruct something your own record could have told you. Filter the lookup. List passes on programId + externalId returns matches in every status, voided and expired included. Filter on status and walletType, or check the isVoided and isExpired flags on each result, before deciding a pass is reusable. Treating an unfiltered result as “no pass exists” is how duplicates get created; treating a voided pass as reusable is how link requests start failing. Paginate to the end. A response page can come back empty and still carry a nextToken. Follow the token until it is absent before concluding the customer has no pass.
Don’t want to build this flow? The wallet button widget does the same thing with no API code. Stell hosts the button, the enrollment page, and the pass issuing.