# Account settings and two-factor authentication Source: https://docs.getstell.com/account/security Manage your Stell portal account: profile, language and theme preferences, two-factor authentication with an authenticator app or email, and your password. Open your account menu at the bottom of the sidebar and click **Settings** to manage your account. Settings has two tabs: **Account** and **Security**. ## Account Your personal preferences: profile photo, name, phone number, timezone, language, theme (system, light, or dark), and how many rows tables show per page. ## Security ### Two-factor authentication Two-factor authentication is required to use the portal. You set it up during [your first login](/getting-started/first-login). From the **Security** tab you can: * See whether your authenticator app is configured. * **Enable 2FA**: scan a QR code with your authenticator app and confirm with a code. * **Disable** your current authenticator setup, for example to switch to a new phone. You'll be asked to set up two-factor authentication again on your next sign-in. ### Change your password 1. Enter your **Current Password**. 2. Enter a **New Password** that meets the requirements shown: at least 8 characters, with an uppercase letter, a lowercase letter, a number, and a special character. 3. Confirm it and click **Update Password**. If you've lost access to your authenticator app and can't sign in, [contact support](/support/contact). They can verify your identity and reset your two-factor setup. # Create and rotate Stell API keys Source: https://docs.getstell.com/api-reference/api-keys Create, rotate and revoke API keys in the Stell merchant portal, and keep one key per integration. **API keys** let your own systems (your CRM, POS, or e-commerce platform) talk to [Stell's API](/api-reference/introduction) on your company's behalf. API keys ## Create a key 1. Open **API Keys** in the sidebar and click **Create API Key**. 2. Give it a **Name** that says what uses it, for example "Production integration". 3. Choose **Expires In**: 30 days, 90 days, 1 year, or Never. 4. Click **Create**. The full key is shown **only once**, right after creation. Copy it and store it somewhere safe, like a password manager or your secrets vault. Afterwards the portal only shows the last few characters. ## Manage keys The list shows each key's status, expiry, creation date, and **Last Used**, which is handy for spotting keys that are no longer in use. From a key's menu you can: * **Rotate**: replace the key's secret immediately. The old secret stops working at once, and the new one is shown a single time. Update your applications right away. * **Delete**: revoke the key permanently. Applications using it lose access immediately, and this can't be undone. Expired and deleted keys move to the **Inactive API Keys** section, where they no longer authenticate requests. ## Good practice * One key per integration: when something needs to be revoked, you only break that one system. * Prefer an expiry over **Never** for keys you can rotate easily. * Never paste an API key into email, chat, or a support ticket. # Authenticate with the Stell API Source: https://docs.getstell.com/api-reference/authentication Send your Stell API key as a Bearer token: the key format, what a missing or invalid key returns, and why keys stay server-side. The Stell API authenticates with **API keys**. You create and manage keys in the merchant portal. See [API keys](/api-reference/api-keys) for creating, rotating, and revoking them. Keys start with `sk_prod_` followed by a random string. The full secret is shown once, when you create the key. ## Sending your key Pass the key as a Bearer token in the `Authorization` header on every request: ```bash theme={null} curl https://api.platform.getstell.com/v1/programs \ --header "Authorization: Bearer sk_prod_your_key_here" ``` A missing or invalid key returns `401 Unauthorized`. A valid key requesting a resource that belongs to another company returns `403 Forbidden`. ## Keep keys server-side An API key grants **full read and write access** to your company's data, including issuing and voiding passes on customers' devices. Treat it like a password. Call the API from your servers only, and never embed a key in a website, mobile app, or anything else you ship to users. Three habits keep keys manageable: * Use one key per integration. Revoking one then never breaks the others. * Store keys in your secrets manager, not in code or config files under version control. * If a key may have leaked, [rotate it](/api-reference/api-keys#manage-keys) immediately. The old secret stops working at once. # Get company details Source: https://docs.getstell.com/api-reference/companies/get-company-details /api-reference/openapi.json get /v1/companies/{companyId} Returns details for a specific company. # List companies Source: https://docs.getstell.com/api-reference/companies/list-companies /api-reference/openapi.json get /v1/companies Returns companies accessible to your API key. Supports cursor-based pagination. # Add pass geofence Source: https://docs.getstell.com/api-reference/engagement/add-pass-geofence /api-reference/openapi.json post /v1/passes/{passId}/geofences Add a single geofence to a pass. Maximum 10 geofences per pass. # Delete all pass geofences Source: https://docs.getstell.com/api-reference/engagement/delete-all-pass-geofences /api-reference/openapi.json delete /v1/passes/{passId}/geofences Remove all pass-level geofence overrides and revert to using program-level geofences # Delete pass geofence by ID Source: https://docs.getstell.com/api-reference/engagement/delete-pass-geofence-by-id /api-reference/openapi.json delete /v1/passes/{passId}/geofences/{geofenceId} Remove a specific geofence from a pass by its ID. # Get pass geofences Source: https://docs.getstell.com/api-reference/engagement/get-pass-geofences /api-reference/openapi.json get /v1/passes/{passId}/geofences Retrieve effective geofences for a specific pass including program-level defaults and pass-level overrides # Replace all pass geofences Source: https://docs.getstell.com/api-reference/engagement/replace-all-pass-geofences /api-reference/openapi.json put /v1/passes/{passId}/geofences Set pass-level geofence overrides that replace all program-level geofences # Update pass geofence by ID Source: https://docs.getstell.com/api-reference/engagement/update-pass-geofence-by-id /api-reference/openapi.json patch /v1/passes/{passId}/geofences/{geofenceId} Update a specific geofence on a pass. Supports partial updates. # Stell API errors and status codes Source: https://docs.getstell.com/api-reference/errors How the Stell API reports errors: the JSON error shape, status codes and error codes, and which errors are safe to retry. The API uses conventional HTTP status codes: `2xx` for success, `4xx` for something wrong with the request, and `5xx` for a problem on Stell's side. ## Error response format Every error returns a JSON body with the same shape: ```json theme={null} { "error": { "code": "NOT_FOUND", "message": "Resource not found", "details": {} }, "requestId": "01JXAMPLE0000000000000000" } ``` * `error.code`: a machine-readable code your integration can branch on. * `error.message`: a human-readable explanation of what went wrong. * `error.details`: additional context when available, such as which field failed validation. * `requestId`: a unique identifier for the request. Include it when you [contact support](/support/contact) about a failed call. It lets us find the exact request in our logs. ## Status codes | Status | Code | Meaning | | - | - | - | | `200 OK` | n/a | The request succeeded. | | `201 Created` | n/a | The resource was created. Returned by create endpoints such as issuing a pass. | | `204 No Content` | n/a | The request succeeded and there is nothing to return, typical for deletes. | | `400 Bad Request` | `BAD_REQUEST` | The request is malformed: missing required fields, wrong types, invalid JSON. | | `400 Bad Request` | `VALIDATION_ERROR` | A field failed validation against its own rules, such as an unrecognized field, a phone number not in E.164 format, an enum value outside the accepted set. Check `error.details` for the offending field. | | `401 Unauthorized` | `UNAUTHORIZED` | The API key is missing, invalid, expired, or revoked. See [Authentication](/api-reference/authentication). | | `403 Forbidden` | `FORBIDDEN` | The key is valid but the resource belongs to another company. | | `404 Not Found` | `NOT_FOUND` | The resource doesn't exist, often because of a mistyped or stale ID. | | `409 Conflict` | `CONFLICT` | The request conflicts with the resource's current state, for example updating a voided or expired pass, or deleting a merchant that still has stores. `error.message` names the specific rule. | | `429 Too Many Requests` | n/a | You're sending requests faster than the API allows. Back off and retry with a delay. | | `500 Internal Server Error` | `INTERNAL_ERROR` | Something went wrong on Stell's side. Retry with backoff; if it persists, contact support with the `requestId`. | | `503 Service Unavailable` | n/a | The API is temporarily unavailable. Retry with backoff. | ## Handle errors well * Branch on `error.code`, not on `error.message`. Messages can change, codes won't. * Retry `5xx` and `429` responses with exponential backoff. Don't retry `4xx` responses unchanged; fix the request first. * Log the `requestId` for every failed call so support conversations start with the right context. # Add to Apple Wallet and Google Wallet from your own site or app Source: https://docs.getstell.com/api-reference/integration/add-to-wallet Issue a pass from your backend and send the customer to one permanent link that opens the right wallet, from a page, app screen, email or QR code. This flow puts a pass in the customer's wallet from your own touchpoint: a page, app screen, email, or printed QR code. Your backend orchestrates it, and the API key never leaves your server. The simplest version, and the one to build: issue the pass once, store Stell's pass `id` and its permanent `link` on your customer record, and send the customer to that link. Your own data answers whether this customer already has a pass. ```mermaid theme={null} sequenceDiagram autonumber participant C as Customer participant B as Your backend participant S as Stell API C->>B: Taps Add to Wallet Note over B: No pass id on the customer record B->>S: POST /v1/passes S-->>B: 201 with pass id and link (PREACTIVE) Note over B: Store the pass id and link on the customer record B-->>C: Redirect to link, pass lands in the wallet S--)B: Webhook PASS_STATUS_CHANGED (PREACTIVE → ACTIVE) ``` **Issue once, with context.** [Create new pass](/api-reference/passes/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](/api-reference/integration/in-store). Returns `201` with the pass object. Store its `id` and its `link`. **One permanent link.** Every pass response carries `link`: a public URL that never expires and stays the same for the life of the pass. It is safe to store, to email, and to print as a QR code. There is no separate call to make: the link is in the create response, and in every later pass response from [Get pass details](/api-reference/passes/get-pass-details), [List passes](/api-reference/passes/list-passes), and [Update pass](/api-reference/passes/update-pass). **A returning customer skips the create.** If you already hold a `link`, send the customer straight to it. That covers the customer who taps the button twice, reinstalls the wallet app, or moves to a new phone: same pass, same link. The link works for passes in `PREACTIVE`, `ACTIVE`, and `INACTIVE`, so a customer who deleted the pass can re-add the one they already have. A voided or expired pass still carries a `link`, but opening it shows **This pass is no longer available**. **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](/api-reference/webhooks/deliveries) tells your backend. Write it back to your customer record from there. ## Two ways to open the link **As-is: the hosted pass page.** Opened without parameters, the link shows a page headed **Your pass is ready** with an **Add to Apple Wallet** or **Add to Google Wallet** button, matching the wallet type the pass was issued for. On a phone that can't hold the pass (an Android phone opening an Apple Wallet pass, or the reverse), the page says so and names the right device. On a computer it adds an **On a phone?** section with the link ready to copy, so the customer can open it on their phone; there is no QR code. Use the page whenever you don't control the device the link will be opened on: email, SMS, print. **With `?delivery=direct`: straight to the wallet.** Append `?delivery=direct` and the link answers with a `302` to the wallet artifact itself: the `.pkpass` file for an Apple Wallet pass, the save link for a Google Wallet pass. No page, no device detection. Use it behind your own button when you already know the device, such as an **Add to Apple Wallet** button in your iOS app. The redirect only happens for a pass that is ready to be added: issued for Apple Wallet or Google Wallet, in `PREACTIVE`, `ACTIVE`, or `INACTIVE`. In every other case (a voided or expired pass, a wallet type the link can't serve, an invalid link, a temporary failure), the response is the hosted pass page with a `200`, not an error status. Don't build a client that assumes a `302`. **Device handling in direct mode is yours.** 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 artifact is 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](/enrollment/adding-to-wallet) and [wallet button widget](/enrollment/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, reserve the Apple button for devices that can use it, or drop the `delivery` parameter and let the hosted pass page handle the desktop visitor. The Google save link opens in any browser. The older [Generate Apple Wallet installation link](/api-reference/passes/generate-apple-wallet-installation-link) and [Generate Google Wallet save link](/api-reference/passes/generate-google-wallet-save-link) endpoints return the same short-lived URLs. They are deprecated in favor of `link` but keep working, with no removal date set. Don't use them in a new integration. ## 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, so two calls for the same customer produce two passes with two different `id`s. 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 step (sending the 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` and `link` 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. Send them the existing pass's `link` again, since it hasn't changed. Issue a replacement only after you [void](/api-reference/passes/void-pass) the old pass, so exactly one live pass per customer remains. Issuing passes for an existing member base? [Batch create passes](/api-reference/passes/batch-create-passes) takes up to 1000 passes in one call and returns a `link` for each pass it creates, ready to drop into a mailing. ## If you don't store the pass id You can run the flow without keeping the `id` and `link`, 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. ```mermaid theme={null} sequenceDiagram autonumber participant C as Customer participant B as Your backend participant S as Stell API C->>B: Taps Add to Wallet B->>S: GET /v1/passes?programId=…&externalId=…&status=ACTIVE S-->>B: Existing pass with its link, or none opt No existing pass B->>S: POST /v1/passes S-->>B: 201 with pass id and link (PREACTIVE) end B-->>C: Redirect to link, pass lands in the wallet ``` **Filter the lookup.** [List passes](/api-reference/passes/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 customers end up on **This pass is no longer available**. Each result carries its `link`, so once you have the reusable pass you redirect without a further call. **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](/enrollment/wallet-button-widget) does the same thing with no API code. Stell hosts the button, the enrollment page, and the pass issuing. # Wallet pass webhooks, lifecycle and go-live checklist Source: https://docs.getstell.com/api-reference/integration/go-live Keep your systems in step with webhooks, handle all six pass statuses, and check off a go-live list before launching your wallet pass integration. ## Webhooks: closing the loop Signed events keep your systems in step with what customers do in their wallets. Three event types exist today: `PASS_CREATED`, `PASS_STATUS_CHANGED`, and `TRANSACTION_CREATED`. Each is routed to your handler via the `X-Stell-Event` header. Create subscriptions [in the portal](/api-reference/webhooks/overview). The full delivery contract, covering headers, payloads, and signature verification with code samples, is documented in [webhook deliveries](/api-reference/webhooks/deliveries). ```mermaid theme={null} sequenceDiagram autonumber participant S as Stell participant E as Your endpoint S->>E: POST with X-Stell-Event, X-Stell-Delivery-Id, X-Stell-Signature E->>E: Verify signature, dedupe on delivery ID E-->>S: 2xx immediately, process async alt Non-2xx or timeout S->>E: Retry, up to 3 attempts per cycle Note over S: Failed events are re-driven in repeated cycles for up to ~24 hours end ``` The integration-level takeaways: * **Verify, then acknowledge fast.** Check the signature, return a `2xx` immediately, and process the event asynchronously. * **Delivery is at-least-once.** Deduplicate on `X-Stell-Delivery-Id` so a retried delivery is never processed twice. * **Failures are retried, persistently.** Each delivery cycle makes up to 3 attempts with short backoff, and failed events are re-driven in repeated cycles for up to roughly 24 hours. An endpoint recovering from an outage will see many deliveries of the same event. The dedupe rule above absorbs exactly that. * **No lookup needed.** `PASS_CREATED` and `PASS_STATUS_CHANGED` payloads carry your `externalId`, so the write-back to your customer record takes no extra call. * **Subscribe to what you act on.** If your backend already knows it issued a pass, because it made the call, `PASS_CREATED` tells you nothing new. It earns its place when passes are also issued outside your integration, through the portal or an enrollment page. ## The pass lifecycle Six statuses, delivered to you through the same webhook. Handle all six in your write-back logic: | Status | Meaning | | - | - | | `PREACTIVE` | Issued, but not yet added to a wallet | | `ACTIVE` | Live in the customer's wallet | | `INACTIVE` | Removed from the wallet by the customer | | `VOIDED` | Voided by you | | `REVOKED` | Revoked, access withdrawn | | `EXPIRED` | Past its validity date | Two transitions dominate: `PREACTIVE → ACTIVE` when a customer adds the pass, and `ACTIVE → INACTIVE` when one removes it. Reflect both in your CRM so campaigns and support see wallet reality. For the merchant-facing view of the same lifecycle, see [pass lifecycle](/passes/lifecycle). ## Go-live checklist * [ ] The API key is stored and used server-side only. It never reaches a browser, app, or client-side code. * [ ] Add to Wallet issues at most one pass per customer: the pass `id` is stored on your customer record and guards the create, or every create is preceded by a status-filtered lookup on `programId` + `externalId`. * [ ] The permanent pass `link` is what you store, email, or print. Short-lived `walletLink` URLs and the deprecated link endpoints are requested at the moment of use and never cached. * [ ] Clients that open `link?delivery=direct` handle a `200` HTML response (the hosted pass page) as well as the `302`. * [ ] Your webhook endpoint verifies `X-Stell-Signature`, responds `2xx` quickly, and processes events asynchronously. * [ ] Deliveries are deduplicated on `X-Stell-Delivery-Id`. * [ ] All six pass statuses are handled in your write-back logic. * [ ] The `externalId` mapping is tested end to end: create → add → webhook → write-back lands on the right customer record. * [ ] The in-store payload, whether a template placeholder or the API `payload` field, points at the identifier your POS resolves, verified on a test pass. * [ ] **NFC only:** your terminal fleet supports Apple VAS / Google Smart Tap with keys provisioned (confirmed with your terminal vendor), and an end-to-end tap is verified at a pilot store: tap → customer identifier at the POS → points accrual → pass update on the device. Questions along the way? [Contact support](/support/contact). # Identify wallet pass holders at the POS: scan or NFC tap Source: https://docs.getstell.com/api-reference/integration/in-store Read a customer identifier from a barcode scan or an NFC tap (Apple VAS, Google Smart Tap) at checkout, with no Stell call in the checkout path. In store, the pass identifies the customer at your POS. Identification is local. The scan or tap hands your POS an identifier it resolves against your own systems, with no Stell call in the checkout path, so checkout keeps working even if everything else is down. Your systems stay the ledger: points and balances are computed where they always were. The barcode carries the payload configured on your pass template, using placeholders such as `{{customer.externalId}}`, `{{customer.emailAddress}}`, or `{{customer.mobileNumber}}`, or set per pass with the API's `payload` field. Left unset, it falls back to the pass `id`. Point it at the identifier your POS can resolve, usually `externalId`. See [barcodes on pass templates](/pass-templates/barcodes) for the template side. ```mermaid theme={null} sequenceDiagram autonumber participant C as Customer participant P as Your POS participant B as Your backend participant S as Stell API C->>P: Presents pass, barcode scanned Note over P: Payload resolved locally, no Stell call P->>B: Sale settles, member identified B->>S: PATCH /v1/passes/{passId} with new loyaltyState B->>S: POST /v1/transactions (optional) ``` The customer taps once at the terminal, and payment and identification happen in the same gesture. Apple VAS and Google Smart Tap ride the contactless tap. No app to open, no barcode to aim at. The reader authenticates with merchant-specific keys and decrypts a per-tap payload, so it can't be screenshotted or replayed the way a barcode can. For NFC there is no template setting. Send the `payload` field when you issue the pass. Left unset, it falls back to the pass `id`. Point it at the identifier your POS can resolve, usually `externalId`, and keep it to **64 characters or fewer**. The same field also drives the barcode kept as a fallback. Two prerequisites, both arranged with Stell support: * **NFC pass enablement.** Apple VAS and Google Smart Tap activated for your program. Stell is a certified Apple VAS provider and handles the approvals and certificates. * **Terminal provisioning.** Your terminals must support VAS / Smart Tap and carry the right keys. The terminals and the vendor relationship are yours; confirm support and key setup with your terminal vendor. ```mermaid theme={null} sequenceDiagram autonumber participant C as Customer participant T as Terminal participant P as Your POS participant B as Your backend participant S as Stell API C->>T: Taps pass, Apple VAS / Google Smart Tap T->>P: Decrypted payload, your customer identifier P->>B: Sale settles, member identified B->>S: PATCH /v1/passes/{passId} with new loyaltyState B->>S: POST /v1/transactions (optional) ``` ## After the sale **Push the new balance to the pass.** [Update pass](/api-reference/passes/update-pass) with the new `loyaltyState`. Everything downstream of the scan or tap is identical for barcode and NFC. **Optionally record the visit.** [Create transaction](/api-reference/transactions/create-transaction) needs only `passId` and `type`. Everything else is optional, so send what your POS knows: * `type`: what happened to the pass, for example `PURCHASE`, `REDEMPTION`, or `SCAN_ONLY`. * `channel` (`IN_STORE`) and `tapMode` (`SINGLE_TAP` when pass and payment rode one tap, `DOUBLE_TAP` for separate taps, `NONE` without a terminal). * `instrument`: how the pass was presented. `QR` for a barcode scan, `APPLE_VAS` or `GOOGLE_SMART_TAP` for a tap. * `status` (`APPROVED`, `DENIED`, `FAILED`, or `PENDING`) with a `statusReason` when it explains a denial or failure. * `pointsEarned`, `pointsSpent`, and your `reference` (receipt number). * `storeId` and `terminalId`, if you track them. * `payment`: the payment side, with `method` (`APPLE_PAY`, `CARD`, `VIPPS`, and so on), `amount`, and PSP references. * `additionalData`: POS metadata such as register ID, line items, merchant category code, and raw POS or PSP data. See the [request schema](/api-reference/transactions/create-transaction) for the full list of accepted keys. Recorded transactions appear in the portal's [Transaction Log](/transactions/overview). Staff can then investigate usage without leaving Stell. **Choosing hardware?** The [reader directory](https://getstell.com/readers) on getstell.com lists payment terminals and NFC readers that read Apple Wallet and Google Wallet passes. For the business case behind tap at the till, read [NFC loyalty identification](https://getstell.com/articles/nfc-loyalty-identification-how-tap-at-the-till-works). # Wallet pass API integration guide Source: https://docs.getstell.com/api-reference/integration/overview Connect your CRM, POS or loyalty platform to Apple Wallet and Google Wallet through the Stell API: who builds what, core concepts and the endpoints you need. This guide is for backend developers building a direct integration against the Stell API. Your systems call the API and own the sync between your customer records and Stell. Four flows make up every integration: [Add to Wallet](/api-reference/integration/add-to-wallet), [updating passes](/api-reference/integration/updating-passes), [in-store usage](/api-reference/integration/in-store), and [webhooks and go-live](/api-reference/integration/go-live). Where this guide says "your CRM", read it as whatever system of record you run: a CRM, a loyalty engine, or your own platform. Nothing in the API is specific to any of them. Stell has prebuilt connectors for a growing set of POS and CRM platforms. If yours is one of them, you may not need to write integration code at all. The connector handles the flows in this guide. See the [partner directory](https://getstell.com/partners) for what's available, or [ask us about your stack](https://getstell.com/contact?context=docs:integration-connectors). ## Who builds what The dividing line is simple: you own the calls, Stell owns the wallets. * The pass engine: issuance, signing, and distribution for Apple Wallet and Google Wallet * Real-time pass updates and push notifications to customer devices * A permanent Add to Wallet link on every pass, with a hosted pass page or direct wallet delivery * Signed webhooks that tell your backend when a pass changes status * The merchant portal, for self-service management of your pass templates and change messages * A server-side service that calls the Stell API * An Add to Wallet touchpoint: a page, app screen, or email * An HTTPS webhook endpoint that receives status events * The mapping between your customer records and passes, via `externalId` * The business logic: when points change, when tiers change, what messages to send ## Before you start You need three things: 1. An **API key** for your backend (`Authorization: Bearer sk_prod_…`). [Create one in the portal](/api-reference/api-keys). It must stay server-side; see [Authentication](/api-reference/authentication). 2. Your **`programId`**, the identifier for your pass program, used in every create and lookup call. Find it with [List programs](/api-reference/programs/list-programs) or in the portal. 3. A **webhook subscription**: your endpoint URL, the event types you want, and a signing secret, [created in the portal](/api-reference/webhooks/overview). No Stell account yet? API keys and programs come with one. [Talk to a wallet specialist](https://getstell.com/contact?context=docs:integration-overview) about your integration and we'll get you set up. ## Core concepts **`externalId`.** Your own customer identifier, and the join key across the whole integration. You set it at pass creation, use it for lookups, and get it back in every webhook delivery. No extra mapping table needed. **Pass `id`.** Stell's identifier for a pass, returned when the pass is issued. Store it on your customer record: it is what keeps the integration to one pass per customer, since the API creates a new pass on every create call. If you don't store it, look it up by `programId` + `externalId` before every create. See [Add to Wallet](/api-reference/integration/add-to-wallet). **Pass `link`.** Every pass response carries a permanent public `link`. It never expires, so store it, email it, or print it as a QR code. Opened as-is it shows a hosted page with the right wallet button; with `?delivery=direct` it redirects straight to the wallet artifact. Only the optional `walletLink` (via `includeWalletLink=true`) and the deprecated per-wallet link endpoints are short-lived (roughly 15 minutes for Apple Wallet, roughly an hour for Google Wallet), and those you request at the moment of use. See [Add to Wallet](/api-reference/integration/add-to-wallet). **Wallet-controlled display.** Apple and Google decide the final presentation of passes and notifications, and may coalesce rapid updates. Don't design flows that rely on one visible notification per update. **The in-store payload.** What your POS reads from a scan or tap is a choice, not a constant. Set it with the `payload` field when issuing the pass, or configure it on the pass template with placeholders such as `{{externalId}}`. Left unset, it falls back to the pass `id`. See [In-store](/api-reference/integration/in-store). ## Endpoint summary The complete surface a typical integration touches: | Purpose | Endpoint | | - | - | | Look up an existing pass by `externalId` | [List passes](/api-reference/passes/list-passes) | | Issue a pass | [Create new pass](/api-reference/passes/create-new-pass) | | Issue passes in bulk (optional) | [Batch create passes](/api-reference/passes/batch-create-passes) | | Open the pass in a wallet | The `link` field on every pass response; no endpoint to call | | Update a pass | [Update pass](/api-reference/passes/update-pass) | | Record a transaction (optional) | [Create transaction](/api-reference/transactions/create-transaction) | | Void a pass | [Void pass](/api-reference/passes/void-pass) | Inbound to you: [webhook deliveries](/api-reference/webhooks/deliveries), signed POSTs to your endpoint when a pass changes status or a transaction is recorded. # Update a wallet pass from your backend Source: https://docs.getstell.com/api-reference/integration/updating-passes Change points, tier or membership status with one API call. Stell updates the pass in Apple Wallet and Google Wallet and can send a lock-screen notification. When points, tiers, or offers change in your systems, one API call updates the pass everywhere it lives. ```mermaid theme={null} sequenceDiagram autonumber participant B as Your backend participant S as Stell participant A as Apple Wallet participant G as Google Wallet B->>S: PATCH /v1/passes/{passId} S-->>B: 200 S--)A: Silent push, device fetches the refreshed pass S--)G: Wallet object updated directly ``` **One call updates everything.** [Update pass](/api-reference/passes/update-pass) with the new `loyaltyState` (`pointsBalance`, `currentTierId`, `stampsCollected`, voucher fields), or `membershipStatus`, or both, returns `200` with the full pass object, permanent `link` included, so you never need a separate call to recover it. Stell handles delivery to both platforms. **Voided and expired passes reject updates.** A pass in either state returns `409` with a `CONFLICT` code. See [Errors](/api-reference/errors). There's no wallet to sync the update to. **Notify when it matters.** Include an optional `notificationMessage` to surface a lock-screen notification with the update. Use it for tier upgrades and rewards, not every point change. You can also configure standing change messages on the [pass template](/pass-templates/anatomy), self-service in the portal. **No pass `id` stored? No problem.** Look it up first with [List passes](/api-reference/passes/list-passes) filtered by `programId` and `externalId`. **Display is wallet-controlled.** Apple Wallet and Google Wallet decide the final notification presentation and may coalesce rapid updates. Don't rely on one visible notification per call. # Stell API: issue Apple Wallet and Google Wallet passes Source: https://docs.getstell.com/api-reference/introduction One REST API to issue, update and void Apple Wallet and Google Wallet passes from your CRM, POS or backend. Base URL, resources and typical integrations. The Stell API gives your systems everything the merchant portal does. Issue and update passes for Apple Wallet and Google Wallet, manage programs and pass templates, record transactions, and read the state of your stores and terminals, all from your CRM, POS, e-commerce platform, or your own backend. Everything is scoped to your company. An API key only ever sees and changes your own company's data. ## Base URL All requests go to: ```text theme={null} https://api.platform.getstell.com ``` Every endpoint is versioned under the `/v1` path prefix. Every request must be authenticated. See [Authentication](/api-reference/authentication). ## How the resources fit together * **Companies, merchants, and stores.** Your company holds one or more merchants (brands). Each merchant has stores, physical or online. **Terminals** are the Stell tap points attached to those stores. * **Programs, pass templates, and passes.** A program is the loyalty product, membership, or ticket offering you run. Each program has one or more pass templates that define how the pass looks. Passes are issued to customers from those templates. * **Transactions.** The record of a pass used at a terminal, with a status of Approved, Denied, Failed, or Pending. * **Engagement.** Notifications and messages to pass holders. Engage is a gated module; these endpoints only work if Engage is enabled for your company. The sidebar groups the endpoints the same way, starting where most integrations start: issuing and managing passes. ## Typical integrations * Issue a pass from your own signup flow instead of the enrollment page. * Record transactions from your POS so purchases earn loyalty points automatically. * Keep customer data in sync by reading passes and transactions into your CRM. * Trigger a pass update (points balance, tier, expiry) when something changes in your systems. For a step-by-step walkthrough of these flows, start with the [integration guide](/api-reference/integration/overview). For updates in the other direction, where Stell notifies your systems, use webhooks. You create subscriptions in the merchant portal (see [event webhooks](/api-reference/webhooks/overview)), and [webhook deliveries](/api-reference/webhooks/deliveries) documents the requests your endpoint receives. Webhooks push events to you; the API is how you read and change state. ## Evaluating Stell Not a Stell customer yet? The reference and the integration guide are open to read before you sign anything. When you want to talk through your own stack, a wallet specialist will walk through your POS, CRM and pass types with you and show a live pass. Book a session about your integration. API keys come with your Stell account. # Create merchant Source: https://docs.getstell.com/api-reference/merchants/create-merchant /api-reference/openapi.json post /v1/merchants Creates a new merchant within your company. # Delete merchant Source: https://docs.getstell.com/api-reference/merchants/delete-merchant /api-reference/openapi.json delete /v1/merchants/{merchantId} Deletes a merchant permanently. Fails if merchant has existing stores. # Get merchant details Source: https://docs.getstell.com/api-reference/merchants/get-merchant-details /api-reference/openapi.json get /v1/merchants/{merchantId} Returns details for a specific merchant. # List merchants Source: https://docs.getstell.com/api-reference/merchants/list-merchants /api-reference/openapi.json get /v1/merchants Returns merchants for your company. Supports filtering and pagination. # Update merchant Source: https://docs.getstell.com/api-reference/merchants/update-merchant /api-reference/openapi.json patch /v1/merchants/{merchantId} Updates an existing merchant. Only provided fields will be modified. # Create program pass template Source: https://docs.getstell.com/api-reference/pass-templates/create-program-pass-template /api-reference/openapi.json post /v1/programs/{programId}/templates Create a new pass template within a program. Each program supports at most one template. # Delete program pass template Source: https://docs.getstell.com/api-reference/pass-templates/delete-program-pass-template /api-reference/openapi.json delete /v1/programs/{programId}/templates/{templateId} Permanently deletes a pass template from a program. Fails if the program has non-voided passes. # Get pass template details Source: https://docs.getstell.com/api-reference/pass-templates/get-pass-template-details /api-reference/openapi.json get /v1/templates/{templateId} Retrieve complete details about a specific pass template. # Get program pass template Source: https://docs.getstell.com/api-reference/pass-templates/get-program-pass-template /api-reference/openapi.json get /v1/programs/{programId}/templates/{templateId} Retrieve complete details about a specific template within a program. # List all pass templates Source: https://docs.getstell.com/api-reference/pass-templates/list-all-pass-templates /api-reference/openapi.json get /v1/templates Retrieve all pass templates across programs with filtering and pagination. # List program pass templates Source: https://docs.getstell.com/api-reference/pass-templates/list-program-pass-templates /api-reference/openapi.json get /v1/programs/{programId}/templates Retrieve all pass templates for a specific program. # Update program pass template Source: https://docs.getstell.com/api-reference/pass-templates/update-program-pass-template /api-reference/openapi.json patch /v1/programs/{programId}/templates/{templateId} Update design configuration or settings for an existing template within a program. # Batch create passes Source: https://docs.getstell.com/api-reference/passes/batch-create-passes /api-reference/openapi.json post /v1/passes/batch Create multiple passes in a single request. Supports up to 1000 passes per call with automatic duplicate detection. # Create new pass Source: https://docs.getstell.com/api-reference/passes/create-new-pass /api-reference/openapi.json post /v1/passes Create a new pass for a program. The pass is created in PREACTIVE status and includes a unique authentication token. The response includes the permanent link field for the pass. # Create pass image upload Source: https://docs.getstell.com/api-reference/passes/create-pass-image-upload /api-reference/openapi.json post /v1/passes/image-uploads Request an upload URL for a per-pass image override. Send the PNG or JPEG bytes to the returned URL with the declared Content-Type, then pass the uploadId as templateOverrides.images.{slot} when creating or updating a pass. You can also skip this step and pass a public HTTPS sourceUrl on the pass request directly. Unused uploads expire after 24 hours. # Create secure Apple Wallet pass Source: https://docs.getstell.com/api-reference/passes/create-secure-apple-wallet-pass /api-reference/openapi.json post /v1/apple/passes/secure Create a secure Apple Wallet pass with account binding and enhanced security. # Generate Apple Wallet installation link Source: https://docs.getstell.com/api-reference/passes/generate-apple-wallet-installation-link /api-reference/openapi.json get /v1/passes/{passId}/links/apple Generate a time-limited URL for adding the pass to Apple Wallet. Deprecated: prefer the permanent link field returned on every pass object, which never expires and resolves to the applicable wallet action for any wallet type. # Generate Google Wallet save link Source: https://docs.getstell.com/api-reference/passes/generate-google-wallet-save-link /api-reference/openapi.json get /v1/passes/{passId}/links/google Generate a time-limited URL for adding the pass to Google Wallet. The link expires after 24 hours. Deprecated: prefer the permanent link field returned on every pass object, which never expires and resolves to the applicable wallet action for any wallet type. # Get pass details Source: https://docs.getstell.com/api-reference/passes/get-pass-details /api-reference/openapi.json get /v1/passes/{passId} Retrieve details of a specific pass. # Get pass registrations Source: https://docs.getstell.com/api-reference/passes/get-pass-registrations /api-reference/openapi.json get /v1/passes/{passId}/registrations Retrieve Apple Wallet device registrations for a specific pass. # Get pass transactions Source: https://docs.getstell.com/api-reference/passes/get-pass-transactions /api-reference/openapi.json get /v1/passes/{passId}/transactions Retrieve transaction history for a specific pass. # List passes Source: https://docs.getstell.com/api-reference/passes/list-passes /api-reference/openapi.json get /v1/passes Returns passes for your company. Supports filtering, sorting, and pagination. # Update pass Source: https://docs.getstell.com/api-reference/passes/update-pass /api-reference/openapi.json patch /v1/passes/{passId} Update an existing pass including customer information, status, and semantic tags. Changes propagate to the customer's mobile wallet asynchronously (eventually consistent) once the pass record is written, so a wallet may briefly still show the previous values. The response includes the permanent link field for the pass. # Void pass Source: https://docs.getstell.com/api-reference/passes/void-pass /api-reference/openapi.json delete /v1/passes/{passId} Void an existing pass. This performs a soft delete by setting the pass status to VOIDED. This action cannot be undone. # Create new program Source: https://docs.getstell.com/api-reference/programs/create-new-program /api-reference/openapi.json post /v1/programs Create a new loyalty or entry program. The program will be created in DRAFT status and can be published later. # Delete program Source: https://docs.getstell.com/api-reference/programs/delete-program /api-reference/openapi.json delete /v1/programs/{programId} Deletes a program permanently. Fails if the program has non-voided passes. # Get encryption key Source: https://docs.getstell.com/api-reference/programs/get-encryption-key /api-reference/openapi.json get /v1/programs/{programId}/encryption-key Retrieve the ECDH P-256 encryption key pair for a program. Returns both compressed (Apple VAS) and uncompressed (Google Smart Tap) public key formats when available, alongside the private key needed to derive the shared secret during an NFC tap. A program has at most one key pair; a program that has never had keys generated returns 404 rather than an empty result. Treat the response as secret material: it grants the ability to decrypt that program's pass payloads. # Get program details Source: https://docs.getstell.com/api-reference/programs/get-program-details /api-reference/openapi.json get /v1/programs/{programId} Retrieve details of a specific program # List programs Source: https://docs.getstell.com/api-reference/programs/list-programs /api-reference/openapi.json get /v1/programs Returns programs for your company. Supports filtering by type and status. # Update program Source: https://docs.getstell.com/api-reference/programs/update-program /api-reference/openapi.json patch /v1/programs/{programId} Update an existing program. Supports partial updates. # Create store Source: https://docs.getstell.com/api-reference/stores/create-store /api-reference/openapi.json post /v1/stores Creates a new store for a merchant. # Delete store Source: https://docs.getstell.com/api-reference/stores/delete-store /api-reference/openapi.json delete /v1/stores/{storeId} Deletes a store permanently. Fails if store has existing terminals or transactions. # Get store details Source: https://docs.getstell.com/api-reference/stores/get-store-details /api-reference/openapi.json get /v1/stores/{storeId} Returns details for a specific store. # List stores Source: https://docs.getstell.com/api-reference/stores/list-stores /api-reference/openapi.json get /v1/stores Returns stores for your company. Supports filtering by merchant. # Update store Source: https://docs.getstell.com/api-reference/stores/update-store /api-reference/openapi.json patch /v1/stores/{storeId} Update store information. Supports partial updates. # Health check Source: https://docs.getstell.com/api-reference/system/health-check /api-reference/openapi.json get /v1/health Returns service status, version, and environment details. Does not require authentication. # Create terminal Source: https://docs.getstell.com/api-reference/terminals/create-terminal /api-reference/openapi.json post /v1/terminals Create a new terminal at a store. # Delete terminal Source: https://docs.getstell.com/api-reference/terminals/delete-terminal /api-reference/openapi.json delete /v1/terminals/{terminalId} Deletes a terminal permanently. # Get terminal details Source: https://docs.getstell.com/api-reference/terminals/get-terminal-details /api-reference/openapi.json get /v1/terminals/{terminalId} Retrieve details of a specific terminal. # List terminals Source: https://docs.getstell.com/api-reference/terminals/list-terminals /api-reference/openapi.json get /v1/terminals List all terminals with optional filters. # Update terminal Source: https://docs.getstell.com/api-reference/terminals/update-terminal /api-reference/openapi.json patch /v1/terminals/{terminalId} Update terminal information. Supports partial updates. # Create transaction Source: https://docs.getstell.com/api-reference/transactions/create-transaction /api-reference/openapi.json post /v1/transactions Record a new transaction against a pass. # Get transaction details Source: https://docs.getstell.com/api-reference/transactions/get-transaction-details /api-reference/openapi.json get /v1/transactions/{transactionId} Retrieve details of a specific transaction. # List transactions Source: https://docs.getstell.com/api-reference/transactions/list-transactions /api-reference/openapi.json get /v1/transactions List all transactions with optional filters. # Webhook deliveries: payloads, retries and signature verification Source: https://docs.getstell.com/api-reference/webhooks/deliveries The requests Stell sends to your webhook endpoint: headers, payloads for each event, retry behavior, and signature verification in Node.js and Python. This page documents the requests Stell sends to a webhook endpoint, for the developer building the receiver. For creating and managing subscriptions in the portal, see [event webhooks](/api-reference/webhooks/overview). Webhooks push events to you; to read or change state from your side, such as looking up a pass or recording a transaction, use the [Stell API](/api-reference/introduction). ## The delivery request Each delivery is an HTTP `POST` to the subscription's URL with a JSON body: ```http theme={null} POST /webhooks/stell HTTP/1.1 Content-Type: application/json User-Agent: Stell-Webhooks/1.0 X-Stell-Event: PASS_STATUS_CHANGED X-Stell-Delivery-Id: 9d4c1e0a...74b25f X-Stell-Signature: t=1717160000,v1=8f2a4c...e1b9 ``` | Header | Purpose | | - | - | | `X-Stell-Event` | The event type of this delivery. Use it to route to the right handler. | | `X-Stell-Delivery-Id` | A unique identifier for the logical delivery, stable across retries. **Use this as your idempotency key.** | | `X-Stell-Signature` | The payload signature, present when the subscription has a signing secret. See [verify the signature](#verify-the-signature). | Any custom headers configured on the subscription, for example `Authorization: Bearer `, are sent verbatim on every delivery. Respond with any **2xx** status code (`200`, `202`, …) to acknowledge the delivery. Only the status code matters; the response body is ignored. ## Event payloads Payload conventions across all events: * Timestamps are ISO 8601 strings in UTC, for example `2026-05-31T14:32:00.000Z`. * Identifiers are opaque strings. Don't parse meaning out of their format. * Optional fields with no value are **omitted entirely**, never sent as `null`. Check for presence, not for null. * Payloads carry identifiers, not personal data. To link a delivery to a customer in your own system, set an **External ID** when the pass is issued. It's echoed back in every `PASS_CREATED` and `PASS_STATUS_CHANGED` delivery. ### Pass created `PASS_CREATED` fires once, when a pass is issued, whatever issued it: the portal, an enrollment page, or the [API](/api-reference/passes/create-new-pass). It arrives before the customer has added the pass to a wallet, so `status` is normally `PREACTIVE`. The payload mirrors [Pass status changed](#pass-status-changed) without `previousStatus`, since a pass being issued has no prior lifecycle state. | Field | Type | Optional | Description | | - | - | - | - | | `id` | string | No | The pass that was issued. | | `companyId` | string | No | Your account identifier. | | `programId` | string | Yes | The program the pass belongs to. | | `externalId` | string | Yes | Your external identifier for the pass, if one was set when the pass was issued. | | `status` | string | No | The status the pass was created in, usually `PREACTIVE`. | | `walletType` | string | Yes | The wallet platform: `APPLE_WALLET`, `GOOGLE_WALLET`, or `UNKNOWN`. | | `occurredAt` | string (ISO 8601) | No | When the pass was issued. | ```json Example theme={null} { "id": "pass_ghi789", "companyId": "company_abc123", "programId": "program_def456", "externalId": "member_001", "status": "PREACTIVE", "walletType": "APPLE_WALLET", "occurredAt": "2026-05-31T14:32:00.000Z" } ``` Subscribing to both `PASS_CREATED` and `PASS_STATUS_CHANGED` gives you the full arc: issued, then added to the wallet. If your own system issued the pass, `PASS_CREATED` mainly earns its place for passes issued elsewhere. ### Pass status changed `PASS_STATUS_CHANGED` fires each time a pass moves between [lifecycle states](/passes/lifecycle): `PREACTIVE`, `ACTIVE`, `INACTIVE`, `VOIDED`, `REVOKED`, `EXPIRED`. The payload carries both the new and the previous status, so you can detect specific transitions without tracking prior state yourself. The enrollment moment, a customer adding their pass to Apple Wallet or Google Wallet, is the transition `status: "ACTIVE"` with `previousStatus: "PREACTIVE"`. Filter on that pair if you only care about enrollments. | Field | Type | Optional | Description | | - | - | - | - | | `id` | string | No | The pass whose status changed, the same ID the pass was created with. | | `companyId` | string | No | Your account identifier. | | `programId` | string | Yes | The program the pass belongs to. | | `externalId` | string | Yes | Your external identifier for the pass, if one was set when the pass was issued. | | `status` | string | No | The new status. | | `previousStatus` | string | Yes | The status before this change. Absent if there was no prior status. | | `walletType` | string | Yes | The wallet platform: `APPLE_WALLET`, `GOOGLE_WALLET`, or `UNKNOWN`. | | `occurredAt` | string (ISO 8601) | No | When the status change occurred. | ```json Example theme={null} { "id": "pass_ghi789", "companyId": "company_abc123", "programId": "program_def456", "externalId": "member_001", "status": "ACTIVE", "previousStatus": "PREACTIVE", "walletType": "APPLE_WALLET", "occurredAt": "2026-05-31T14:35:12.000Z" } ``` ### Transaction created `TRANSACTION_CREATED` fires when a new transaction is recorded against a pass, most commonly a customer tapping their pass at a terminal. It's delivered once per transaction. | Field | Type | Optional | Description | | - | - | - | - | | `id` | string | No | Unique identifier of the transaction. | | `companyId` | string | No | Your account identifier. | | `passId` | string | No | The pass the transaction applies to. | | `storeId` | string | No | The store where the transaction occurred. | | `programId` | string | Yes | The program the pass belongs to. | | `terminalId` | string | Yes | The terminal that produced the transaction, if applicable. | | `type` | string | Yes | Transaction type: `SCAN_ONLY`, `CHECK_IN`, `CHECK_OUT`, `PASS_ONLY`, `PAYMENT_ONLY`, `PASS_AND_PAYMENT`, `ONLINE`, or `UNKNOWN`. | | `instrument` | string | Yes | How the transaction was captured: `CARD`, `CASH`, `MOBILE`, `OTHER`, `APPLE_VAS`, `GOOGLE_SMART_TAP`, `QR`, `NFC_TYPE_1`..`NFC_TYPE_5`, or `MIFARE`. Same vocabulary as the [API's transaction enums](/api-reference/transactions/create-transaction). | | `status` | string | Yes | Outcome: `APPROVED`, `DENIED`, `FAILED`, `PENDING`. Same vocabulary as the [API's transaction enums](/api-reference/transactions/create-transaction). | | `statusReason` | string | Yes | Human-readable reason accompanying the status, when available. | | `reference` | string | Yes | Your or the terminal's reference for the transaction. | | `transactionDate` | string (ISO 8601) | Yes | When the transaction took place, if reported separately from `createdAt`. | | `pointsEarned` | number | Yes | Loyalty points earned in this transaction. | | `pointsSpent` | number | Yes | Loyalty points spent in this transaction. | | `remainingUsesBefore` | number | Yes | Remaining entry/access uses before this transaction (access-control passes). | | `remainingUsesAfter` | number | Yes | Remaining entry/access uses after this transaction (access-control passes). | | `payment` | object | Yes | Payment details, when the transaction involved a payment: `amount` (with `value` and ISO 4217 `currency`) and `method`. | | `additionalData` | object | Yes | Free-form key/value data attached to the transaction. | | `createdAt` | string (ISO 8601) | No | When the transaction record was created. | | `updatedAt` | string (ISO 8601) | No | When the transaction record was last updated. | ```json Example theme={null} { "id": "txn_8f2a1c9e", "companyId": "company_abc123", "programId": "program_def456", "passId": "pass_ghi789", "storeId": "store_jkl012", "terminalId": "terminal_mno345", "reference": "POS-2026-0531-0042", "transactionDate": "2026-05-31T14:31:58.000Z", "type": "CHECK_IN", "instrument": "APPLE_VAS", "status": "APPROVED", "pointsEarned": 10, "remainingUsesAfter": 4, "payment": { "amount": { "value": 49.9, "currency": "NOK" }, "method": "CARD" }, "additionalData": { "lane": "north-entrance" }, "createdAt": "2026-05-31T14:32:00.000Z", "updatedAt": "2026-05-31T14:32:00.000Z" } ``` In this example, optional fields with no value, such as `statusReason` and `pointsSpent`, are simply absent. ## Retries and idempotency Deliveries are **at-least-once**: your endpoint receives every event at least once, and may occasionally receive the same event twice, for example when a retry lands after your endpoint already processed the original but the response was lost. Processing the same delivery twice must not produce a duplicate side effect. A delivery succeeds when your endpoint responds with a 2xx status code. Anything else, whether a non-2xx status, a connection or TLS error, or a timeout, triggers a retry with short backoff, for up to **3 attempts** in total: | Attempt | Delay before sending | | - | - | | 1 | Immediate | | 2 | \~2 seconds | | 3 | \~4 seconds | After the third failed attempt, the delivery cycle fails, but the event is not gone. Failed events are re-driven from Stell's internal queue, so during a sustained outage your endpoint sees repeated delivery cycles for up to roughly 24 hours before the event ages out. The delivery ID stays the same across all of them, which is why deduplication (below) is essential. An endpoint that was down for an hour will receive well over three POSTs for the same event once it recovers. ### Deduplicate on the delivery ID The same `X-Stell-Delivery-Id` identifies the same logical delivery across retries: 1. Read `X-Stell-Delivery-Id` from the incoming request. 2. If you've already processed that ID, acknowledge with 2xx and do nothing else. 3. Otherwise process the event, record the ID as processed, then acknowledge with 2xx. Record the ID and the side effect atomically, or the ID first, so a crash between processing and acknowledging can't cause a duplicate on the next retry. Keep processed IDs for at least a day, because failed events can be re-delivered for up to roughly 24 hours. ### Endpoint recommendations * **Acknowledge fast, process asynchronously.** Respond 2xx as soon as the delivery is safely recorded, for example queued for background processing, and do the heavy work out of band. * **Verify the signature first**, before doing any work, when you use a signing secret. * **Tolerate unknown event types.** New types are added over time; if `X-Stell-Event` is something you don't handle, acknowledge with 2xx and ignore it. * **Don't rely on ordering.** Deliveries aren't guaranteed to arrive in the order events occurred. For pass status changes, reconcile using `status`, `previousStatus`, and `occurredAt` rather than arrival order. ## Verify the signature When the subscription has a [signing secret](/api-reference/webhooks/overview#create-a-subscription), every delivery is signed so you can verify it genuinely came from Stell and wasn't tampered with in transit. Without a secret, deliveries are sent unsigned. Configuring one is strongly recommended for production endpoints. The `X-Stell-Signature` header uses the widely adopted Stripe-style format, so existing verification snippets need minimal changes: ``` X-Stell-Signature: t=1717160000,v1=8f2a4c...e1b9 ``` * `t`: the Unix timestamp, in **seconds**, when Stell signed the payload. * `v1`: a lowercase hex-encoded **HMAC-SHA256** over the string `.`, meaning the timestamp, a literal period, then the exact raw request body bytes. The HMAC key is the subscription's signing secret, used as a UTF-8 string (including its `whsec_` prefix). To verify: 1. Read the **raw request body** before any JSON parsing. Re-serializing parsed JSON changes the bytes and breaks verification. 2. Parse the header into `t` and `v1`. 3. Compute `HMAC-SHA256(signingSecret, ".")` and hex-encode it. 4. Compare with `v1` using a **constant-time** comparison. 5. Reject deliveries whose timestamp is too old or in the future. A tolerance of about 5 minutes is a good default and defends against replays. ```javascript Node.js theme={null} const crypto = require("node:crypto"); const TOLERANCE_SECONDS = 5 * 60; function verifyStellSignature(rawBody, signatureHeader, signingSecret) { // Parse "t=,v1=" into its parts. const parts = Object.fromEntries( signatureHeader.split(",").map((part) => { const index = part.indexOf("="); return [part.slice(0, index), part.slice(index + 1)]; }), ); const timestamp = Number(parts.t); const provided = parts.v1; if (!Number.isFinite(timestamp) || typeof provided !== "string") { return false; } // Reject stale or future-dated deliveries (replay protection). const now = Math.floor(Date.now() / 1000); if (Math.abs(now - timestamp) > TOLERANCE_SECONDS) { return false; } // Recompute the signature over ".". const signedContent = `${timestamp}.${rawBody}`; const expected = crypto .createHmac("sha256", signingSecret) .update(signedContent) .digest("hex"); // Constant-time compare. Buffers must be equal length for timingSafeEqual. const expectedBuffer = Buffer.from(expected, "hex"); const providedBuffer = Buffer.from(provided, "hex"); if (expectedBuffer.length !== providedBuffer.length) { return false; } return crypto.timingSafeEqual(expectedBuffer, providedBuffer); } ``` ```python Python theme={null} import hashlib import hmac import time TOLERANCE_SECONDS = 5 * 60 def verify_stell_signature(raw_body: bytes, signature_header: str, signing_secret: str) -> bool: # Parse "t=,v1=" into its parts. parts = {} for part in signature_header.split(","): key, _, value = part.partition("=") parts[key] = value try: timestamp = int(parts["t"]) provided = parts["v1"] except (KeyError, ValueError): return False # Reject stale or future-dated deliveries (replay protection). now = int(time.time()) if abs(now - timestamp) > TOLERANCE_SECONDS: return False # Recompute the signature over ".". signed_content = f"{timestamp}.".encode("utf-8") + raw_body expected = hmac.new( signing_secret.encode("utf-8"), signed_content, hashlib.sha256, ).hexdigest() # Constant-time compare. return hmac.compare_digest(expected, provided) ``` The signature covers the **raw body bytes**. In Express, use `express.raw({ type: 'application/json' })` (or read the raw stream) for the webhook route. Parsing the body as JSON and `JSON.stringify`-ing it back changes the bytes and verification fails. Common pitfalls: * **Verifying parsed JSON instead of the raw body.** Capture the body before any JSON middleware touches it. * **Plain `==` comparison.** Use `crypto.timingSafeEqual` or `hmac.compare_digest` to avoid leaking signature bytes through timing. * **Skipping the timestamp check.** Without it, a captured delivery can be replayed indefinitely. * **Treating `t` as milliseconds.** It's Unix time in seconds. # Event webhooks for wallet pass events Source: https://docs.getstell.com/api-reference/webhooks/overview Subscribe your own endpoint to pass events, such as a pass added to a wallet or tapped at a terminal, from the Stell merchant portal. **Event webhooks** notify your own systems the moment something happens to a pass, for example when a customer adds their pass to a wallet, or taps it at a terminal. You subscribe an endpoint once, and Stell sends it a small JSON message for each event; delivery, retries, and signing are handled for you. Event webhooks card on the program page Three event types are available: * **Pass status changed.** Fires each time a pass moves between [lifecycle states](/passes/lifecycle). The most common use is detecting enrollment: when a customer adds their pass to Apple Wallet or Google Wallet, the pass goes from `PREACTIVE` to `ACTIVE`, and that change fires the webhook. * **Pass created.** Fires the moment a pass is issued, before the customer has added it to a wallet. Use it to mirror new passes into your own systems no matter where they were issued: the portal, an enrollment page, or the API. * **Transaction created.** Fires when a transaction is recorded against a pass, for example an NFC tap at a terminal. Subscriptions belong to a program. If you run several programs and want the same events from each, create a subscription on each program. The same endpoint URL works fine, and every delivery says which program it came from. ## Before you start * **An HTTPS endpoint** that can receive POST requests. The URL must be public; plain HTTP, localhost, and private network addresses are rejected. * **Which events you need.** For enrollment notifications, **Pass status changed** is enough. * **Your endpoint's credentials**, if it requires them, for example a bearer token to send in an `Authorization` header. This page covers setting up the subscription in the portal. Give whoever builds the receiving endpoint the [webhook deliveries](/api-reference/webhooks/deliveries) reference. It documents the exact requests Stell sends. ## Create a subscription 1. Open [your program](/programs/the-program-page) and find the **Event webhooks** card. 2. Click **Configure** (or **Manage**, if the program already has subscriptions), then **Add subscription**. 3. Give it a **Name** that says where the events go, for example "Loyalty CRM sync", and optionally a **Description**. 4. Enter the **Webhook URL**, the HTTPS endpoint that receives the deliveries. 5. Under **Signing secret**, click **Generate**, then **Copy**. Sharing this secret with your endpoint lets it verify that deliveries really come from Stell. This is optional, but recommended. 6. Tick the **Event types** the endpoint should receive. The **Example payload** panel next to the form shows exactly what your endpoint gets for each selected type. 7. Leave **Active** on. 8. If your endpoint requires authentication, add it under **Custom Headers**, described below. 9. Click **Create subscription**. Create event subscription dialog Treat the signing secret like a password: store it with your endpoint's configuration and share it only through a secure channel, never email or chat. You can copy it again later from the edit dialog, and **Regenerate** replaces it. After regenerating, update your endpoint or its signature checks will start failing. ### Custom headers Custom headers are sent with every delivery, typically your endpoint's authentication. Click **Add Header** and pick a preset: * **Authorization**, for token auth. Enter the value your endpoint expects, for example `Bearer your-token-here`. * **X-API-Key**, for endpoints keyed by API key. * **Custom**, any header of your own, as long as its name starts with `X-`. Only `Authorization` and `X-*` headers are allowed. Headers without a value aren't saved, and if two headers share a name, only the last value is kept. ## Manage subscriptions Open **Manage** on the **Event webhooks** card to see the program's subscriptions. Each shows its name, event types, and an **Active** or **Inactive** badge. * **Edit** changes anything about the subscription: name, URL, events, secret, headers. The program it belongs to can't be changed; create a subscription on the other program instead. * **Pause** turns off the **Active** switch in the edit dialog. Deliveries stop, but the configuration is kept for when you switch it back on. * **Delete** stops all event deliveries to the endpoint permanently. This can't be undone. ## Test your webhook There's no test button. Verify with a real pass instead, which also proves the whole chain: 1. [Issue a pass](/passes/issuing) in the subscribed program, with your own details. 2. Add it to your wallet. 3. Adding the pass activates it, and that status change fires the webhook within moments. Check that your endpoint received the delivery. Issuing a pass doesn't fire **Pass status changed**. The first delivery of that type comes when the customer adds the pass to a wallet, and a pass that's never added sends none. To catch the issuing moment itself, subscribe to **Pass created**. ## If deliveries don't arrive * **Right program?** The most common miss: the pass belongs to a different program than the one with the subscription. * **Subscription Active**, with the right event type ticked? * **Did a subscribed event actually happen?** Issuing a pass fires **Pass created** but not **Pass status changed**, which waits for the customer to add the pass to a wallet. * **Does your endpoint answer with a 2xx status?** Failed deliveries are retried, then re-driven for up to roughly 24 hours. See [retries](/api-reference/webhooks/deliveries#retries-and-idempotency). * **Expecting a header that isn't arriving?** Only `Authorization` and `X-*` custom headers are sent; anything else was dropped when the subscription was saved. Still stuck? [Contact support](/support/contact). They can check the delivery attempts for your subscription. # Automated push messages for wallet passes Source: https://docs.getstell.com/engage/automations Set up push messages that send themselves when a pass is installed, on a birthday, after inactivity or when points or tier change, with a frequency cap. An **automation** is a push message that sends itself when something happens, such as when a customer adds your pass, has a birthday, or reaches a points threshold. Set it up once, and it keeps running until you pause it. Automations ## Triggers | Trigger | Fires | | - | - | | **Pass Installed** | When a new pass is added to a wallet, optionally delayed by a number of hours | | **Birthday** | On or around the contact's birthday. Use days before or after (for example, 7 days before) | | **Pass Anniversary** | On the anniversary of the pass installation | | **Inactive Days** | When a contact hasn't engaged for a number of days | | **Points Earned** | When points reach a threshold | | **Tier Changed** | When the customer's loyalty tier changes | ## Create an automation 1. Open **Automations** in the sidebar and click **Create Automation**. 2. Name the automation and choose the **Trigger Type** and its settings. 3. Optionally add **Conditions** to narrow who receives it, and a **Frequency Cap** (at most N times per N days) so nobody gets spammed by a repeating trigger. 4. Write the message: up to 150 characters, with variables for personalization, and a live lock-screen preview. 5. Click **Save and Activate** to start it, or **Save as Draft** to finish later. The trigger type is fixed once the automation is created. To switch triggers, create a new automation. ## Manage automations The automations list groups everything under **All**, **Drafts**, **Active**, and **Paused**. Open an automation to see its stats (total sent, queued, delivery rate, and when it last triggered), and use **Pause** / **Activate** to stop and resume it, or **Edit** to change its settings and message. # Send push notification campaigns to pass holders Source: https://docs.getstell.com/engage/campaigns Send a one-off push message, such as an offer, announcement or reminder, to everyone with your pass or to a chosen segment, right away or on a schedule. A **campaign** is a one-off push message (an offer, an announcement, a reminder) sent to an audience you choose. In the sidebar, campaigns live under **Push**. Create a campaign ## Send a message Creating a campaign is a four-step flow: **Segment → Message → Schedule → Review**. Pick **All Pass Holders** to reach everyone with your pass installed, or **By Segments** to target specific [segments](/engage/contacts-and-segments). The **Estimated Reach** card shows how many pass holders can actually receive the message. Up to 150 characters. Use the **Insert variable** picker to personalize, for example with a first name or points balance, and watch the preview show how the notification lands on a customer's lock screen. * **Send Immediately**: notifications go out right away. * **Send at a Scheduled Time**: pick a date, time, and timezone. * **Send Recurring**: repeat daily, weekly, or monthly from a start date. Check the audience, message, and schedule summary, give the campaign a name, and click **Send Campaign** (or **Schedule Campaign**). You can also **Save as Draft** to finish later. ## Following up Open a campaign to see its details and delivery stats: how many notifications were delivered and the audience, message, and schedule it ran with. Sent campaigns are read-only; every individual notification also appears in the [notification history](/engage/notifications). ## Tips * Personalized messages perform better: `Hi {{firstName}}` beats a generic blast. * Scheduled sends respect the timezone you pick, so a 09:00 send arrives at 09:00 local to that timezone, not to each recipient. * Reach only counts customers with the pass installed. If reach looks low, your customers may have [Pre-Active or Inactive passes](/passes/lifecycle). # Contacts and audience segments in Engage Source: https://docs.getstell.com/engage/contacts-and-segments Add and import the customers you message in Engage, then group them into dynamic or static segments so each campaign reaches the right audience. **Contacts** are the customers you can message; **segments** group them into audiences you can target with [campaigns](/engage/campaigns). Contacts ## Contacts Open **Contacts** in the sidebar to see your audience. Each contact shows their name and email, external ID, pass status, program, and segment memberships. Search by name, email, or external ID, and filter by segment or **Show only contacts with an active pass**. ### Add contacts * **One at a time**: click **Add Contact** and fill in the name, optional email and external ID, the program, and any segments. * **In bulk**: click **Import** to upload a CSV or XML file. You map your file's columns to contact fields and review a preview before importing. Contacts can also be created automatically through the API or integrations connected to your programs. ### Work with a contact Click a contact to open their details, where you can **Edit**, **Send Notification** (a direct message to just that contact's pass), or **Delete**. Select multiple rows to delete in bulk. ## Segments Open **Segments** to see your audiences. Each segment card shows its member count, how many of those members hold an installed pass, and the resulting reachable percentage. ### Create a segment Click **Add Segment** and choose the type: * **Dynamic**: define criteria (field, operator, value), and the segment keeps itself up to date as contacts change. * **Static**: upload a fixed list of contacts. Segments can also be created via the API or data imports, which is useful when your CRM already knows your audiences. Watch the **reachable** percentage on a segment before sending to it. Members without an installed pass can't receive push messages, so a 1,000-member segment with 40% reachability delivers to 400 people. # Push notification history and delivery status Source: https://docs.getstell.com/engage/notifications Review every push notification sent to your pass holders from campaigns, automations and direct sends, and find out why a message failed to deliver. The **Notifications** page is the audit trail of Engage: every push notification that has been sent, or is about to be, across campaigns, automations, and direct sends. Notification history ## Reading the history Notifications are grouped under **All**, **Pending**, **Sent**, and **Failed**. Each row shows: * **Message**: the delivered text, linking to the pass where relevant. * **Source**: a **Campaign** (links to the campaign), an **Automation** (links to the automation), or **Direct** (sent to a single contact from their detail view). * **Program**, **Status** (Pending, Sent, or Failed), and the sent time. Click a notification to see its full details. ## Troubleshooting * **Pending** notifications are queued. Scheduled campaigns sit here until their send time. * **Failed** notifications are kept under the **Failed** tab for troubleshooting. A common cause is the customer removing the pass from their wallet after the message was queued. If the same audience keeps failing, [contact support](/support/contact) with the notification details. # Engage: push notifications to wallet passes Source: https://docs.getstell.com/engage/overview Engage, Stell's messaging module, sends push notifications to the lock screen through the wallet passes your customers already carry. No app needed. **Engage** is the messaging side of the portal: push notifications delivered straight to the wallet passes your customers already carry. No app, no SMS costs: the message lands on their lock screen through the pass. Engage overview Engage is an optional module. If you don't see it in your sidebar, it isn't enabled for your company. [Contact support](/support/contact) to get started. ## What's in Engage | Page | What it's for | | - | - | | **Overview** | Your Engage dashboard: total contacts, active segments, active automations, sent campaigns, and recent activity | | **Contacts** | The customers you can message. See [contacts and segments](/engage/contacts-and-segments) | | **Segments** | Audience groups for targeting. See [contacts and segments](/engage/contacts-and-segments) | | **Push** | Send a one-off message to an audience. See [campaigns](/engage/campaigns) | | **Automations** | Messages that send themselves when something happens, like a birthday. See [automations](/engage/automations) | | **Message templates** | Ready-made messages to start from. See [message templates](/engage/templates) | | **Notifications** | The history of everything sent. See [notification history](/engage/notifications) | ## How a message reaches a customer A push notification can only reach customers who have your pass **installed in their wallet**. Your own systems can send one too, by [updating a pass through the API](/api-reference/integration/updating-passes) with a notification message. When you pick an audience, the portal shows the **estimated reach** (how many of the targeted contacts can actually receive the message), so there are no surprises at send time. ## Where to start If this is your first time in Engage, send yourself a test: [create a campaign](/engage/campaigns) targeted at a small segment, or start from a [message template](/engage/templates). To learn why pass updates work as a messaging channel you already own, read [pass updates are push notifications you already own](https://getstell.com/articles/pass-updates-are-push-notifications-you-already-own). # Push message templates for wallet passes Source: https://docs.getstell.com/engage/templates Start from ready-made push messages, such as welcomes, birthday greetings and promotions, with placeholders that fill in each customer's own details. **Message templates** are ready-made push messages (welcomes, birthday greetings, promotions) that you can use as a starting point instead of writing from scratch. Message templates ## Use a template 1. Open **Message templates** in the sidebar and browse the gallery. Each card shows the message with its personalization placeholders, like **First Name**, rendered as pills. 2. Click **Use Template** on the one you want. 3. The portal opens the right tool with the message pre-filled: a [campaign](/engage/campaigns) for one-off messages, or a [new automation](/engage/automations) with the matching trigger pre-selected for event-driven templates like birthdays. 4. Adjust the text, choose your audience, and send as usual. ## Placeholders Placeholders are filled in per recipient when the message is sent. `{{firstName}}` becomes "Anna" for Anna and "Jonas" for Jonas. You can add or remove placeholders after picking a template. They work the same as the **Insert variable** picker in the campaign editor, and the preview always shows the message with realistic sample values. # Which wallet buttons customers see on each device Source: https://docs.getstell.com/enrollment/adding-to-wallet Which Add to Apple Wallet and Add to Google Wallet buttons customers see on iPhone, Android and computers, and what happens when they tap one. The last step of [enrollment](/enrollment/how-enrollment-works) is adding the pass to the customer's wallet. Which buttons appear (**Add to Apple Wallet**, **Add to Google Wallet**, or both) and what happens on tap depends on your program's setup and on the customer's device and browser. This page is the reference for both, and the same rules apply to the [wallet button widget](/enrollment/wallet-button-widget) on your own site. This behavior comes with the experiences Stell hosts: the enrollment flow and the [wallet button widget](/enrollment/wallet-button-widget). Passes delivered another way, such as wallet links in [emails](/enrollment/wallet-button-widget#in-your-emails), or an integration your developers build on the [Stell API](/api-reference/integration/add-to-wallet), don't include the QR code hand-off. The pass link an API integration sends shows the right wallet button for the pass, but on a computer it offers the link to copy rather than a QR code. Pick the customer's device: **What the customer sees:** the **Add to Apple Wallet** button, in Safari, Chrome, or any other browser. Google Wallet is never offered on iPhone (there is no Google Wallet app on iOS). **What tapping does:** the pass opens straight in Apple Wallet. One tap on **Add** and it's done. If your program offers only Google Wallet, an iPhone visitor instead sees "No compatible wallet is available for your device" with a suggestion to try an Android device. **What the customer sees:** the **Add to Google Wallet** button. Apple Wallet passes can't open on Android, so that button is never offered here. **What tapping does:** Google's save page opens and the pass lands in Google Wallet. If your program offers only Apple Wallet, an Android visitor instead sees "No compatible wallet is available for your device" with a suggestion to try an iPhone or Mac. **What the customer sees:** every wallet your program offers. A program with both shows **Add to Apple Wallet** and **Add to Google Wallet** side by side, and the customer chooses. The wallet step on a computer with the Add to Apple Wallet and Add to Google Wallet buttons side by side **What tapping Add to Apple Wallet does** depends on the browser: * **Safari on a Mac**: the Wallet app on the Mac opens with the pass. * **Any other browser** (Chrome, Edge, or Firefox on a Mac; any browser on Windows): a **Scan to add your pass** dialog shows a QR code. The customer points their iPhone camera at it and the pass opens in Apple Wallet on the phone. The pass is never downloaded as a file. The Scan to add your pass dialog showing a QR code to scan with an iPhone The pass link behind the QR code is time-limited. If the customer waits too long, the code stops working. Closing the dialog and tapping **Add to Apple Wallet** again produces a fresh one. **What tapping Add to Google Wallet does** is the same in every browser: Google's save page opens, the customer signs in to their Google account, and the pass appears in Google Wallet on their Android phone. Two things hold on every device: * The pass is created the moment the customer taps a wallet button. The button shows a spinner while the pass is prepared. * Each pass belongs to exactly one wallet. A customer who picks Apple Wallet gets an Apple Wallet pass, not a copy in both. Wallet-specific enrollment links (for example from your emails) can preselect one wallet, but the device and program checks still apply: a link asking for a wallet the device can't hold falls back to whatever is available. A customer stuck on this step? See [enrollment troubleshooting](/enrollment/troubleshooting) for the situations support staff hear about and how to resolve them. # How customers enroll and get their wallet pass Source: https://docs.getstell.com/enrollment/how-enrollment-works How customers get your pass through the web or App Clip enrollment flow: entry points, the form and consents, member verification and adding it to a wallet. **Enrollment** is how your customers get their pass. They open an enrollment link, fill in a short form, accept the consents, and the pass lands in their wallet. This page walks through that journey so you and your support staff know exactly what customers see. ## How customers get there | Entry point | How it works | | - | - | | **Link or QR code** | The customer opens your enrollment link in their browser, typically from a QR code in store, your website, or a receipt | | **App Clip** | On iPhone, tapping an App Clip code or NFC tag opens the same flow instantly as a lightweight native experience. No app install | | **Pre-filled link** | Links from your systems can securely pre-fill the customer's details, skipping typing and sometimes the whole form | | **Existing-member link** | Customers who already exist in your CRM can get a direct link that just issues their pass | For how to put these entry points in front of customers (emails, social posts, your website, and in-store material) see [promote your program](/promote/overview). ## The customer journey The walkthrough below follows the **Mintleaf Rewards** demo program on an iPhone. Your program shows your own branding, form fields, and consents. The steps stay the same. The customer lands on your program's welcome screen: your header image and branding at the top, a welcome like "Join \[program name]", and a short invitation text. One tap on **Continue** starts the enrollment. The enrollment welcome screen with the Mintleaf Coffee header image, the title Join Mintleaf Rewards, and a Continue button A short form collects the customer's details. Your program's configuration decides which fields appear and which are required, marked with an asterisk: name, email, phone, birth date, address, and so on. Validation happens as the customer types, and the phone field has a country-code picker. Customers arriving through a pre-filled link find these fields already completed. The personal details form with email, phone, first name, and last name fields filled in, and a Continue button When the form is submitted, Stell checks whether the email or phone number matches an existing member. If it does, the customer verifies their identity with a 6-digit code sent by email or SMS, then goes straight to adding their pass. The consents step is skipped, since they accepted when they first enrolled. New customers skip this step, and so do programs using Vipps login, where identity is already verified. New members see the consents next. Each consent is a toggle, with required ones labeled. The join button stays inactive until every required consent is given. Below the toggles, a legal note names your company as the consent processor, with links to your terms of use and privacy policy. The consents screen with two toggles, Receive Marketing emails marked required and Receive Marketing SMS optional, legal text, and an inactive Join Mintleaf Rewards button The customer is asked to add their pass: **Add to Apple Wallet** on iPhone, **Add to Google Wallet** on Android. On a computer both buttons appear side by side and the customer picks their wallet. The pass is created the moment they tap (the button shows a spinner while it's prepared) and then handed over in the way the device supports: directly into the wallet on a phone, and in most desktop browsers through a QR code the customer scans with their iPhone. See [adding the pass to a wallet](/enrollment/adding-to-wallet) for exactly what each device and browser gets. The Almost there screen telling the customer their Mintleaf Rewards pass is ready, with an Add to Apple Wallet badge Once the pass is added, the customer sees a confirmation ("Welcome to \[program name]!") with a short how-it-works guide matched to the program's pass type. If your program has follow-up links configured, like "download our app", they appear here. The Welcome to Mintleaf Rewards confirmation screen with a green checkmark and a three-step how-it-works guide ## NFC and barcode passes What the customer does at checkout depends on the program's pass type: * **NFC (tap to use).** The pass works through Apple Pay. The customer taps their phone as usual and the pass is applied automatically. * **Barcode/QR.** The customer opens their wallet and shows the barcode at checkout. The confirmation screen tells the customer which applies to them. ## What's specific to your program The flow's skeleton is the same for everyone. Your program controls the branding and texts, which form fields are asked, which consents appear, a minimum age requirement, which wallets are offered ([Apple Wallet, Google Wallet, or both](/enrollment/adding-to-wallet)), whether existing members must verify with a code, whether the pass is also sent by email or SMS, and any follow-up links on the confirmation screen. If you want any of these changed, [contact support](/support/contact). For every place a customer can join and what each channel is good for, see [loyalty enrollment channels compared](https://getstell.com/articles/enrollment-channels-compared-every-place-a-customer-can-join). # Troubleshoot wallet pass enrollment problems Source: https://docs.getstell.com/enrollment/troubleshooting A support reference for enrollment issues: form and eligibility errors, verification codes, and passes that won't add to Apple Wallet or Google Wallet. A reference for support staff: the situations customers report during [enrollment](/enrollment/how-enrollment-works), what they see, and how to resolve them. ## Form and eligibility | Customer reports | What's happening | Resolution | | - | - | - | | "It says I need to correct errors" | A field failed validation or a required field is empty | Have them check the highlighted fields. Usually a typo in the email or phone format | | "It says I'm not old enough" | The program has a minimum age and the birth date entered is below it | The age rule is the merchant's policy. If the customer believes it's wrong, check the date they entered | | "It says access is restricted" | The program is limited to a specific customer group, and their details aren't on the list | Eligibility is controlled by the merchant. Escalate to whoever owns the program's access list | | "It says onboarding is unavailable" | Enrollment is switched off for the program | Check whether this is intentional (maintenance, campaign ended), then contact Stell to re-enable | ## Verification codes (existing members) When the email or phone matches an existing member, a 6-digit verification code is required. | Customer reports | Resolution | | - | - | | "I never got the code" | Check the email or phone they entered for typos, and check spam. If the contact details in your CRM are outdated, the code goes to the old address. Update the CRM record | | "The code is invalid" | Re-enter carefully. Codes are 6 digits, and after several wrong attempts there's a short cooldown | | "The code expired" | They took too long. Have them tap **Send Verification Code** again for a fresh one | | "It says too many attempts" | Rate limiting kicked in. Wait the time shown, then request a new code | ## Getting the pass into the wallet Which buttons appear on which device, and what tapping them does, is covered in [adding the pass to a wallet](/enrollment/adding-to-wallet). | Customer reports | What's happening | Resolution | | - | - | - | | "Nothing happens after the form" / "Couldn't prepare my pass" | Pass generation failed, often a temporary hiccup reaching the merchant's customer system | Try again. If it persists, [contact Stell support](/support/contact) with the program and the time it happened | | "The pass won't add to my wallet" | Wallet add failed on the device | Update the phone's software and retry, or try another device. On a computer, scanning the QR code with an iPhone is the reliable path | | "It shows a QR code instead of adding the pass" (on a computer) | Expected: only Safari on a Mac can add the pass directly. Every other desktop browser hands the pass to the customer's iPhone by QR code | Have them point their iPhone camera at the code, and the pass opens in Apple Wallet on the phone | | "I scanned the QR code but nothing happens" / "the code stopped working" | The code carries a time-limited pass link that has expired | Close the dialog and tap **Add to Apple Wallet** again for a fresh code, then scan it right away | | "It says no compatible wallet is available" | The program doesn't support a wallet for the customer's device | Have them try from the device type suggested on screen. If the program should support that wallet, [contact Stell support](/support/contact) | | "My emailed pass link doesn't work anymore" | Pass links are short-lived and expire | Have them go through the enrollment link again. Existing members just verify and get a fresh pass | | "I never received the pass email" | The email address had a typo, or delivery failed | Verify the address. The customer can always re-run enrollment to get the pass directly | ## Checking from the portal When a customer says they enrolled but something's off, look them up under **Engage → Contacts** (search by email or external ID) and check their pass: * Pass is **Pre-Active**: the pass was issued but never added to the wallet. Walk the customer through adding it. * Pass is **Inactive**: the customer removed the pass from their wallet. They can re-add it. * No contact found: enrollment likely never completed. Have them try again. See [pass lifecycle](/passes/lifecycle) for the full status reference. If you can't resolve it, [contact Stell support](/support/contact) with the pass ID or the customer's external ID. # Add to Wallet button for your website and emails Source: https://docs.getstell.com/enrollment/wallet-button-widget Put an Add to Apple Wallet and Add to Google Wallet button on your website or in emails. Signed-in members get their pass in one tap, with no enrollment form. The **wallet button widget** is an **Add to Wallet** button you place on your own website or in your emails. When a customer who is signed in to your site (or opens your email) taps it, their pass is issued and added to **Apple Wallet** or **Google Wallet**. No enrollment form needed. It's a distribution channel alongside the [standard enrollment flow](/enrollment/how-enrollment-works), best suited for customers who already exist in your systems. If your developers would rather build the flow themselves, the API returns a permanent wallet link for every pass. See [Add to Wallet from your own site or app](/api-reference/integration/add-to-wallet). Stell provides a **configurator** that generates the code for you. You pick the options, see a live preview, and copy a ready-made snippet. ## What you need | | | | - | - | | **Program nano ID** | A short identifier for your program. Stell provides it, or a pre-filled configurator link, during onboarding. [Contact support](/support/contact) if you don't have yours. | | **Member identifier** | Each visitor's or recipient's ID from your own systems: whatever your CRM uses as the member ID, typically a UUID like `8e4a00f7-e6da-4048-9684-b42801178375`. An email address also works. Your site template or email tool fills this in per person. | ## The configurator Open the configurator at [stell.in/embed/configurator](https://stell.in/embed/configurator). It has two tabs: **On your site** for embedding the button in web pages, and **In your emails** for email templates. The configurator's On your site tab, with the configuration form, a live preview of the Apple Wallet and Google Wallet buttons, and the copyable embed snippet On the **On your site** tab you set: | Option | What it controls | | - | - | | **Program nano ID** | Which program the button issues passes for | | **Member identifier** | A test value for the preview, replaced per visitor in production | | **Wallets** | Which wallet buttons render. See [the options below](#wallet-options) | | **Locale** | The language of the button labels (defaults to the visitor's browser language) | | **Layout** | Whether multiple buttons stack horizontally or vertically | | **Base64 encode identifier** | Leave on unless Stell has told you otherwise | The **Preview** card renders the real button with your test values. Tapping it runs the same flow your visitors get. ### Wallet options | Setting | What renders on the page | | - | - | | **Default: program config** | A button for every wallet your program supports: both **Add to Apple Wallet** and **Add to Google Wallet** if your program offers both. The safe choice for most sites. | | **Apple Wallet** | Only the Apple Wallet button, on every device. | | **Google Wallet** | Only the Google Wallet button, on every device. | | **Apple + Google** | Both buttons, on every device. An Android visitor will also see the Apple Wallet button, and an iPhone visitor the Google Wallet one. | | **Auto: detect device** | Picks by the visitor's device: only the Apple Wallet button on iPhone, only the Google Wallet button on Android, and both on desktop. Use this to avoid showing a button the visitor's phone can't use. | Whatever you pick, the widget never shows a button for a wallet your program doesn't support. The selection is always filtered against your program's configuration. Further down the page you'll find a built-in integration guide with a pre-launch checklist, Content Security Policy notes, a Shopify example, and troubleshooting. On computers, the widget's buttons behave the same as in the enrollment flow: in every desktop browser except Safari on a Mac, **Add to Apple Wallet** opens a dialog with a QR code the visitor scans with their iPhone, and **Add to Google Wallet** opens Google's save page in place. See [adding the pass to a wallet](/enrollment/adding-to-wallet). ## On your site Open the configurator with your program's nano ID, enter a test member identifier, and pick wallets, locale, and layout. Check the preview. Click **Copy** on the **Drop it on any page** card. The snippet looks like this: ```html theme={null}
```
Place the snippet where the button should appear: a member page, order confirmation, or account area. Replace the `data-identifier` value with the signed-in visitor's CRM ID or email from your site template, so every visitor gets their own pass. The button must never render with a hard-coded or shared identifier.
## In your emails The **In your emails** tab generates wallet links for email templates instead of a script. Email clients don't run JavaScript, so the button is a plain image wrapped in a link. The configurator's In your emails tab, with test values and the email links card containing wallet link templates and downloadable button images 1. Paste the HTML snippet from the **Email links** card into your email template. It contains an **Add to Apple Wallet** and an **Add to Google Wallet** button, each linking to a URL template like `https://stell.in/api/pass/{nanoId}/{identifier}?wallet=apple`. 2. At send time, have your email tool replace `{nanoId}` with your program's nano ID and `{identifier}` with each recipient's CRM member ID. Their email address works too. 3. For non-English emails, swap the button image's `/en.svg` for the matching language (`/nb.svg`, `/sv.svg`, `/de.svg`, and so on). Email links deliver the pass for the wallet named in the link, with no device detection or [QR code hand-off](/enrollment/adding-to-wallet), so they work best tapped on the customer's phone. Prefer hosting images yourself? **Download all assets** on the same tab gives you every Apple Wallet and Google Wallet button image as a single ZIP. ## Before you launch * **Identifier is per-visitor.** Each signed-in visitor or email recipient must get their own identifier. Test with at least two different members. * **Test on a real phone.** Add a pass to Apple Wallet on an iPhone and Google Wallet on an Android device, not just in a desktop browser. * **Check your Content Security Policy.** If your site enforces a strict CSP, allow the Stell origins. The configurator's guide lists exactly which directives to add. * **Check localization.** If your audience isn't English-speaking, set the locale (or verify auto-detect) and use matching email button images. The full pre-launch checklist and troubleshooting guide are built into the [configurator page](https://stell.in/embed/configurator) itself, below the snippet. # Sign in to the Stell merchant portal Source: https://docs.getstell.com/getting-started/first-login Your first sign-in to the Stell merchant portal: use the temporary password from your invitation, set your own password and turn on two-factor authentication. When your company is set up on Stell, you receive an invitation email with a temporary password. Your first login takes a few minutes: you set your own password, then set up two-factor authentication. Both are required before you reach the portal. ## Sign in 1. Go to [platform.getstell.com](https://platform.getstell.com/login). 2. Enter your **Email** and the temporary password from your invitation email. 3. Click **Sign in**. ## Set your password On your first login, you're asked to replace the temporary password. 1. On the **Set New Password** screen, enter a password of at least 8 characters. 2. Click **Set New Password**. ## Set up two-factor authentication Two-factor authentication is required for every portal user. You verify your identity with a second step each time you sign in, using either an authenticator app or email codes. We recommend an authenticator app: it works offline and codes arrive instantly. Any standard app works, such as 1Password, Google Authenticator, or Microsoft Authenticator. ### With an authenticator app 1. On the **Set Up Authenticator App** screen, scan the QR code with your authenticator app. If you can't scan it, enter the **Shared Secret** key manually instead. 2. Enter the 6-digit **Verification Code** the app shows. 3. Click **Finish Setup**. ### With email codes 1. On the **Set Up Email Verification** screen, confirm your email address and click **Send Code**. 2. Enter the 6-digit **Verification Code** from the email. From now on, each sign-in asks you to **Verify Your Identity** with a code from your chosen method. You can manage your two-factor methods and passkeys later under [Account security](/account/security). ## Forgot your password? 1. On the login page, click **Forgot password?** 2. Enter your email and click **Send reset code**. 3. Enter the 6-digit **Verification Code** from the email and your new password, then click **Reset Password**. ## Next steps Once you're in, take the [portal overview tour](/getting-started/portal-overview) to get your bearings. # Stell merchant portal overview Source: https://docs.getstell.com/getting-started/portal-overview A tour of the Stell merchant portal: the dashboard, announcements, recent activity and the sidebar that takes you to programs, passes, Engage and transactions. After signing in, you land on the dashboard, your starting point for everything in the portal. Merchant portal dashboard ## The dashboard The dashboard greets you with your company name and gives you three things at a glance: * **Announcements**: recent news from Stell that may affect your programs or passes. * **Feature cards**: shortcuts into the main areas of the portal. Click **Open** on a card to jump in. * **Recent Activity**: the latest pass and transaction events across your company. Click **View all** to open the full [transaction log](/transactions/overview). ## Navigation The sidebar on the left is organized into sections: | Section | What you find there | | - | - | | **Dashboard** | The overview page you land on after signing in | | **Wallet** | [Programs](/programs/overview), [Passes](/passes/overview), and [Transactions](/transactions/overview) | | **Security** | [API Keys](/api-reference/api-keys) for integrations | | **Organization** | Your company's [merchants, stores, and terminals](/organization/overview) | | **Engage** | Customer messaging: Overview, Contacts, Segments, Push, Automations, Message templates, and Notifications | | **Help** | The Support page, with contact details for Stell support | **Engage** and **Organization** are optional modules. If you don't see them in your sidebar, they aren't enabled for your company. [Contact support](/support/contact) if you think they should be. ## Your account menu At the bottom of the sidebar, click your name to open the account menu: * **Switch company**: if you belong to more than one company, pick which one you're working in. Everything in the portal shows data for the active company only. * **Settings**: manage your [account security](/account/security), including two-factor authentication and passkeys. * **Sign Out**: end your session. ## Where to go next If your programs are already set up, head to [issuing your first pass](/passes/issuing). To understand how your pass designs work, start with [programs](/programs/overview). # Stell documentation for Apple Wallet and Google Wallet passes Source: https://docs.getstell.com/index Guides and API reference for issuing loyalty passes, memberships, tickets and access passes to Apple Wallet and Google Wallet with Stell. Stell is a pass management platform. Your company designs a pass once and issues it to customers, where it lands in Apple Wallet or Google Wallet and stays up to date on its own. ## How it works How wallet passes work: the customer enrolls, the pass is added to their wallet, it stays updated automatically, the customer taps a Terminal or shows the barcode at checkout, and the transaction is recorded A customer enrolls and the pass is added to their wallet. You keep it current from the merchant portal or from your own systems. At checkout the customer taps a Terminal or shows the barcode, and the transaction is recorded. ## Where to go next The daily work: manage programs, design pass templates, issue passes, run Engage campaigns, and investigate transactions. Get your company set up, see how customers enroll, and promote your program. Issue and update passes from your own CRM, POS, or backend, and receive event webhooks. ## Core concepts The same words are used in the portal, in these docs, and in the API. | Term | What it means | | - | - | | **Program** | The loyalty product, membership, or ticket offering you run. Everything else belongs to a program. | | **Pass Template** | The design that defines how a pass looks and which fields it carries. A program can have more than one. | | **Pass** | One pass issued to a customer from a template, living in their Apple Wallet or Google Wallet. | | **Transaction** | The record of a pass used at a Terminal, with a status of Approved, Denied, Failed, or Pending. | Your company holds one or more **merchants** (brands), each with its own **stores** and **Terminals**, the Stell tap points where customers use their passes. See [your organization](/organization/overview). ## New here? If someone invited you to the portal, start with [your first login](/getting-started/first-login). If your company is new to Stell, work through the [onboarding checklist](/onboarding/checklist). If your company is evaluating Stell, the [API reference](/api-reference/introduction) is open to read, and you can [talk to a wallet specialist](https://getstell.com/contact?context=docs:home) about your program. # Launch and grow a wallet pass program Source: https://docs.getstell.com/launch-and-grow/overview Everything around the portal: onboarding your company, how customers enroll and add your pass to their wallet, promoting your program, and getting help. The **Merchant portal** tab covers how to work in the portal. This tab covers everything around it: getting your company set up, how customers enroll, how to promote your program, and where to turn when you need help. ## What's in this tab A checklist that takes you from the invitation email to a live program your customers can join. How customers get their pass: the enrollment flow, the wallet button widget, and troubleshooting. A launch plan and ready-to-adapt examples for email, social media, your website, and your stores. Answers to common questions and how to reach Stell support. ## Where to start If your company is new to Stell, work through the [merchant onboarding checklist](/onboarding/checklist) first. It ends with a live program. From there, [how enrollment works](/enrollment/how-enrollment-works) shows what your customers will experience, and the [promotion section](/promote/overview) helps you bring them in. # Onboarding checklist: from invite to a live program Source: https://docs.getstell.com/onboarding/checklist A step-by-step checklist for new Stell merchants: secure your account, review your program and pass design, issue a test pass, try enrollment and go live. New to Stell? This checklist takes you from the invitation email to a live program your customers can join. Use the temporary password from your invitation email, set your own password, and set up two-factor authentication. Full walkthrough: [your first login](/getting-started/first-login). Take the [portal tour](/getting-started/portal-overview): the dashboard, the sidebar sections, and the account menu. If your team has several companies, check you're working in the right one. Stell sets up your program with you. Open [Programs](/programs/overview) and check the essentials: the program status, whether tap to use (NFC) matches your setup, and that the wallet certificate is in place. Open the [pass template editor](/pass-templates/overview) and make the pass yours: logo, colors, fields, and barcode. Remember that saving applies to all issued passes immediately. [Issue a pass](/passes/issuing) with your own details, add it to your wallet, and check it looks right. If your stores are live, do a test tap and find it in the [transaction log](/transactions/overview). Open your program's enrollment link or QR code and walk through it as a customer would. See [how enrollment works](/enrollment/how-enrollment-works). Check the texts, fields, and consents say what you expect. If your CRM, POS, or e-commerce platform talks to Stell, create an [API key](/api-reference/api-keys) per integration, and consider [event webhooks](/api-reference/webhooks/overview) on the program page for event notifications. Import your [contacts, create segments](/engage/contacts-and-segments), and send yourself a test [campaign](/engage/campaigns) before the first real one. Put the enrollment QR code where customers see it, brief your store staff on what a tap or scan looks like, and share [enrollment troubleshooting](/enrollment/troubleshooting) with your support team. For a full launch plan and example emails, posts, and in-store material, see [promote your program](/promote/overview). Stuck on any step? [Contact support](/support/contact). For onboarding questions, email works best. # Merchants, stores and terminals in Stell Source: https://docs.getstell.com/organization/overview See how your company is set up in Stell: the merchants in your organization, their stores, and the terminals where customers tap or scan their passes. The **Organization** section shows how your company is set up in Stell: the merchants that make up your organization, their stores, and the terminals registered at each store. Organization overview This section is **read-only**, and like Engage it's an optional module. If you don't see Organization in your sidebar, it isn't enabled for your company. Changes to merchants, stores, or terminals are made by Stell. [Contact support](/support/contact) to request them. ## How it fits together ``` Company └── Merchants: the businesses in your organization └── Stores: physical locations └── Terminals: the tap points where passes are used ``` A **terminal** is the registered tap point that produces [transactions](/transactions/overview). It may be connected to physical NFC readers or a payment terminal, but in the portal you see the registered terminal itself. ## What you can look up * **Overview**: your company details and counts of merchants, stores, and terminals. * **Merchants**: each merchant's status, location, and its stores. * **Stores**: every store across your merchants, with address and contact details, and the terminals registered there. * **Terminals**: each terminal's status, serial number, and home store. These pages are most useful when you're investigating a transaction: the [transaction detail](/transactions/overview) links straight to the store and terminal involved. # Wallet pass anatomy: areas, styles and layout Source: https://docs.getstell.com/pass-templates/anatomy The areas on the front and back of a wallet pass, the pass styles for loyalty, coupons, event tickets and boarding passes, and how each style shapes the layout. A wallet pass has a fixed set of areas, each with its own job. Once you know them, every control in the [template editor](/pass-templates/overview) maps to a spot on the pass. The areas of a wallet pass: logo and header fields at the top, strip image with the primary field, secondary and auxiliary field rows, and the barcode at the bottom ## The front of the pass From top to bottom: | Area | What goes there | | - | - | | **Logo and logo text** | Top-left corner: your logo (and optionally a short logo text next to it). This is your brand anchor, the first thing a customer recognizes | | **Header fields** | Top-right corner, opposite the logo. Always visible, even when the pass is stacked with other passes in the wallet, so use them for the single most useful value, like a points balance | | **Strip, background, or thumbnail image** | The visual heart of the pass. Depending on the pass style, this is a **strip image** (a banner across the upper part), a full **background image** (Event Ticket only), or a square **thumbnail** beside the primary field (Generic and Event Ticket). The poster layouts replace these with full-bleed **artwork** | | **Primary field** | The pass's headline value, rendered large, for example the member's name or balance. On styles with a strip image, the primary field sits on top of it | | **Secondary and auxiliary fields** | Rows of smaller values in the middle of the pass: member since, tier, store, and so on. How many fit and how they're arranged depends on the pass style (see the per-style layouts below) | | **Barcode or NFC** | The bottom of the pass: the scannable barcode, or for tap-to-use programs the NFC capability (shown with a small tap icon). See [barcodes and NFC](/pass-templates/barcodes) | Here is every area in place on a real pass, a store card open in Apple Wallet on an iPhone: The front of a store card pass open in Apple Wallet on an iPhone, with the Hold Near Reader prompt below it ## The back of the pass Customers reach the back through the **⋯** button when the pass is open in their wallet. **Back fields** hold everything that doesn't belong on the front: terms, contact details, longer descriptions, links. There's much more room here, and text can be longer. The back of a pass in Apple Wallet on an iPhone, showing wallet toggles followed by Terms & Conditions, Customer Support, and Serial Number back fields The toggles above your back fields belong to the wallet, not your template. Each customer controls them on their own device. Knowing what they do helps when a customer reports a pass that "isn't working": | Wallet control | What it does | | - | - | | **Automatic Updates** | Lets the pass receive changes from your program: new field values and design updates. If a customer turns this off, their pass keeps its old content until they turn it back on | | **Allow Notifications** | Whether this pass may show notifications on the customer's lock screen, including the messages you send to it. Turned off, the pass still updates, just silently | | **Show on Lock Screen** | Whether the pass may surface on the lock screen when the customer is near one of its [locations](/pass-templates/barcodes#also-in-the-advanced-tab). This toggle only appears when the pass template has locations set | | **Automatic Selection** | Lets the iPhone present this pass automatically where it's requested. For tap-to-use programs, this is what brings up the right pass at the Terminal without the customer hunting for it | | **Remove Pass** | Deletes the pass from this device | The **Serial Number** on the back of the pass is the same value as the **Pass ID** on the pass detail page in the portal. A customer can read it to you to confirm you're both looking at the same pass. See [passes](/passes/overview). ## Featured actions On iPhones running **iOS 27 and later**, Apple Wallet can show up to **two action tiles directly below the pass**. Each tile is a shortcut, like **View Membership Benefits** or **Call**, that opens a web page, starts a phone call, or shows a location in Apple Maps. Featured actions work on every pass style, not only the poster layouts. Set them up in the **Actions** section of the editor's **Advanced** tab: 1. Click **Add action**. 2. Choose an **Action type**. The type decides the tile's icon and text in the wallet, so you can't write your own label. 3. Enter what the action type needs: a phone number, coordinates, or a URL (see the table below). What a tile needs, and what happens when a customer taps it, depends on the action type: | Action type | What you enter | What a tap does | | - | - | - | | **Call** | A phone number | Starts a phone call to that number | | **Go to Location** | Coordinates (latitude and longitude) | Opens the location in Apple Maps | | All other types | A URL, for example a page on your website | Opens the page in the browser | The other types are View Schedule, Watch Trailer, Listen to Music, Add to Balance, Order Delivery or Pickup, Shop Online or In-App, View Membership Benefits, Book an Appointment, Book a Car, Book a Flight, Book a Stay, and View Offers and Rewards. The menu shows a category under each type, such as Membership or Events. It's only a grouping: you can use any action type on any pass style. Good to know: * **Only the first two actions show.** You can add more, but the wallet ignores everything after the second. Use the arrows to move the two that matter most to the top. * **Each action type can be used once** per pass template. * **Apple Wallet only.** Google Wallet passes and iPhones on iOS 26 and earlier don't show the tiles. The rest of the pass looks the same either way. * **Check the preview.** The editor preview shows the tiles below the pass. Click a tile to jump to its settings. * **Actions for a single pass.** An integration can give one pass its own actions through the [Stell API](/api-reference/passes/update-pass). They replace the template's actions for that pass, and the pass detail page lists them under **Template Overrides**. The editor also offers **footer fields**. These only appear on the [generic poster layout](#layout-by-style), as a final line at the very bottom of the pass. On other layouts, keep your content in the areas above. ## Pass styles The **pass style** (chosen in the **Settings** tab) decides the overall layout, most importantly which images the pass can show: The five pass styles: Store Card and Coupon use a strip image, Generic uses a thumbnail or a poster layout, Event Ticket uses a thumbnail or a poster layout, and Boarding Pass has no strip image | Style | Visual base | Typical use | | - | - | - | | **Store Card** | Strip image | Loyalty and membership cards (the most common choice) | | **Coupon** | Strip image | Offers and vouchers | | **Generic** | Thumbnail beside the primary field, or the [poster layout](#layout-by-style) with full-bleed artwork on the newest iPhones | Anything that doesn't fit the other styles | | **Event Ticket** | Strip image, or a background image with a thumbnail, or a full-poster layout with its own artwork on newer iPhones | Events and entry passes | | **Boarding Pass** | No strip or background; a slim footer image above the barcode | Travel. Also allows 5 secondary/auxiliary fields instead of 4 | Switching style keeps your fields and images but changes how they're laid out. Check the preview after switching, since an image one style shows may not appear on another. A strip image won't show on a Generic pass, for example. ## Layout by style Each style arranges the same areas differently. The two **poster** variants are the new, modern pass presentations: full-bleed artwork carries the design, and they activate automatically on newer iPhones when your template has poster artwork. Store card layout: logo and header fields at the top, strip image with the primary field on it, secondary and auxiliary fields sharing one row, barcode at the bottom The most common layout, and the one used by most loyalty programs. It has a strip image with the primary field on it, and secondary and auxiliary fields **sharing one row**: up to 4 fields in total, kept short so they fit. The Coupon style is laid out the same way; it reads as an offer, where a store card reads as a membership. Generic pass layout: a larger primary field in the top section with a square thumbnail beside it, and separate secondary and auxiliary rows The primary field renders **larger, in the top section**, with an optional square **thumbnail** beside it. The standard generic layout has no strip or background image. Unlike store cards, secondary and auxiliary fields stay on **separate rows**, up to 4 each, for a total of 8 displayed fields. The generic poster layout: primary logo and header fields at the top, full-bleed artwork, the barcode centered on the pass, primary fields at the bottom, and footer fields at the very bottom The modern take on the generic pass, on iPhones running **iOS 27 and later**. The **artwork is the pass**: it fills the whole front like a background image, which the standard generic layout doesn't support (an automatic dark gradient keeps the top readable; you can turn it off in the editor's visual options). The **barcode moves to the center**, **primary fields move to the bottom**, and **footer fields appear** as a final line. The **Primary Logo** slot replaces your regular logo at the top, falling back to it if not set. If you configure a generic poster, customers on **iOS 26 and earlier** automatically see the standard **Generic** layout instead, so keep the standard images and fields in good shape. One template serves both. Event ticket layout: notched top, event name as primary field with a square thumbnail beside it, location as secondary field, and time/level/row/seat as auxiliary fields Recognizable by the **curved notch at the top**, the classic ticket cutout. Like Generic, it can show a **thumbnail**: a square image beside the primary field, optionally over a full **background image**. Put the event name in the primary field, the venue in secondary, and time/seat details in auxiliary. A strip image replaces the background and thumbnail when used. The poster event ticket layout: primary logo and date at the top, full-bleed artwork, a barcode band, and event details and seats at the bottom The modern event ticket, on iPhones running **iOS 18 and later**. It is structured like the generic poster: full-bleed **artwork**, the **Primary Logo** at the top with the date and time opposite, the **barcode as a band** across the pass, and event details and seat information along the bottom. Event tickets also get extra poster slots in the editor: a **Secondary Logo** and a **Venue Map**. Customers on **iOS 17 and earlier** automatically see the standard **Event Ticket** layout instead. The same template serves both. Boarding pass layout: origin and destination codes rendered large as primary fields, auxiliary fields above secondary fields, QR barcode at the bottom Origin and destination render large as the primary fields. Two quirks are unique to this style: **auxiliary fields sit above secondary fields**, and rows fit up to **5 fields** instead of 4. ## How this knowledge maps to the editor * The wallet draws the pass; you supply the ingredients. You can't move the areas around; you decide what goes *in* each area. * Less is more: passes are glanced at, not read. A strong image, one primary value, and a couple of supporting fields beat a crowded layout. * Field values can be **dynamic**, showing each customer their own name, balance, or tier. See [fields and dynamic content](/pass-templates/fields). For design advice beyond the mechanics, read [what makes a pass worth keeping](https://getstell.com/articles/designing-a-wallet-pass-people-keep) on getstell.com. Want to go deeper? Apple's developer documentation covers the underlying pass format in technical detail. See [Wallet Passes](https://developer.apple.com/documentation/walletpasses) on the Apple Developer site. You won't need it to use the editor; everything merchant-relevant is covered in these pages. # Apple Wallet vs Google Wallet passes Source: https://docs.getstell.com/pass-templates/apple-vs-google How one Stell pass template renders in Apple Wallet and Google Wallet, what carries over and what doesn't, and how to design a pass that works on both. You maintain **one template per program**, and Stell renders it for both wallets. Apple Wallet follows your template closely, and it's what the editor preview shows. Google Wallet uses a more standardized card layout, so the same template comes out simpler there. Knowing the differences saves surprises. ## What carries over to Google Wallet | Template setting | On Google Wallet | | - | - | | Organization name | Shown as the issuer and program name | | Background color | The card's background color | | Logo text | The account name label | | Primary, secondary, and auxiliary fields | Up to **3 rows of 3 fields** on the card front, in that priority order | | Header and back fields | In the **details section** below the card, not on the front | | Barcode | Rendered on the card | | NFC (tap to use) | Becomes Google **Smart Tap** | ## What doesn't carry over * **Strip image**: Google Wallet has no strip-image concept; the card relies on your background color and logo. * **Pass style**: Store Card, Coupon, and the other styles are Apple layouts. On Google, every program uses the standard card layout. * **Background image and poster artwork**: not used on the Google card. * **Notification icon**: Apple-specific. ## What this means in practice * **Don't put critical information only in an image.** A points balance baked into the strip image is invisible on Google Wallet. Keep values in [fields](/pass-templates/fields), which render on both. * **Pick a background color that stands alone.** On Apple the strip image dominates; on Google the background color *is* the design. * **Mind the front-field budget.** Google shows at most 3 rows × 3 fields on the front; anything beyond that moves to the details section. Your most important fields should be in the primary and secondary areas so they win the front-row spots on both wallets. * **Test on both.** After significant template changes, [issue a test pass](/passes/issuing) for each wallet type and look at them side by side. The [customer enrollment flow](/enrollment/how-enrollment-works) issues passes for both wallets, matched to the customer's device. Passes can also be issued from the portal or [via the API](/api-reference/integration/add-to-wallet). For a side-by-side of the two wallets beyond pass design, see the [wallet comparison](https://getstell.com/platform/wallets) on getstell.com. # Wallet pass barcodes, QR codes and NFC Source: https://docs.getstell.com/pass-templates/barcodes How a wallet pass is read at checkout: barcode and QR code formats, NFC tap, and what to put in the barcode so your POS can look up the member. The bottom of the pass is where it gets *used*: either a **barcode** the store scans, or **NFC** so the customer just taps their phone. Configure both in the **Advanced** tab of the [template editor](/pass-templates/overview). ## Barcode or NFC? * **NFC (tap to use)**: the pass works through a tap, like a payment card. Whether NFC is available depends on your program's setup (it requires NFC-capable terminals and certificates); when active, the pass shows a small tap icon instead of a barcode. * **Barcode**: works with any scanner your stores already have. This is the default for most programs. A pass shows one or the other, not both. Here is the same pass both ways, in Apple Wallet on an iPhone: A pass with NFC enabled in Apple Wallet, showing a tap icon on the pass and the Hold Near Reader prompt below it The same pass with a QR code at the bottom instead of the NFC tap icon ## Barcode formats A template can hold up to 3 barcodes. Supported formats: **QR**, **PDF417**, **Aztec**, **Code 128**, **Code 39**, **Codabar**, **EAN-13**, and **Interleaved 2 of 5**. Pick the format your POS scanners actually read. When in doubt, **QR** is the safest modern choice, and your POS supplier can confirm. Each barcode also takes an optional **alt text**, shown under the barcode so staff can type the code manually if scanning fails. ## Dynamic barcode content The barcode **message** (what the scanner actually reads) supports the same `{{…}}` placeholders as [fields](/pass-templates/fields). This is how the barcode identifies the individual customer: | Goal | Message | | - | - | | Scan resolves to your CRM member number | `{{customer.externalId}}` | | Scan resolves to the customer's phone number | `{{customer.mobileNumber.national}}` | | Scan resolves to the Stell pass | `{{passId}}` (the default when no message is set) | Combinations work too, for example `{{customer.externalId}}-{{passId}}`. Whatever your POS looks up members by, that's what belongs in the barcode message. ## Encoding The default encoding (ISO 8859-1) is right for almost everyone and what scanners expect. Switch to UTF-8 only if your barcode content needs characters outside Western alphabets, and confirm your scanners handle it. ## Also in the Advanced tab * **Locations**: pin your stores' coordinates and the pass surfaces on the customer's lock screen when they're nearby. A relevance radius controls how close is "nearby". * **Sharing**: prohibit sharing of the pass, or set a custom share URL and text. * **Pass actions**: event-guide links for event tickets, such as parking, directions, and merchandise, plus venue contacts. * **Actions**: up to two shortcut tiles below the pass on iPhones running iOS 27 and later. See [featured actions](/pass-templates/anatomy#featured-actions). ## Reading passes at your POS For how your POS reads the barcode or the NFC tap and looks up the member, see [identify pass holders at the POS](/api-reference/integration/in-store) in the API guide. To choose between the two, read [QR, NFC, or both](https://getstell.com/articles/qr-nfc-or-both-choosing-how-customers-identify), and check which terminals and readers support NFC passes in the [reader directory](https://getstell.com/readers). # Wallet pass fields and personalized content Source: https://docs.getstell.com/pass-templates/fields Add text fields to a wallet pass and personalize them with placeholders, so every pass shows its holder's own name, points balance, tier or dates. **Fields** are the text on your pass: labels and values placed in the areas described in [anatomy of a pass](/pass-templates/anatomy). Their real power is **placeholders**: values that resolve per customer, so every pass shows its holder's own name, balance, or tier. ## Editing fields The Content tab In the **Content** tab, fields are grouped by area: **Header**, **Primary**, **Secondary**, **Auxiliary**, and **Back** fields. For each field you set: * **Label**: the small heading (for example "POINTS"). Optional. * **Value**: the text shown. Type static text, insert placeholders with the placeholder picker, or mix both: `Points: {{loyaltyState.pointsBalance}}`. * **Change message**: an optional notification text shown when this field's value changes, like "Points changed to %@". This is how customers get a lock-screen ping when their balance updates. You can drag fields to reorder them, move a field to a different area, and add or remove fields. Secondary and auxiliary areas hold up to 4 fields each (5 on the Boarding Pass style); keep the front sparse and put the rest on the back. ## Placeholders Placeholders are `{{…}}` tokens replaced per customer when the pass is rendered. The most useful ones: | Show | Placeholder | | - | - | | Customer's name | `{{customer.displayName}}`, or `{{customer.firstName}}` / `{{customer.lastName}}` | | Points balance | `{{loyaltyState.pointsBalance}}` | | Stamps collected | `{{loyaltyState.stampsCollected}}` | | Membership tier | `{{loyaltyState.memberTier}}` | | Your customer ID | `{{customer.externalId}}` | | Program name | `{{programName}}` | | Pass expiry date | `{{expirationDate}}` | | Visits used (entry passes) | `{{entryState.visitsUsed}}` | The placeholder picker in the field editor shows the full catalog, organized by category. A placeholder with no value for a given customer simply renders as empty. Placeholders insert values; they don't calculate. `{{loyaltyState.pointsBalance}}` works; `{{loyaltyState.pointsBalance}} + 10` shows literally that text. ## Dates Also in the **Content** tab: * **Expiration date**: when the pass should stop being valid. * **Relevant dates**: moments when the pass should surface on the customer's lock screen, useful for events. ## A sensible field layout For a typical loyalty card: * **Header**: points balance, visible even when the pass is stacked in the wallet. * **Primary**: the customer's name, large on the strip. * **Secondary**: tier and member-since. * **Back**: terms, support contact, and links. Check the live preview as you go. It renders placeholders so you can judge real proportions. # Wallet pass image sizes and dimensions Source: https://docs.getstell.com/pass-templates/images Every image slot on a wallet pass with its recommended dimensions: logo, strip, background, icon, thumbnail, footer and poster artwork, and when each is shown. Images do most of the visual work on a pass. Each slot has a fixed place and recommended dimensions, and using the right size keeps the pass crisp on every device. Upload and replace images in the **Design** tab of the [template editor](/pass-templates/overview), or click the image directly in the preview. The Design tab The Design tab also holds the pass **colors** (background, foreground for value text, label, strip, and footer background), plus visual options like automatic colors derived from your artwork. ## Standard images | Image | Dimensions | Where it appears | | - | - | - | | **Logo** | up to 160 × 50 px | Top-left corner of the pass front. Aspect ratio is preserved | | **Strip Image** | 375 × 98 px | The banner across the upper front, on Store Card, Coupon, and Event Ticket styles. The primary field renders on top of it | | **Pre-install Strip Image** | 375 × 98 px | Replaces the strip image while the pass isn't installed on any device yet. This is useful for an arrow pointing at the **Add to Apple Wallet** button. Once the customer adds the pass, the regular strip takes over | | **Background** | 375 × 530 px | Fills the entire pass, behind the fields. Event Ticket style only. On Generic passes, full-bleed imagery comes from the poster **Artwork** instead | | **Icon** | 29 × 29 px | Next to push notifications from this pass on the lock screen, not on the pass itself | | **Thumbnail** | 90 × 90 px | A square image beside the primary field, on Generic and Event Ticket styles | | **Footer** | 286 × 15 px | A slim image just above the barcode. Boarding Pass style only | The **Pre-install Strip Image** is an easy win for enrollment conversion: customers who see the pass before adding it get a visual nudge toward the add-to-wallet button. ## Poster images Newer iPhones can show Event Ticket passes (iOS 18 and later) and Generic passes (iOS 27 and later) in a richer **poster layout**. See the poster tabs under [layout by style](/pass-templates/anatomy#layout-by-style) for how they rearrange the pass. On older iOS versions the same template falls back to the style's standard layout. These slots appear for those styles: | Image | Dimensions | Role | | - | - | - | | **Artwork** | 358 × 448 pt | The poster's main visual. Uploading it is what enables the poster layout | | **Primary Logo** | 126 × 30 pt | Branding on the poster, falling back to your regular Logo if not set | | **Secondary Logo** | 135 × 12 pt | Smaller secondary branding on the poster | | **Venue Map** | original size | A seating or venue map for events | On older devices the pass falls back to the standard layout, so keep the regular images in good shape too. ## Practical tips * Upload PNG or JPG. For photos use JPG; for logos and graphics with transparency use PNG. * Design for the exact pixel sizes above at 2× or 3× resolution if you have it, since the wallet scales down better than up. * The strip image sits **behind** the primary field text. Keep the area where the text lands calm and high-contrast, or the value becomes unreadable. * Remember that which images show depends on the pass style. See [anatomy of a pass](/pass-templates/anatomy). A background image uploaded on a Store Card template won't appear. * Google Wallet doesn't use the strip image. See [Apple Wallet vs Google Wallet](/pass-templates/apple-vs-google). # Design wallet passes in the template editor Source: https://docs.getstell.com/pass-templates/overview Design how your passes look in Apple Wallet and Google Wallet with the Stell template editor: colors, logo, images, fields and barcodes, with a live preview. Every program has a **pass template**: the design and content that all of its passes inherit: colors, logo, images, text fields, and barcodes. The template editor lets you fine-tune it with a live preview. Pass template editor To open the editor, go to your program and click **Edit template** in the **Wallet card template** section. Before diving into the editor, it pays to understand what you're editing. See the [anatomy of a pass](/pass-templates/anatomy) for how the areas of a pass fit together. ## How the editor works The left side shows a live preview of your pass, front and back. The right side holds the controls, organized in tabs: | Tab | What you edit | Details | | - | - | - | | **Settings** | Organization name, description, and pass style | The pass style sets the overall layout. See [anatomy](/pass-templates/anatomy) | | **Design** | Colors, visual options, and images | See [images](/pass-templates/images) for every slot and its dimensions | | **Content** | Logo text, the pass fields, and dates | See [fields and dynamic content](/pass-templates/fields) | | **Advanced** | Barcodes, locations, sharing, pass actions, and featured actions | See [barcodes and NFC](/pass-templates/barcodes) and [featured actions](/pass-templates/anatomy#featured-actions) | | **Localization** | Translations for each language you support | Organization name, description, logo text, fields, and barcode alt text per language | | **Semantic** | Structured data the wallet can use | Loyalty, event, or transit details, depending on the pass style | The fastest way to find the right control is to click the pass itself: click the background to open colors, click an image to open that image's upload, click a field to open its editor. ## Saving changes 1. Make your edits. The preview updates as you go. A colored dot next to the template name warns you about validation issues. 2. Click **Save**. The save dialog summarizes your changes and lists any errors (which block saving) or warnings (which don't). 3. Click **Confirm Save**. Saving applies your changes to **all existing passes immediately**: every customer's pass updates to the new design, not just passes issued from now on. Click **Discard** to throw away unsaved changes. If you navigate away with unsaved changes, your browser asks you to confirm first. ## From preview to wallet The preview is faithful: what you see in the editor is what customers get on their phone. The same template shown in the editor screenshot above renders like this in Apple Wallet: The pass designed in the template editor, open in Apple Wallet on an iPhone ## Good to know * The template belongs to its program. You can't move it to another program from the portal. * The preview shows the Apple Wallet layout. Google Wallet renders the same template differently. See [Apple Wallet vs Google Wallet](/pass-templates/apple-vs-google). * Stell creates an initial working template for your program, so you're always improving a working pass rather than starting from a blank canvas. # Issue a wallet pass from the portal Source: https://docs.getstell.com/passes/issuing Issue an Apple Wallet or Google Wallet pass to a customer from the Stell merchant portal, with optional customer details and an external ID from your CRM. Issuing a pass creates the pass record for a customer. How the customer receives it (for example through an enrollment link or your own systems) happens after this step. Issue pass dialog ## Issue a pass 1. Open **Passes** in the sidebar. 2. Click **Issue pass**. 3. Choose the **Program** the pass belongs to. If your company has only one program, it's selected for you. 4. Choose the **Wallet**: **Apple Wallet** (iPhone and Apple Watch) or **Google Wallet** (Android devices). If the program supports only one wallet, it's selected for you. 5. Fill in what you know about the customer. Every field is optional: * **First Name** and **Last Name** * **Email** * **Mobile Number** * **External ID**: the customer's ID from your CRM or source system, useful for matching the pass to your own records later. 6. Click **Issue pass**. The new pass appears in the passes list, and you can open it to follow its status, send notifications, or share it with the customer. The **Issue pass** button only appears when your company has at least one program that can issue passes. If you don't see it, check your [programs](/programs/overview) or [contact support](/support/contact). ## Tips * Customer details can be added when you have them. A pass doesn't need a name or email to work. * Use **External ID** consistently if you connect Stell to your CRM or POS, so passes and customers match up across systems. * To issue passes automatically from your own systems instead, see the [API integration guide](/api-reference/integration/overview). # Wallet pass statuses and lifecycle Source: https://docs.getstell.com/passes/lifecycle The statuses a wallet pass moves through from issue to expiry, what the pass detail page shows, and how to void a pass that should stop working. Every pass moves through a lifecycle from the moment you issue it. The pass detail page shows where a pass is in that lifecycle and everything that has happened to it. Pass detail page ## Pass statuses The status badge next to the cardholder's name shows the pass's current state: | Status | Meaning | | - | - | | **Pre-Active** | Issued, but the customer hasn't added it to a wallet yet | | **Active** | Added to a wallet and working at your stores | | **Inactive** | The customer removed it from their wallet. It becomes active again if they re-add it | | **Voided** | You cancelled the pass. It no longer works, but you can still view its history | | **Revoked** | The pass was withdrawn by the issuer and no longer works | | **Expired** | The pass reached the end of its validity | Pre-Active, Active, and Inactive shift automatically as the customer adds or removes the pass from their wallet. Voided, revoked, and expired are final: a pass in one of these statuses can't return to active. ## The pass detail page Open any pass from the [passes list](/passes/overview) to see: * **Pass and customer info**: the pass ID, wallet, dates, and the customer details provided at issue time. * **Live preview**: how the pass currently looks in the customer's wallet, based on your [pass template](/pass-templates/overview). * **Status timeline**: every status change the pass has been through, in order. * **Wallet installations**: the devices where the pass has been added to a wallet. This tells you whether the customer actually has the pass on their phone. * **Transactions**: every time this pass was used at one of your stores, linking to the full [transaction detail](/transactions/overview). * **Send a notification**: push a message to the wallets where this pass is installed. ## Void a pass Void a pass when it should stop working, for example if it was issued by mistake or the customer's membership ended. Voiding is permanent. A voided pass stops working and can't be reactivated. To give the customer a working pass again, issue a new one. 1. Open the pass from the **Passes** list. 2. Click **Void pass**. 3. In the confirmation dialog, click **Void pass** to confirm. The pass status changes to voided. You can still open the pass to view its history, transactions, and wallet installations. The **Void pass** button is only shown for passes that can still be voided. It's hidden once a pass is already voided, revoked, or expired. ## Follow status changes in your own systems Set up [event webhooks](/api-reference/webhooks/overview) on the program to send status changes to your own systems as they happen, for example when a customer adds the pass to a wallet. For why the days after issue matter most, read [what happens to a wallet pass after you issue it](https://getstell.com/articles/pass-lifecycle-management-explained) on getstell.com. # Find and manage issued wallet passes Source: https://docs.getstell.com/passes/overview Browse, search and filter every pass issued for your programs in the Stell portal, and open any pass to see its status, wallet and history. A **pass** is the card, ticket, or credential a customer holds in their Apple Wallet or Google Wallet. The **Passes** page lists every pass issued for your company's programs, so you can see who's using them and look up any individual pass. Passes list ## The passes list Open **Passes** in the sidebar. Each row shows: | Column | Meaning | | - | - | | **ID** | The pass identifier (shortened; hover to see the full ID) | | **Status** | The pass's current lifecycle status, like active or voided | | **Wallet** | Whether it's an Apple Wallet or Google Wallet pass | | **Cardholder** | The customer's name, when provided | | **Program** | The program the pass belongs to. Click it to open the program | | **Created** | When the pass was issued | Click any row to open the [pass detail page](/passes/lifecycle), where you can follow the pass's status history, wallet installations, and transactions. If your company hasn't issued any passes yet, the list is empty. Start with [issuing a pass](/passes/issuing). ## What you can do from here * **Issue pass**: create a new pass for a customer. See [issue a pass](/passes/issuing). * **Open a pass**: click a row to inspect a single pass, send it a notification, or [void it](/passes/lifecycle#void-a-pass). # Wallet pass programs in Stell Source: https://docs.getstell.com/programs/overview A program defines a wallet pass your customers can add, such as a loyalty pass or an entry pass, with its design and behavior. See and open your programs. A **program** is the foundation everything else builds on: it defines a pass your customers can add to their wallet (for example a loyalty card or an entry pass), together with how that pass looks and behaves. Programs list ## Your programs Open **Programs** in the sidebar to see your company's programs. Each card shows the program's name, its type (**Loyalty** or **Entry**), and whether its pass template is set up. Click a card to open the program. Programs are set up for you by Stell. If a program is missing or you need a new one, [contact support](/support/contact). ## The program page Each program page shows the program's status and settings, its pass template and signing certificate, the passes issued under it, and its [event webhooks](/api-reference/webhooks/overview) in the API reference. See [the program page](/programs/the-program-page) for a walkthrough of every section. ## Next steps * [The program page](/programs/the-program-page): what you see and can do when you open a program. * [Pass templates](/pass-templates/overview): the most common thing you'll do here is adjust how your pass looks. # Manage a wallet pass program Source: https://docs.getstell.com/programs/the-program-page What the program page in the Stell portal shows and lets you do: program status, the pass template, the wallet certificate, issued passes and event webhooks. Click a program card on the [Programs list](/programs/overview) to open it. The header shows the program's name and its type (**Loyalty** or **Entry**), and below it a card for each part of the program's setup. Program page ## Overview The first card sums up the program: | Field | Meaning | | - | - | | **Status** | **Published**, **Draft**, or **Archived** | | **Subtype** | How the program works. For loyalty: **Points**, **Stamps**, **Tiers**, or **Membership**; for entry: **Visit-based**, **Time-based**, or **One-time** | | **Managed by** | Who set up and maintains the program, usually **Stell** | | **Tap to use (NFC)** | Whether passes can be used by tapping a terminal | | **Country** | The market the program operates in | | **Created / Updated** | When the program was set up and last changed | A description is shown below the fields when one is set. ## Wallet card template How this program's pass looks in your customers' wallets, shown with the pass logo and your organization name. Click **Edit template** to open the [pass template editor](/pass-templates/overview), where you can adjust colours, the logo, and the details your customers see. If the template hasn't been set up yet, the card says so. Once it's in place, this is where you fine-tune the look. ## Wallet certificate The Apple certificate that signs this program's passes. You'll see the Pass Type Identifier, Team Identifier, who manages the certificate (**Stell** or **Your account**), its expiry date with a **Valid**, **Expiring soon**, or **Expired** badge, and whether it's NFC capable. * **Stell-managed**: renewal is handled for you; there's nothing to do here. * **Managed by your account**: click **Replace certificate** to upload a new `.p12` file and passphrase before the current one expires. If the card says **No signing certificate**, passes for this program can't be issued until one is added. Click **Upload certificate** to add your own, or [contact support](/support/contact) to have Stell provision one. ## Program passes Every pass issued under this program, newest first, with the same details as the main [passes list](/passes/overview). Click a row to open the [pass detail page](/passes/lifecycle). ## Event webhooks Send this program's pass events (passes being added, used, or changed) to your own HTTPS endpoints. Useful when your systems should react to pass activity. Click **Configure** to subscribe your first endpoint, or **Manage** to edit existing subscriptions; the card shows how many are active. See [event webhooks](/api-reference/webhooks/overview) in the API reference for the full setup guide. ## Sections you may also see Depending on how the program is set up, extra cards can appear: * **NFC terminal encryption keys**: shown when tap to use (NFC) is enabled. Holds the keys you need when configuring NFC terminal providers: a **Private key**, an Apple (compressed) and a Google (uncompressed) **public key**, and an **Adyen key**. Each is hidden until you click reveal, and can be copied from there. * **iCloud Account Binding**: when enabled, passes lock to the customer's iCloud account as they're added to Apple Wallet, so a pass can't be shared between accounts. Requires NFC to be enabled. The private and Adyen NFC keys can decrypt pass payloads. Reveal them only when you need them, and share them only with trusted terminal providers. # Program launch emails with Add to Wallet buttons Source: https://docs.getstell.com/promote/email Announce your pass program by email: an annotated example, emails to model yours on, and one-tap wallet links that issue the pass with no form to fill in. Email is the strongest launch channel. You already know who to send to, and recipients who exist in your CRM can get their pass with **one tap**: the email wallet links issue the pass directly, no enrollment form. Everyone else clicks through to the enrollment flow instead. Either way, the email's only job is to make adding the pass feel as easy as it is. ## Anatomy of a program email A good program email is short and has exactly one call to action. The Mintleaf Rewards announcement below is annotated top to bottom: Annotated Mintleaf Rewards announcement email, with callouts for the subject line, hero image, value proposition, Add to Apple Wallet and Add to Google Wallet buttons, fallback link, and footer 1. **Subject line.** Lead with the benefit, not the technology. "Your coffee now earns you free coffee" beats "Introducing our new digital wallet pass solution". 2. **Preview text.** The line shown next to the subject in the inbox. Use it to remove friction: "No app to install. It takes one tap." 3. **Hero.** Your brand and the pass itself. Showing the pass on a phone tells the whole story in one image. 4. **Value proposition.** One or two sentences on what the customer gets and why it's effortless. 5. **The wallet buttons.** The single call to action: **Add to Apple Wallet** and **Add to Google Wallet** buttons, side by side. Nothing else in the email should compete with them. 6. **Fallback text link.** A plain link under the buttons ("Button not working? Use this link") for email clients that block images. 7. **Footer.** Your usual sender details and unsubscribe link. If you market in the United States, the wallet providers expect their trademark attributions here. See [using the wallet badges](/promote/overview#using-the-wallet-badges). ## Example emails Brands lead differently: some with a full campaign production, some with a personal note, some with the product itself. The three Mintleaf examples below show the range. Pick the style closest to your brand and model your email on it. The structure never changes: one message, one call to action. **Campaign style**, for your full mailing list at launch. A branded hero, the value proposition, and one button. Recipients may or may not be in your CRM, so the call to action links to the enrollment flow. **Subject:** Your coffee now earns you free coffee. Meet Mintleaf Rewards
**Preview text:** No app, no plastic card. It takes 30 seconds. The Mintleaf Rewards announcement email: dark green hero with the Mintleaf wordmark, a headline about the loyalty card moving into the phone's wallet, and a Join Mintleaf Rewards button
**Minimal style**, for customers already in your CRM. No hero, no production: a short personal note and the wallet buttons. Reads like a message, not a campaign, and the buttons issue the pass directly, no form, because the recipient is already known. **Subject:** Your Mintleaf Rewards pass is ready, Anna
**Preview text:** One tap and it's in your wallet. We've already filled everything in. The minimal Mintleaf Rewards invitation email: a small logo, a short personal note to Anna, Add to Apple Wallet and Add to Google Wallet buttons, and a fallback link
**Visual style.** The pass itself is the hero, so the customer sees exactly what lands in their wallet before they tap. The strongest version of the one-tap invitation when your pass design is worth showing off. **Subject:** This is your new loyalty card, Anna
**Preview text:** It's already made. One tap and it's in your wallet. The Mintleaf Rewards pass showcase email: the pass rendered large, straddling the dark green hero and the white body, with Add to Apple Wallet and Add to Google Wallet buttons below
## Making the buttons work The wallet buttons in these emails come from Stell's configurator. It generates an email-safe HTML snippet with both buttons, official badge images in your language, and per-recipient links your email tool fills in at send time. The setup (snippet, identifiers, localized images) is covered on the [wallet button widget page](/enrollment/wallet-button-widget#in-your-emails). Don't hand-build the links. If a recipient isn't in your CRM, say a newsletter subscriber who never bought anything, the one-tap links can't identify them. Send those recipients to your enrollment link instead, where the [enrollment flow](/enrollment/how-enrollment-works) collects their details. ## Before you send * **One call to action.** If the email also promotes this month's blend, the pass loses. Give the program its own send. * **Design for the phone.** The wallet add happens on a phone, so most recipients will read the email there. One column, big buttons. * **Segment members from non-members.** Members get the one-tap invitation, everyone else gets the announcement with the enrollment link. A "fill in this form" email to someone you already know feels careless. * **Test on real devices.** Send the email to yourself and a colleague, and add the pass on a real iPhone and a real Android phone, not just a desktop preview. Test with two different member identifiers, not one. The most common launch bug is every recipient getting a link to the *same* pass, and two test recipients catch it immediately. Email reaches customers who *don't* have the pass yet. Once they've added it, [Engage](/engage/overview) push notifications reach them directly on the lock screen, with no inbox to fight through. If you don't see Engage in your navigation, it isn't enabled for your company. [Contact support](/support/contact). # In-store posters and QR codes for your pass program Source: https://docs.getstell.com/promote/in-store Posters, stands, banners, counter cards and screens with QR codes that get customers to add your pass at the counter, plus what your staff should say. The store is where your program sells itself. The customer is standing at the counter, about to pay, and the benefit is seconds away. In-store material has one job, to bridge from the physical space to the customer's phone, and the bridge is almost always a **QR code** pointing at your enrollment link. Scanning it opens the [enrollment flow](/enrollment/how-enrollment-works), and the pass lands in **Apple Wallet** or **Google Wallet** before the coffee is ready. Use the enrollment QR code you received during onboarding. [Contact support](/support/contact) if you don't have it. The formats below run from a one-line counter card to a ceiling banner visible across the store. Whatever the format, the discipline is the same: one headline, one or two lines of benefit, the QR code, and a scanning instruction. A piece that needs a paragraph isn't working. ## Counter and wall | Format | The moment | Mintleaf example | | - | - | - | | **Poster** (A3/A2, wall or window) | Seen from across the room. Make it a billboard, not a brochure | "Tap. Sip. Earn. Mintleaf Rewards lives in your phone's wallet. Scan to join." | | **Counter card / table tent** | The queue and the table, with time to read and phone in hand | "Join before you pay. Scan to add Mintleaf Rewards. It takes 30 seconds, and this coffee already counts." | | **Register-topper** | Mounted on the terminal itself, at eye level while the customer pays | "Earn · Enjoy · Join today" with a QR code. Small, but it's the one sign the paying customer looks straight at | | **Receipt footer** | Already in the customer's hand. One line and a QR code, if your printer supports it | "Missing out on free coffee? Scan to join Mintleaf Rewards." | | **Window sticker** | Signals the program before the customer walks in | The badge plus "Mintleaf Rewards accepted here" | Mintleaf Rewards A3 poster mockup with the headline Tap. Sip. Earn., a phone showing the pass, a QR code with a scanning instruction, and the Add to Apple Wallet badge Mintleaf Rewards counter card at the point of sale with a QR code and the headline Join before you pay A Mintleaf Rewards register-topper card mounted on top of a point-of-sale terminal, with Earn and Enjoy columns, a central QR code, and a Join today button, above a screen showing the order total ## Stands and banners Counter material catches customers who are already paying. Stands and banners catch the ones still walking around: in the queue, between the shelves, browsing. * **Floor stand or easel.** Placed in the walkway where customers queue or browse. The strongest version is radically simple: a giant QR code and a single line. No headline contest, no paragraph. The code *is* the message, and it scans from several steps away because of its size. Put your logo in the center of the code if you like, and keep the line direct: "Scan the QR code to join Mintleaf Rewards." * **Hanging banner.** Suspended from the ceiling, visible across the whole store and above the shelf lines. It works at two distances: the headline and brand read from across the room, and the QR code takes over once the customer is close. Hang it where people naturally stop, above the queue, the till, or a central aisle, and low enough that the code is scannable without stretching. * **Illuminated totem (lightbox).** A slim, backlit floor column. Same single-line-and-QR discipline as the floor stand, but it glows: it holds its own in a dim corner, a shop window, or after dark, and reads as a permanent fixture rather than a poster. Place it where people pause, and keep the QR code large. * **Outdoor A-frame.** A pavement sign on the sidewalk, catching customers before they're even inside. Keep it to one headline and a QR code big enough to scan from a stride away, and check your local rules on what you're allowed to place on the pavement. A Mintleaf Rewards floor easel in a store walkway, holding a dark green board with a giant QR code and the line Scan the QR code to join Mintleaf Rewards A Mintleaf Rewards banner hanging from the ceiling on two wires, with the headline Every cup counts, a QR code, and a scanning instruction A tall, slim illuminated Mintleaf Rewards lightbox totem in an aluminium frame, lighting the floor around it, with the words Earn Redeem Enjoy, a glowing QR code, and a list of member perks A Mintleaf Rewards A-frame pavement sign on a sidewalk outside the café, under a green Mintleaf Coffee awning, with the headline Rewards, on your phone and a large QR code Bigger codes scan from further away. For stands and banners, make the QR code far larger than feels natural on a design canvas. It should scan comfortably from where customers actually stand, not from where the designer sits. ## Print rules for the badge and QR Print is the one channel with its own badge rules. A printed badge can't be tapped, so it works differently: * **Use the print-quality artwork.** Badges in print must come from the official EPS (vector) files, never from a screenshot or a web image scaled up. * **Pair the badge with the QR code.** The badge tells the customer what they'll get, and the QR code is how they get it. Apple's print guidance pairs them with a scanning instruction, for example: *"Scan the code to add your pass. Just open the Camera app on your iPhone and point it at the code."* Android phones scan the same code from their camera too. * **Give the badge room.** Same clear-space rule as everywhere else. See [using the wallet badges](/promote/overview#using-the-wallet-badges). * **Test-scan the proof.** Before the print run, scan the actual printed proof, not the PDF on your screen, from a realistic distance, with both an iPhone and an Android phone. For stands and banners, test from where customers will actually stand. A QR code that's too small, too low-contrast, or printed on glossy curved material fails quietly. Never stretch, recolor, or screenshot a wallet badge for print. If the file you have isn't an official vector file, get the right one before going to print. See [using the wallet badges](/promote/overview#using-the-wallet-badges). ## Digital screens Menu boards and customer-facing displays reach every customer in the store, and they can show more than print: imagery, the pass itself, even motion. Three patterns work: * **A dedicated promo slide.** Image-led: the pass on a phone, one headline, the QR code with a scanning instruction. Rotate it into your menu loop, or run it full-time on a second screen near the queue. * **A permanent corner.** If the screen is primarily a menu, give the program a fixed panel or corner on every slide instead of its own slide, so the QR code never disappears while a customer is reaching for their phone. * **A self-order kiosk.** If customers order at a kiosk, the screen is already interactive and right in front of them. Put a **check-in** prompt on the attract loop, and a "join after you order" step with a QR code at the end. The customer is looking straight at it, phone often already in hand. A digital screen slide with the headline Your tenth coffee is free, a phone showing the Mintleaf Rewards pass, and a QR code with a scanning instruction A digital menu board with coffee prices on the left and a permanent Scan to join Mintleaf Rewards panel with a large QR code on the right A self-order kiosk screen with the heading Members earn free coffee, a Tap here to check in button, a featured Maple Oat Latte, and a Join Mintleaf Rewards QR code A self-order kiosk screen showing the Mintleaf Rewards pass on a phone with the headline Rewards are ahead and a prompt to tap check in at the top Two rules carry over from print, sharpened for screens: * **Timing.** If the promo is a rotating slide, leave it up 10 to 15 seconds. Spotting the code, raising the phone, and scanning takes longer than reading. The permanent-corner pattern avoids the problem entirely. * **Size and distance.** Customers scan screens from further away than posters, from behind a counter or above a till. Make the code generously large and test-scan from the customer's side of the counter. ## A tap-to-check-in point Almost everything above bridges a *new* customer to their phone with a QR code. A tap point is the other end of the journey, for customers who already carry the pass. A mat or decal placed around a Stell **Terminal** turns the tap point into a check-in: the member taps their phone, the same tap they use to pay, and the visit is logged automatically. No scan, no app, nothing to type. It earns its spot at the counter precisely because it isn't a QR code. Returning members get a one-second ritual, and the artwork doubles as a reminder that the program is worth tapping in for. * Keep the copy to the action and the payoff: "Tap to check in. Visit 10 times and your next coffee is on us." * It only works where your program is **tap-based** and a Terminal is present. If your program uses a barcode instead, this piece doesn't apply. Lead with the QR formats above. * New customers still need the QR code to join first, so pair the tap point with a nearby counter card or register-topper. A flat Mintleaf Rewards tap-to-check-in mat on a counter, with a contactless tap zone, the headline Tap to check in, a visit-streak indicator, and the note visit ten times for a free coffee ## What your staff should say Material gets attention. Staff close the deal. One practiced line at the till is worth a wall of posters: > "Do you have our rewards pass? It's free, it lives in your phone's wallet, and you're in before your coffee's ready. Want me to show you?" Then point at the counter card. The customer scans, and the staff member watches one enrollment happen. After that they can answer anything. The questions that come up, and the answers: No. The pass lives in the wallet your phone already has: Apple Wallet on iPhone, Google Wallet on Android. Scan the code, fill in a few details, tap **Add**, done. Yes. The same QR code works for everyone. iPhones get the pass in Apple Wallet, Android phones in Google Wallet. For a tap-to-use program like Mintleaf Rewards, nothing changes. The customer pays by tapping their phone like they always do, and the pass is applied automatically (on iPhone it works through Apple Pay). For barcode programs, the customer opens the pass in their wallet and shows the code at the till. The enrollment flow shows exactly what's collected and asks for consent before anything is stored, with a link to your privacy policy. Staff shouldn't improvise privacy answers. Point at the consent screen, it says it best. When something goes wrong at the counter, a scan that won't open or a pass that won't add, [enrollment troubleshooting](/enrollment/troubleshooting) is the staff reference for the common cases. # How to promote your wallet pass program Source: https://docs.getstell.com/promote/overview A launch plan and ready-to-adapt examples for promoting your pass program by email, social media, your website and in store, plus the wallet badge rules. Your program only grows when customers hear about it. This section gives you a launch plan and ready-to-adapt examples for every channel: emails, social media posts, website placements, and in-store material, all illustrated with the **Mintleaf Rewards** demo program. Whatever the channel, every promotion ends at one of two entry points: | Entry point | Who it's for | What happens | | - | - | - | | **Enrollment link or QR code** | New customers, or anyone you can't identify | The customer fills in the short [enrollment flow](/enrollment/how-enrollment-works) and adds the pass to **Apple Wallet** or **Google Wallet** | | **Wallet button widget** | Customers already in your systems, signed in on your site or addressed by email | One tap issues their pass directly, no form. See the [wallet button widget](/enrollment/wallet-button-widget) | Pick the right entry point per channel and the rest is communication. ## The channels The strongest launch channel. Announce the program and give existing customers a one-tap button. Reach people you can't email, with post ideas for the launch and beyond. Invite visitors on your public site and put a one-tap button on the signed-in member page. Posters, counter cards, receipts, and screens, plus what your staff should say. ## A launch plan A coordinated launch beats trickling the news out. Here's how a week could look for Mintleaf Coffee: Before any customer hears about the program, have your staff enroll and use their own passes for a few days. They'll find rough edges, and they'll be able to answer customer questions from experience. The [onboarding checklist](/onboarding/checklist) covers the technical side of going live. Posters, counter cards, and receipt QR codes go up first. The counter is where joining makes the most immediate sense. See [in-store material](/promote/in-store). Tell your existing customers. Members in your CRM can get a one-tap invitation, and everyone else gets the enrollment link. See [email](/promote/email). Add a banner or program page for new visitors, and the wallet button to the signed-in member page. See [website and my page](/promote/website). Post the announcement once the other channels are ready, so everyone who clicks can join immediately. See [social media](/promote/social-media). Promotion isn't a one-week project. Keep the receipt QR printing, rotate ongoing social posts, and brief new staff. Once customers hold the pass, Engage push notifications reach them directly on the lock screen. If you don't see Engage in your navigation, it isn't enabled for your company. [Contact support](/support/contact). Engage reaches customers who already hold your pass. The channels in this section are for reaching everyone who doesn't, yet. Mintleaf Rewards promotion across channels: an announcement email, an Instagram post, a counter poster with a QR code, and a website button ## Using the wallet badges The **Add to Apple Wallet** and **Add to Google Wallet** badges are recognizable. Customers know what tapping them means. That recognition only works if the badges look exactly the way Apple and Google designed them, so both providers set rules for their use. The short version: * **Use the official artwork only.** Never recreate, recolor, crop, stretch, rotate, or animate a badge, and never add shadows or other effects. The easiest way to stay compliant: the wallet button widget renders correct badges for you on the web, and the configurator's **Download all assets** button gives you every official button image as a ZIP for emails. See the [wallet button widget](/enrollment/wallet-button-widget). * **Keep the badge secondary.** It's the call to action, not the hero. Your program and brand carry the message, the badge closes it. Don't let it dominate the layout, and don't place text or graphics over it. * **Give it room.** Leave clear space around the badge. As a rule of thumb, at least a tenth of the badge's height on every side. * **Match the format to the medium.** Vector files (SVG) for web and email, print-quality vector files (EPS) for anything printed. Never print a screenshot of a badge. * **In print, pair the badge with a QR code.** Paper can't be tapped. The badge signals what the customer gets, the QR code is how they get it, and a short scanning instruction bridges the two. See [in-store material](/promote/in-store). Never design your own version of a wallet badge, not even "just matching our brand colors". Altered badges break the providers' trademark rules and customers trust them less. The full rules live with the providers: Apple's [Add to Apple Wallet guidelines](https://developer.apple.com/wallet/add-to-apple-wallet-guidelines/) and Google's [Google Wallet brand guidelines](https://developers.google.com/wallet/generic/resources/brand-guidelines). Downloading badge artwork directly from Apple requires accepting Apple's wallet marketing agreement, and your communications remain your responsibility under both providers' trademark rules. When in doubt, check their pages, which always reflect the current requirements. ## Watch it work You'll see promotion working in the portal. Newly issued passes appear in [Passes](/passes/overview), and once customers start using them, every tap or scan shows up in the [Transaction Log](/transactions/overview). # Promote your pass program on social media Source: https://docs.getstell.com/promote/social-media Post ideas and example copy for announcing your pass program on Instagram, Facebook, TikTok and LinkedIn, with every post routed to your enrollment link. Social media reaches the customers you can't email: followers, locals, people tagged by friends. Since you can't identify who's looking, every social post leads to the same place, your **enrollment link**, where the [enrollment flow](/enrollment/how-enrollment-works) takes anyone from interested to pass-in-wallet in about 30 seconds. The one-tap wallet button doesn't apply here, because it needs a known customer. ## Where the link goes Most platforms make links awkward in feed posts, so plan the route to the enrollment link per format: | Format | How to link | | - | - | | Instagram / Facebook feed post | "Link in bio". Put the enrollment link in your profile, or use your bio link tool | | Instagram / Facebook story | Link sticker straight to the enrollment link | | Short video (TikTok, Reels) | Link in bio, plus the call to action spoken and on screen | | X, LinkedIn | The enrollment link inline in the post | Skip QR codes in feed posts. The viewer is already holding the phone they would scan with, and a tappable link always wins on a screen. QR codes belong where the customer's phone is *not* the medium: [print and in-store screens](/promote/in-store). ## The announcement Lead with the benefit, make the friction-free part explicit ("no app"), and post it once the other channels are live, so everyone who clicks can join on the spot. Example copy for Mintleaf Coffee, by platform style: Visual-first: the strongest image is the pass itself on a phone, held somewhere unmistakably yours. ```text theme={null} Your morning coffee now comes with perks. Mintleaf Rewards lives in your phone's wallet. No app to install, no plastic card to lose. Join in 30 seconds and every coffee counts when you tap to pay. The tenth one is on us. Link in bio to join. ``` Image direction: a hand holding an iPhone with the Mintleaf Rewards pass open, latte in the background. A 15-second clip beats any explanation. Concept for TikTok or Reels: ```text theme={null} Scene 1 (3s): Barista slides a flat white across the counter. Scene 2 (5s): Customer taps their phone to pay. Cut to the pass in the wallet, where the stamp count ticks up. Scene 3 (4s): On-screen text, "No app. It just works when you pay." Scene 4 (3s): "Mintleaf Rewards. Link in bio." ``` The moment that sells it is the stamp counting up by itself: no scanning, no extra step. Shorter, link inline: ```text theme={null} Mintleaf Rewards is live. Your loyalty card, but in your phone's wallet. No app, no plastic, and every coffee counts automatically when you tap to pay. Join in 30 seconds: ``` ## Keeping it going The announcement gets you launch-week joins. Steady posts keep the program visible all year. Themes that keep working, with a Mintleaf-style hook for each: | Theme | Example hook | | - | - | | Milestone | "Member number 1,000 just got their free flat white. Welcome aboard, all of you." | | Reward spotlight | "Reminder: stamp number ten is a free coffee. Any coffee. Yes, the big one too." | | How it works | "Did you know? Your pass counts your coffees automatically when you tap to pay. There's nothing to scan." | | Behind the counter | "Maria has personally handed out 200 free tenth-coffees. She'd like to make it 201." | | Seasonal | "Pumpkin season is here, and yes, it counts toward your free coffee." | Each post carries the same quiet footer: a line on how to join, link in bio. ## Badges and imagery In social imagery, prefer showing **the pass on a phone** over the wallet badges. The pass carries your brand, and a feed post isn't a place anyone can tap a badge anyway. If you do include an **Add to Apple Wallet** or **Add to Google Wallet** badge in a graphic, the usual rules apply: official artwork, unmodified, with room around it. See [using the wallet badges](/promote/overview#using-the-wallet-badges). Mintleaf Rewards Instagram announcement mockup: a phone showing the green Mintleaf pass held over a latte, with the caption beneath Mintleaf Rewards Instagram story mockup with a Join now link sticker over a photo of the pass on a phone # Promote your pass program on your website Source: https://docs.getstell.com/promote/website Invite visitors on your public website with the enrollment link, and give signed-in members a one-tap Add to Wallet button on their account page. Your website serves two audiences, and each gets its own tool. Anonymous visitors, the people browsing your menu or finding your opening hours, get the **enrollment link**, which takes them through the short [enrollment flow](/enrollment/how-enrollment-works). Signed-in members on your "my page" get the **wallet button widget**. You already know who they are, so one tap on **Add to Apple Wallet** or **Add to Google Wallet** issues their pass with no form at all. ## Your public site New visitors won't go looking for your program. Put it where they already are: * **Homepage banner.** A short strip or section announcing the program, linking to the program page or straight to the enrollment link. * **A dedicated program page.** The place every other channel can link to, and the one spot that explains the program properly. * **Footer link.** Cheap, permanent visibility on every page. * **Order confirmation page.** If you sell online, the post-purchase moment is ideal: the customer just demonstrated they're coming back. A program page doesn't need much. Mintleaf's version: > **Earn with every cup** > > Mintleaf Rewards lives in your phone's wallet. No app, no plastic card. Every coffee counts automatically when you tap to pay, and your tenth is free. > > 1. **Join.** 30 seconds, right from your phone > 2. **Tap to pay** like you always do, and your pass counts each visit > 3. **Enjoy** your free tenth coffee > > \[ Join Mintleaf Rewards ] > > *Questions? It works with Apple Wallet and Google Wallet, and there's nothing to install.* One headline, the benefit, three steps, one button to the enrollment link. A short FAQ below it (no app needed, works on iPhone and Android, what happens with my details) catches the hesitant. Mintleaf Coffee homepage banner announcing Mintleaf Rewards with a Join now button ## My page: one tap for signed-in members The signed-in member page is the best home for the [wallet button widget](/enrollment/wallet-button-widget#on-your-site). The member is identified, so the widget issues their pass on a single tap: no form, no typing, no drop-off. Setup, options, and the embed snippet are all on the widget page. What's left is placement: * **Account dashboard.** The first thing a member sees after signing in. * **A "my benefits" or "my membership" section.** Where members already look for perks. * **Post-purchase page.** They're signed in, they just bought something, and the pass would have counted it. Don't hide the button behind a separate "get your pass" sub-page. The fewer clicks between signed-in and pass-in-wallet, the more passes get added. A signed-in Mintleaf Coffee account page showing the Add to Apple Wallet and Add to Google Wallet buttons rendered by the wallet button widget ## Which entry point where | Visitor | What to give them | | - | - | | Anonymous visitor on your public site | The enrollment link (banner, program page, footer) | | Signed-in member on my page | The wallet button widget: one tap, no form | | Email recipient | The email wallet links. See [email](/promote/email) | ## Badges on the web On my page, the widget renders official, correctly localized wallet badges for you, so there's nothing to get wrong. If you place static badge images elsewhere on your site, use the official SVG artwork and follow [the badge rules](/promote/overview#using-the-wallet-badges). # Contact Stell support Source: https://docs.getstell.com/support/contact How to reach Stell support by phone or email, what to include so your request can be resolved quickly, and what happens after you get in touch. The **Support** page in the portal sidebar always has the up-to-date contact details for your company, including a pre-filled email template. Here's how to choose the right channel. ## Choose your channel * **Phone, for urgent production issues.** Call when customers can't add passes, a live distribution is failing, or portal access is blocking today's work. The number is on the portal's Support page. * **Email, for most things.** Best for portal questions, setup requests, API help, template changes, and anything that benefits from screenshots: [hello@getstell.com](mailto:hello@getstell.com). * **Slack, for quick questions.** If your company has a shared Slack channel with Stell, use it for quick checks and short back-and-forth. Keep detailed error reports in email so the full context stays in one place. ## What to include The more of this you include, the faster the answer: * Your company or merchant name. * The affected program, pass, template, distribution, transaction, or integration, with IDs or references where possible (pass ID, transaction ID, order reference). * What you expected, what happened, and the exact error text if you saw one. * How urgent it is, including any launch date or customer-facing impact. * Screenshots, when they help. Never include secrets: API keys, tokens, passwords, or private keys. Avoid customer names, emails, and phone numbers unless they're needed to investigate the issue. ## What happens next We'll either answer directly or ask for the exact detail we still need. For urgent issues, call first and follow up by email with the IDs and screenshots. # Stell portal FAQ: passes, enrollment and Engage Source: https://docs.getstell.com/support/faq Answers to common questions about the Stell merchant portal: access and optional modules, passes, customer enrollment, Engage messaging and integrations. ## Access and account They're optional modules enabled per company. If your company should have one, [contact support](/support/contact). [Contact support](/support/contact). They can verify your identity and reset your two-factor setup. If you can still sign in, you can switch authenticator under [account security](/account/security) yourself. The portal shows one company at a time. Open the account menu at the bottom of the sidebar and switch to the right company. ## Passes **Pre-Active**: issued but not yet added to a wallet. **Active**: in a wallet and working. **Inactive**: the customer removed it from their wallet, and it reactivates if they add it back. See [pass lifecycle](/passes/lifecycle). No. Voiding is permanent. Issue a new pass for the customer instead. Look up the pass and check its status, then check the [transaction log](/transactions/overview) for a denied transaction. The status reason usually explains it. Yes. Saving the [pass template](/pass-templates/overview) updates all existing passes immediately. ## Enrollment Yes. Android users enrolling get an **Add to Google Wallet** button, just as iPhone users get Apple Wallet. If a customer sees "no compatible wallet," the program doesn't have that wallet enabled. [Contact support](/support/contact). Find them under **Engage → Contacts** and check the pass status. Most often the pass is Pre-Active, meaning they never finished adding it to their wallet. See [enrollment troubleshooting](/enrollment/troubleshooting). Yes. Fields, consents, branding, age limits, and verification rules are configured per program. [Contact support](/support/contact) with the changes you want. ## Messaging (Engage) Push messages only reach customers with the pass **installed in a wallet**. Members whose passes are Pre-Active or Inactive aren't reachable. Sent campaigns can't be edited. Scheduled notifications sit in the **Pending** tab of [notification history](/engage/notifications) until their send time. ## Integrations No. The full key is shown only once, at creation or rotation. Rotate the key to get a new secret, then update your application. Set up [event webhooks](/api-reference/webhooks/overview) on the program page. Stell sends the program's pass events to your HTTPS endpoint, and [webhook deliveries](/api-reference/webhooks/deliveries) documents the exact requests for your developers. Didn't find your answer? [Contact support](/support/contact). # Transaction log: taps, scans and loyalty points Source: https://docs.getstell.com/transactions/overview Look up every tap and scan of a pass across your stores and terminals, with payments, loyalty points and the status reason behind a denied transaction. A **transaction** is a record of a pass being used at one of your stores: a tap, a scan, sometimes with a payment and loyalty points attached. The **Transaction Log** is your company's complete, newest-first record of all of them. Transaction log ## Finding a transaction Open **Transactions** in the sidebar. Filter by **Reference**, **Status**, **Type**, **Instrument** (how the pass was captured, for example a card terminal, QR scan, or NFC tap), and a **From**/**To** date range, then click **Apply**. Each row shows the date, status, type, the pass and cardholder, the store and terminal, the amount, and points earned or spent. Click a row to open the full detail. Your own POS can also record transactions here through the API. See [identify pass holders at the POS](/api-reference/integration/in-store). ## Transaction statuses | Status | Meaning | | - | - | | **Approved** | The transaction went through | | **Denied** | The transaction was rejected. The detail page shows the reason | | **Failed** | Something went wrong processing it. The detail page shows the reason | | **Pending** | Still being processed | ## The transaction detail The detail page shows everything your company is involved in for one transaction: * **Capture details**: the reference, type, instrument, and exactly when it was processed. * **Payment & rewards**: the amount, payment and PSP references, and loyalty points earned or spent (shown when the transaction has a payment or points component; a plain tap may have neither). * **Line items**: the purchased items with SKU, quantity, and amount, when your POS provides them. * **Related records**: direct links to the [pass](/passes/overview), the [store, and the terminal](/organization/overview) involved. When a customer disputes a denied tap, start here: the status reason on the detail page usually explains it, for example a voided pass or an expired membership.