# VXPass Docs > Public copy of the VXPass admin guide. The canonical web rendering lives at https://vx-pass.web.app/docs. ## Getting started VXcards lets your customers carry your loyalty program in **Apple Wallet** and **Google Wallet**. Customers register through a web form or via WhatsApp, receive a wallet pass linked to your client account, and you manage them — and send updates — from this admin portal. ### Key concepts **Client** — Your company's account on VXcards. All customers, passes, and team members are scoped to a client. **Pass configuration** — The design and field layout of your wallet pass. Edited in the pass builder, then *published* to take effect. **Campaign** — A one-shot or scheduled message you send to customers. Can include a wallet overlay (visual update on the pass) and/or a push notification. **Member** — A teammate with a role on your client. ## First steps 1. **Sign in** at the portal. 2. If you're an owner or admin, invite your team in **Team**. 3. Open the **Pass Builder** and customize your pass. 4. **Publish** the configuration. 5. Share your registration link or WhatsApp number with customers. > **Use case:** **Coffee shop launching a stamp card.** Owner signs up, designs a pass with their brand colors and a "Buy 9, get 1 free" stamp counter on the front, publishes, then prints a QR poster pointing to the registration URL. Customers scan, fill the form, and the pass appears in their wallet — no app download. ## Roles & permissions Every team member has a role on your client. Roles are hierarchical — each one inherits everything below it.
| Role | Rank | What they can do | | --- | --- | --- | | `viewer` | 0 | Read-only across all sections. Cannot see audit logs. | | `store_manager` | 1 | Viewer + manage promotions, coupons, discounts, approve receipts, run the POS scanner for assigned stores. | | `editor` | 2 | Full content powers: pass templates, campaigns, loyalty rules, promotions, POS across all stores. | | `admin` | 3 | Editor + invite/remove members, assign roles, edit client settings, view audit logs. | | `owner` | 4 | Admin + billing, ownership transfer, client deletion. |
> **Use case:** **Multi-location retailer.** The HQ marketing lead is an `editor` — they design the pass and run cross-store campaigns. Each store has its own `store_manager` who can only review receipts and run POS for their location. The CEO stays as `owner` for billing. ## Pass builder The pass builder is where you compose a **pass configuration** — the design, fields, and behavior of a wallet pass. The page is split into three top-level tabs: `Pass Builder`, `Nearby Message`, and `Localization`. Inside *Pass Builder*, you'll move through four sections (Basic Info → Front → Back → Graphics) plus a **Live Preview** that updates as you type. ### What the actual screen looks like ### The five sections, explained **Basic Info** — Pass description, background / foreground / label colors, default language. Sets the global look and the canonical text rendering. **Front** — What the customer sees without flipping the pass: *header*, *primary*, *secondary*, and *auxiliary* fields. Loyalty points, tier, stamp count, and member name typically live here. **Back** — Fields revealed when the customer taps the *i* on Apple Wallet (or "Details" on Google Wallet). Long-form info, T&Cs, store hours, support email, and your **back links**. **Graphics** — Logo (top-left), icon (notification thumbnail), and strip image (banner across the middle). Resized server-side via `sharp`. **Live Preview** — The right-hand panel renders an Apple Wallet card and (toggled) a Google Wallet card from your current draft. Below it you can expand the raw JSON payload that will be signed into the `.pkpass`. > **⚠ Warning:** **Publishing does NOT push updates to passes already in customer wallets.** **Apple Wallet** passes are refreshed on *Refresh All Passes*, when a campaign fires, or when a receipt is approved. **Google Wallet** objects are re-upserted only on Refresh All Passes, receipts, and campaigns. ## Loyalty configuration The *Loyalty Configuration* page defines how customers **earn** and **progress**. Without a loyalty config, the pass is just a card. ### Preview ### How points get on the pass 1. Customer makes a purchase and submits a receipt (web upload, WhatsApp photo, or POS scan). 2. OCR extracts the total. Points are calculated using your **amount per point** rule. 3. If a tier multiplier applies, points are scaled before being credited. 4. The customer's loyalty balance updates and their pass is re-synced — the new total appears on the wallet card automatically. > **Use case:** **Tiered restaurant program.** Bronze members earn 1 pt / $1; once they cross 500 pts they become Silver and start earning 1.25× on every receipt; at 1,000 pts (Gold) they hit 1.5× and the pass's tier field updates to "Gold member" with a gold accent strip. ## Corporate programs **Programs** are multi-client loyalty alliances. They let you grant special benefits to a group of customers (employees of a corporate partner, students of a school, members of a club) and track their activity across *all* participating clients. ### How they work - You create a program (e.g. `acme-corp-employees`) and define its benefits — bonus point multiplier, exclusive offers, custom tier overrides. - Each client opting into the program adds it to their setup. Customers can be assigned by email-domain whitelist or by a unique signup code. - When a program member earns points at any participating client, the program's multiplier and benefits stack on top of that client's normal loyalty rules. - The pass shows a **program badge** on the back, plus an extra field if you choose to surface it on the front. > **Use case:** **Office-tower partnership.** A coworking building runs a "Tower Perks" program. Café Aurora and three other tenants sign up. Anyone with an `@tenants.tower.com` email gets 2× points and a free monthly drink. Each tenant tracks redemptions independently, but the program reports roll up at the tower level. ## Back of pass & links The *back* of the pass is what the customer sees after tapping the *i* in Apple Wallet (or "Details" / overflow menu in Google Wallet). It's a long, scrollable list of fields and tappable links — your contact info, terms, store list, and any deeplinks back into your app or website. ### Common back-of-pass items - **Member info** — full name, email, phone, sign-up date. - **Customer Service** — email and phone, both rendered as tappable links. - **Special Offers** — a *back link* URL like `https://your-portal/offers/{serialNumber}?token=...`. Tapping opens the customer's personalized offers page; auth is handled by the signed token, no login needed. - **Terms & Conditions** — a long-form text block. - **Store locator** — a link to your map or list of locations. - **Program info** — when the customer is in a corporate program, the program name and its perks. ### Example back-of-pass layout Back links are powerful because they can carry a signed token specific to the customer — letting you render personalized pages (offers, redemptions, even refer-a-friend) without requiring a sign-in flow. ## Notifications inside the pass Beyond campaign-driven messages, the pass itself can emit notifications based on **contextual triggers** — without you sending anything. ### Birthday messages - If a customer's birthday is captured at registration, the pass schedules a wallet update on that date — a celebratory overlay (e.g. "🎉 Happy birthday, Jane! Free pastry today") and a tap-through push notification. - You configure copy and the offer in *Loyalty* / *Notifications*; everything else is automatic. ### Nearby (location-based) The **Nearby Message** tab lets you set a default location-triggered message and a **max distance** (20–500 m) at which it fires. - The pass embeds GPS coordinates of every store registered in *Nearby*. - When the customer's phone gets within range, the lock-screen shows the nearby message — e.g. *"You're near Rivergate Mall. Show your pass for 10% off ☕"*. - Per-store overrides let each location speak differently. - **iOS only** — Apple Wallet renders nearby triggers on the lock screen; Google Wallet does not currently expose this primitive. ### Other in-pass triggers - **Wallet add** — fires once after the pass is added to the wallet. - **First receipt** — fires after the first approved receipt. - **N-th visit** — every Nth visit (configurable). - **Points threshold** — when the customer crosses a milestone (e.g. 500 pts → silver tier). All of these are configured in the *Notifications* page and surface either as wallet-side push (Apple) or web-push (browser-registered customers). > **Use case:** **Foot-traffic boost on slow weekdays.** A café enables a 100-meter nearby trigger at every store with the message *"Tuesday treat — 2× points all day"*. Customers walking past get the lock-screen nudge without any campaign send. ## Customers The Customers page lists everyone with a pass, plus their registration source, language, and status. ### Preview ### Registration channels - **Web form** — your public registration link. - **WhatsApp** — customers send a message to your number; complete an in-WhatsApp registration form. ### Duplicate detection If a new registration's phone or email matches an existing customer, the system flags it. The customer is not silently overwritten — duplicates surface for review. ## Campaigns & notifications Campaigns are how you reach customers after they've registered. Two delivery channels, often combined. ### Preview ### Two delivery channels per campaign - **Wallet overlay** — a visual update applied to the customer's pass (Apple and Google). - **Push notification** — a tap-through alert delivered through the wallet (Apple) or web push (browser). > **Use case:** **Win-back for lapsed customers.** Filter to customers whose last receipt was 30+ days ago, send a wallet overlay reading "We miss you — 2x stamps this week" plus a push notification. Sending also re-syncs the recipients' Google Wallet objects. ## SMS campaigns SMS is a third campaign channel alongside wallet overlays and push. Sent from a per-client 10DLC (US business) phone number, and gated by per-channel consent + a monthly budget cap. ### How provisioning works 1. Platform staff orders a US number for your client through the SMS provisioning wizard on the Companies page. The order is irrevocable, so the wizard locks the client during the call to prevent double-buys. 2. The number is attached to the platform Messaging Profile and registered under the platform's TCR 10DLC campaign. 3. The campaign goes from `pending` → `approved` (usually hours, occasionally days). Sending is blocked until the status reads `approved`. Status syncs automatically; staff can also run a manual sync from the SMS card. 4. Once approved, flip the master `enabled` toggle on the SMS card. SMS now appears as a checkable channel inside the campaign builder. ### Consent rules (carrier-mandated) US 10DLC and CTIA SCT-G require an explicit, per-channel opt-in for SMS. Marketing consent on the registration form is *not* sufficient on its own. - **Web registration:** the SMS opt-in checkbox sits under the marketing-consent row with the required disclosure (msg/data rates, STOP/HELP, frequency). - **WhatsApp registration:** matching opt-in question in the registration flow. - **Admin override:** staff can grant or revoke consent on a customer's behalf (with audit trail) via the `setCustomerSmsConsent` callable. - **STOP / HELP:** when a customer texts `STOP`, `BAJA`, `UNSUBSCRIBE`, or `CANCELAR`, we mark them opted out immediately. `HELP` triggers a static reply with the program name and support contact. - **Existing customers without consent** are excluded from every SMS send. The audience preview's *SMS reach* count shows you exactly how many qualify. ### Segment counting + cost preview SMS bodies are billed per *segment*, not per message: - **GSM-7 (plain ASCII + Latin):** 160 chars in 1 segment, then 153 chars/segment for multi-segment. - **UCS-2 (emoji or non-Latin):** 70 chars/segment, then 67 chars/segment after that. A single emoji can flip the whole message to UCS-2 — a 161-char ASCII message is 2 segments, but adding one emoji makes the same length 3 segments. The campaign builder shows a live segment counter and a cost estimate (`segments × recipients × $0.0075` for US 10DLC). The reach preview drives the recipient count, so toggling audience filters updates the cost in real time. ### Monthly budget cap Set `smsConfigs.{clientId}.monthlyBudgetCents` to enforce a hard ceiling. Sends that would push the projected month-to-date spend over the cap are rejected before dispatch, with the skip reason `budget_exceeded` surfaced in the campaign send result. The SMS spend card shows current month, remaining budget, a usage progress bar (warns at 90%), and 6-month history. Set `monthlyBudgetCents` to `0` or leave it unset to disable the dollar cap. The separate `monthlySendCap` (recipient count) still applies regardless. ### Skip reasons **`recipient_no_consent`** — `smsConsentAtIso` missing — capture consent via re-opt-in. **`recipient_opted_out`** — Customer texted STOP / BAJA / UNSUBSCRIBE. **`recipient_has_no_phone`** — No phone on file. **`client_sms_not_ready`** — SMS card is not in the *enabled + approved + has-from-number* state. **`campaign_sms_body_empty`** — SMS channel turned on but body is blank. **`budget_exceeded`** — Projected spend would exceed `monthlyBudgetCents`. **`send_failed`** — The SMS provider rejected the send. Check the Cloud Functions logs. > **Use case:** **Flash sale to opted-in shoppers.** Filter audience to customers who consented to SMS and have at least 1 visit, write a 140-character body, enable SMS only (no wallet overlay), and schedule for Friday 10am. The reach badge tells you the exact send count; the cost preview projects total spend before you publish. ## WhatsApp WhatsApp messaging runs on top of Meta's WhatsApp Business API. Two distinct use cases share the same connection: **transactional flows** (registration, receipt acks, welcome) and **campaign broadcasts** (announcements, offers, the SMS opt-in invite). ### Preview ### Connection + toggles Set up under the WhatsApp config page (Companies → WhatsApp tab): - **Connection** — we provision the phone number and connect it to Meta's WhatsApp Business API. Status flips from `pending` to `connected` once the WABA is live. - **Default language** — `en` or `es`. Drives template selection when the customer's language is unknown. - **Receipts via WhatsApp** — opt in to let customers submit purchase receipts as photos. - **Monthly budget cap** (`monthlyBudgetCents`) — dollar ceiling on outbound marketing-conversation spend. Sends that would exceed the cap abort with skip reason `budget_exceeded`. Set to `0` to disable. ### Template lifecycle Meta requires every outbound template to be pre-approved per language. Workflow: 1. Submit a template via the WhatsApp config page — one-click "Submit" on each recommended template. 2. Status reads `PENDING` at Meta. Approval typically takes a few hours, occasionally up to 24h. 3. Once status is `APPROVED`, the template appears in the campaign builder dropdown (filtered to MARKETING category — UTILITY templates like receipt acks fire automatically and aren't selectable as broadcast). **Authoring rule:** template body text cannot start or end with a `{{N}}` placeholder. The system blocks templates that violate this. ### WhatsApp as a campaign channel Alongside wallet / push / SMS, WhatsApp can be a 4th delivery channel. Because Meta blocks free-form marketing outside the 24h customer-initiated window, campaigns reference an **approved template + parameter values** instead of typing a body. The builder shows a live preview of the template body with values interpolated. Supported parameter tokens: `{customer.name}`, `{customer.firstName}`, `{brand.name}`. Audience reach for WhatsApp = customers with a phone (opt-in is implicit by Meta policy for approved marketing templates). ### SMS opt-in invite (one-time per customer) A predefined campaign archetype that asks legacy customers (registered before per-channel SMS consent existed) to reply `YES` / `SI` via WhatsApp to grant SMS consent. - Pick it from the chevron menu next to **New Campaign**. The form pre-fills with the right audience filter (no SMS consent + has phone), channel (WhatsApp only), and template (`vxpass_sms_optin_invite`). Most controls are locked. - The inbound webhook recognizes `YES / SI / SÍ / START / UNSTOP / SUSCRIBIR` as a consent grant — stamps `smsConsentAtIso` + `smsConsentSource='whatsapp_optin_reply'` + sends a confirmation. - **STOP / BAJA / UNSUBSCRIBE** opts the customer out cleanly (no re-invite for that customer). - **90-day cool-down**: a customer who already received the invite is skipped on subsequent sends. Meta penalizes accounts that re-send the same MARKETING template too often. - The campaign detail view shows conversion: *Sent N → Opted in M → X%*. ### Skip reasons **`recipient_has_no_phone`** — No phone on file — required for WhatsApp send. **`client_whatsapp_not_ready`** — WhatsApp config not in the *enabled + connected + has-phoneNumberId* state. **`campaign_template_missing`** — No approved template selected on the campaign. **`budget_exceeded`** — Projected spend would exceed `monthlyBudgetCents`. **`optin_invite_too_recent`** — Customer received the SMS opt-in invite within the 90-day cool-down. **`send_failed`** — The WhatsApp send failed at the provider layer (proxy error, transport error, or Meta rejection). Check the Cloud Functions logs. ### Spend tracking Per-client rollup at `whatsAppSpend/{clientId}/months/{YYYY-MM}`. The spend card on the Companies page surfaces current-month cost, remaining budget (warns at 90% of cap), and a 6-month history. Meta marketing conversations cost about **$0.025** each in the US — a 50k-customer broadcast = ~$1,250. > **Use case:** **Backfill SMS consent for existing customers.** A bakery has 5,000 customers who registered before SMS opt-in existed. They run the SMS opt-in invite once via WhatsApp; 1,650 reply YES (33% conversion). Those 1,650 are now SMS-reachable for future flash sales; the rest stay on WhatsApp-only. ## Receipts If receipts via WhatsApp is enabled, customers can submit purchase photos for loyalty credit. ### Lifecycle Status values: `pending`, `failed` (OCR couldn't read), or `approved` (auto-approved if confidence is high). Store managers and editors approve, reject, or manually correct. > **Use case:** **Reward without a POS integration.** A small bakery without a connected POS asks customers to WhatsApp a photo of their receipt. Each approved receipt over $5 adds a stamp. ## Deals & offers Each **tenant** (a store inside a mall or a single-location business) can publish *offers* that customers redeem in-store. Offers appear automatically on the customer's pass back-of-pass offers link and become redeemable through the POS scanner. ### Three types of offers
| Kind | What it does | Example | | --- | --- | --- | | `discount` | A percent or fixed amount off, applied at checkout. The cashier confirms the deal in the POS scanner before completing the sale. | *15% off any pastry* · *$5 off purchases over $30* | | `visit_milestone` | Reward unlocks after the customer hits a target visit count. The pass shows progress; the cashier redeems when it's full. | *10th visit = free coffee* · *5 visits = a t-shirt* | | `purchase_threshold` | Reward unlocks once the customer's cumulative spend at this tenant crosses a target — choose either a free item or a discount as the reward. | *Spend $100 → free dessert* · *Spend $250 → 20% off next visit* |
### Creating an offer The form (a four-section dialog: *Basics*, *Schedule*, *Audience*, *Rule*) is what tenants fill in to publish a deal. ### Where offers show up - **On the pass** — the back-of-pass *Offers* link opens a personalized page listing every redeemable offer for that customer at every tenant. - **In the POS scanner** — when the cashier scans the customer's QR, the matching tenant's eligible offers are listed with a *Redeem* button next to each. - **In campaigns** — you can reference a deal in a wallet overlay or push notification. > **Use case:** **Stamp-card replacement.** Café Aurora creates a `visit_milestone` offer: "10th visit = free latte". Each receipt or POS scan counts as a visit; the pass shows progress (*7 / 10*). On the 10th visit, the cashier scans the QR, taps *Redeem*, hands over the latte, and the counter resets. ## POS scanner & redemption The **Tenant POS** page is the operator-facing scanner used at the register. It runs in any modern browser (no app install) and works through three steps: **Pick store → Scan QR → Redeem or mark a visit**. ### Who can use it - **Store managers** — limited to the stores they're scoped to. The backend rejects scans for stores outside their scope. - **Editors and above** — can run the scanner at any store of their client. - **Viewers** — cannot access the POS page. ### What the cashier sees ### How a redemption works under the hood 1. The cashier scans the customer's pass QR. The QR carries a **signed token** tied to that customer's serial number. 2. The backend verifies the token, confirms the cashier's account is allowed to act on this store, and returns the customer's profile + every eligible offer for this tenant. 3. The cashier picks an offer and taps **Redeem**. The backend records the redemption (audit log + offer counter) and re-syncs the customer's pass. 4. If the customer is just walking in (no offer applies), **Mark visit only** records the visit without consuming any reward. > **⚠ Warning:** **Two-layer permission check.** The frontend only shows the POS link to roles with `redemptions:write`. The backend additionally enforces per-store scope via `assertCanActOnPlace` — so even if a store manager pastes a QR for a customer at a store outside their scope, the redemption is rejected. ### Offline / camera-less fallback If the browser doesn't expose `BarcodeDetector` (older Safari, some kiosks), a manual paste field appears under the camera. ## Refresh All Passes The manual button that re-pushes the current pass design to every customer.
#### check_circle When to use it - After a meaningful pass design change you want existing customers to see. - After fixing a configuration bug that left passes in a stale state.
#### cancel When not to - For minor edits no one will notice — it's expensive. - More than once per change. It re-pushes everyone every time.
### Capacity Refresh All Passes is bounded by Cloud Functions execution time. For very large customer bases the operation may need to be split into batches — contact support if you have more than a couple thousand customers and the run is timing out. ## Troubleshooting **The customer can't see my new pass design** Publishing alone does not update installed passes. Run *Refresh All Passes*, send a campaign, or wait for the next receipt approval. **Pass not arriving on the phone after registration** Check the email/SMS link landed.On iPhone, the customer must open the link in Safari, not an in-app browser.For Android, the customer needs Google Wallet installed and signed in. **Google Wallet shows stale info** Google Wallet objects are only re-upserted on Refresh All Passes, receipts, or campaigns. Trigger one of those. **WhatsApp template was rejected by Meta** Common causes: the template starts or ends with a `{{N}}` placeholder, has unapproved promotional language, or uses formatting Meta doesn't allow. **A registration was blocked as a duplicate** Open the dedup flag in the Customers list. You can either merge into the existing record or release it as a separate customer. **Receipt stuck in "pending"** OCR confidence was below the auto-approval threshold. A store manager or editor needs to review it manually. **Campaign didn't reach everyone** Check that recipients had push enabled.Customers who removed their pass from their wallet won't receive overlays.Web push only works on browsers where the customer accepted the prompt. ## Getting help When contacting support, include: - Your **client ID** - The affected **customer ID** (if applicable) - An approximate **timestamp** It makes investigation dramatically faster.