💰 Loyalty Wallet Integration Guide

This guide explains how to award and redeem loyalty balance using the admin (server-to-server) endpoints and how to read a user’s balance and history with the public bearer on client surfaces.

TL;DR

  • Admin flow (server-to-server): Use your backend bearer (Client Credentials) to credit/debit loyalty for a user after POS events.
  • End-user flow (front end): Your app uses the public bearer (from the session code exchange) to show my balance and my transactions.
📘

For more information about authentication and integration please refer the Authorization Flow guide for full details.


sequenceDiagram
    autonumber
    participant PartnerBE as Partner Backend / POS
    participant FTAuth as Funtrips Auth (OAuth)
    participant FTAPI as Funtrips API (Wallet)
    participant App as Partner App / Webframe
    participant User as End User

    %% A) Server-to-server auth
    PartnerBE->>FTAuth: POST /oauth/token (client_credentials)
    FTAuth-->>PartnerBE: 200 { backend_bearer }

    %% B) POS posts a wallet transaction
    PartnerBE->>FTAPI: POST /v1/wallet/transactions Auth: backend_bearer
    FTAPI-->>PartnerBE: 201 { transaction_id, new_balance }

    Note over FTAPI: If wallet does not exist, auto-create on first transaction

    %% C) Start session for user-facing access (code)
    PartnerBE->>FTAPI: POST /v1/session Auth: backend_bearer
    FTAPI-->>PartnerBE: 200 { session_code, frame_url?, pkce_required }

    %% D) Webframe path (no PKCE; FT exchanges code)
    PartnerBE-->>App: Provide frame_url with session_code
    User->>FTAPI: Open frame_url?session_code=...
    FTAPI-->>App: Issues public_bearer to the frame (server-side exchange)

    %% E) User self-service with public bearer
    App->>FTAPI: GET /v1/wallet/me/balance Auth: public_bearer
    FTAPI-->>App: 200 { balance }

    App->>FTAPI: GET /v1/wallet/me/transactions?limit=...&next_token=... Auth: public_bearer
    FTAPI-->>App: 200 { data:[...], pagination }

    %% F) Admin/audit lookups (backend only)
    PartnerBE->>FTAPI: GET /v1/wallet/admin/users/{external_user_id} Auth: backend_bearer
    FTAPI-->>PartnerBE: 200 { user, current_balance }

    PartnerBE->>FTAPI: GET /v1/wallet/admin/transactions?external_user_id=...&limit=... Auth: backend_bearer
    FTAPI-->>PartnerBE: 200 { data:[...], pagination }

    %% G) Refund or redemption (debit)
    PartnerBE->>FTAPI: POST /v1/wallet/transactions Auth: backend_bearer
    FTAPI-->>PartnerBE: 201 { transaction_id, new_balance }

🧩 Core Concepts

  • User identity: You reference users by your external_user_id. In admin calls, you pass this identifier when needed; in public calls, identity comes from the public bearer itself.
  • Implicit user creation: If a wallet does not exist for a user, the first successful credit/debit can create it automatically (recommended), or you may choose to call an explicit admin “create user” endpoint if present.
  • Idempotency: All transaction-creating endpoints accept Idempotency-Key. Reuse the same key to safely retry without double crediting/debiting.
  • Atomic balance: Each transaction returns the balance_after snapshot; you don’t need to re-fetch to confirm the result.
  • Types:
    • credit = add to balance (earn).
    • debit = subtract from balance (spend/refund-reversal).

🏪 POS → Loyalty: Award on Purchase (Credit)

When a POS sale completes and qualifies for loyalty:

  1. Calculate the loyalty amount in your system (rules, promos, rounding, currency).
  2. Create a credit transaction with your backend bearer. Include:
    • external_user_id
    • type = credit
    • amount (Money)
    • an idempotent reference (e.g., POS receipt/order ID)
    • optional description (“POS purchase”, campaign code, etc.)
  3. Handle responses:
    • 201/200 → success, store transaction.id and balance_after
    • 409 → conflicting duplicate or state; investigate your reference/idempotency usage
    • 422/400 → validation error; correct and retry with same Idempotency-Key if safe

📘

See the admin create transaction for details


⚠️

Why idempotency matters:

If your POS retries after a network blip, re-sending with the same Idempotency-Key guarantees no double credit.


↩️ POS Voids/Refunds: Reverse the Award (Debit)

If a sale is voided or refunded:

  1. Look up your original loyalty reference (e.g., receipt/order ID).
  2. Create a debit transaction for the same amount (or the differential if partially refunded). Use a new Idempotency-Key but keep a clear reference that ties it to the refund event.
  3. Confirm the updated balance_after.

📘

See the admin create transaction for details

💡

Tip: If your business rules prefer reversal semantics, you can store the original transaction ID in the debit’s metadata/reference for reconciliation.


👤 Showing Balance & History to the User (Public Bearer)

Your front end (app or webframe) uses the public bearer to show the authenticated user’s wallet:

  • Get my balance: /reference/wallet_me_balance
📘

See the wallet balance for details

  • Get my transactions: /reference/wallet_me_transactions Optional filters: type=[credit|debit], date ranges, pagination via next_token.
📘

See the list transactions for details

These endpoints are read-only and scoped to the bearer’s subject; no user ID is needed in the path.


🧾 Admin Reads & Reconciliation

Your ops or reporting jobs can read wallets and transaction history on the backend:

📘

See the get wallet balance (admin) for details

📘

See the list transactions (admin) for details

📘

See the list users (admin) for details


⚙️ Concurrency & Race Conditions

If POS and your e-commerce flow can post transactions simultaneously for the same user:

  • Always send one transaction per atomic event (not batched math).
  • Rely on the API’s atomic update to maintain a correct balance_after.
  • If you receive a 409 due to ordering constraints, retry with backoff; keep the same Idempotency-Key if the payload is identical.

🚨 Error Handling & Retries

  • Idempotent retries: For network timeouts/5xx, retry with the same Idempotency-Key.
  • Validation (400/422): Fix the payload (amount format, currency, user ID) and either:
    • Retry with the same Idempotency-Key if it’s truly the same intended transaction, or
    • Use a new key only if you’re intentionally creating a different transaction.
  • Rate limits (429): Respect Retry-After and back off.

🔒 Security Notes

  • Never put tokens in URLs; use the Authorization: Bearer … header over HTTPS.
  • Backend bearer must live only on your server.
  • Public bearer is short-lived; store in memory/session where possible and rotate.
  • Use Idempotency-Key for every credit/debit.
  • Log transaction reference and transaction.id for later audits.

✅ Testing & Go-Live Checklist

  • Create user implicitly via first credit in a sandbox.
  • Verify debit works and properly reduces balance.
  • Confirm /wallet/me endpoints render correct balance & history with public bearer.
  • Exercise idempotent retry (same key, same payload) to prove no double posting.
  • Verify pagination with limit + next_token on transaction lists.
  • Document your mapping: POS receipt/order → reference.