lomi.

Customer portal

Build a hosted customer portal flow with launch sessions, OTP/magic-link auth, and customer-scoped access.

lomi. customer portal provides a hosted customer account area at customers.lomi.africa where end-users can:

lomi. customer portal for payment history and subscriptions
  • View payment history and open invoice receipts when available
  • View and manage subscriptions (pause, resume, cancel at period end, uncancel)
  • Add, remove, and set default saved cards (when enabled in portal policy)
  • Retry failed subscription payments in-portal before checkout fallback
  • Download digital purchases from the library

This guide describes the recommended production integration.

  1. Your merchant backend creates a one-time portal launch session.
  2. You redirect/open the returned launch_url.
  3. The hosted portal consumes the token once and asks the customer to verify via:
    • Email magic link, or
    • SMS OTP
  4. Portal session is established and scoped to (organization_id, customer_id, environment).
  5. Customer sees only their own records.

Do not mint launch sessions in browser code. Create them from your backend only.

Create a launch session

Use POST /customers/{id}/portal-launch-session:

  • Optional return_url
  • Optional flow_type (portal_home, subscription_cancel, subscription_manage)
  • Optional flow_subscription_id (required for subscription_cancel)
  • Optional flow_after_completion_url

See endpoint details and payload examples in Customers.

Eligibility model

A customer is eligible for portal access only if they:

  1. Belong to your organization in the requested environment, and
  2. Have at least one billing record (transaction or subscription)

This avoids exposing an empty portal to contacts that never transacted.

End-to-end example

import axios from "axios";

const apiKey = process.env.LOMI_SECRET_KEY!;
const customerId = "2d8f4f8b-1ea8-4de9-9fd8-f52f743bb265";

const { data } = await axios.post(
  `https://api.lomi.africa/customers/${customerId}/portal-launch-session`,
  {
    return_url: "https://merchant.example.com/account",
    flow_type: "subscription_cancel",
    flow_subscription_id: "3d6236f9-2c3e-4f0c-b00d-e2d73d3f0d24",
    flow_after_completion_url:
      "https://merchant.example.com/account/subscription-cancelled",
  },
  {
    headers: {
      "X-API-KEY": apiKey,
      "Content-Type": "application/json",
    },
  },
);

// Redirect customer to hosted portal
return data.launch_url;
curl -sS -X POST "https://api.lomi.africa/customers/2d8f4f8b-1ea8-4de9-9fd8-f52f743bb265/portal-launch-session" \
  -H "X-API-KEY: $LOMI_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "return_url": "https://merchant.example.com/account",
    "flow_type": "subscription_cancel",
    "flow_subscription_id": "3d6236f9-2c3e-4f0c-b00d-e2d73d3f0d24",
    "flow_after_completion_url": "https://merchant.example.com/account/subscription-cancelled"
  }'

There are three ways customers can reach the portal:

Default org URL (self-serve sign-in)

https://customers.lomi.africa/o/{your-org-slug}

Customers enter the email or phone used at checkout. No merchant API call required. Copy this URL from Settings → Checkout → Storefront → Customer portal.

Pre-authenticated launch (merchant app)

When the customer is already identified in your app, create a launch session server-side and redirect to launch_url. If the session includes a customer_id, the portal skips the second OTP (trusted launch).

import { CustomersService } from "@lomi/sdk";

const { launch_url } = await CustomersService.createPortalLaunchSession(customerId, {
  return_url: "https://your-app.com/account",
});
redirect(launch_url);

Next.js example (app/billing/route.ts):

import { redirect } from "next/navigation";
import { CustomersService } from "@lomi/sdk";

export async function GET() {
  const customerId = await getLoggedInCustomerId();
  const { launch_url } = await CustomersService.createPortalLaunchSession(customerId, {});
  redirect(launch_url);
}

Transactional emails

Customer receipt emails include your org portal URL (customers.lomi.africa/o/{slug}) so buyers can return from their inbox.

Headless API (optional)

For custom portal UIs, use the portal session bearer token from a trusted launch or OTP flow:

  • GET /customer-portal/me
  • GET /customer-portal/transactions
  • GET /customer-portal/subscriptions
  • POST /customer-portal/subscriptions/{id}/actions
  • GET /customer-portal/payment-methods
  • POST /customer-portal/payment-methods/setup-intent
  • POST /customer-portal/payment-methods/{id}/default
  • DELETE /customer-portal/payment-methods/{id}
  • POST /customer-portal/subscriptions/{id}/retry-payment

Pass Authorization: Bearer <portal_session_token>.

Payment methods

When allow_payment_method_update is enabled in portal policy (default: on), customers can:

  1. Create a card setup intent via POST /customer-portal/payment-methods/setup-intent
  2. Confirm the setup intent client-side with your card payment SDK
  3. Persist the card with your attach flow (hosted portal calls the attach edge function after confirmation)

Use POST /customer-portal/payment-methods/{id}/default to set the default card and sync active subscriptions. Use DELETE /customer-portal/payment-methods/{id} to remove a card (blocked when it is the sole card on a billable subscription).

Failed payment recovery

For past_due card subscriptions, POST /customer-portal/subscriptions/{id}/retry-payment attempts an off-session charge with the default saved card. On success, renewal is recorded and dunning is cleared. On failure, the response may include a checkout_url fallback (same path as automated renewal dunning).

The hosted portal also exposes Retry payment alongside Pay now on subscription actions.

Dashboard integration

From Settings → Checkout → Storefront, configure the customer portal:

  • Allow pause / resume / cancel
  • Email magic link and SMS OTP sign-in
  • Payment method updates: allow customers to add, remove, and set default cards
  • Return URL allowlist: required when using return_url or flow_after_completion_url from your backend

From Customers, open a customer with billing history and click Open customer portal to launch the hosted flow in a new tab (same as the API launch session).

Subscription cancellation

Customer-initiated cancellation is at period end by default: access continues until next_billing_date, then the subscription moves to cancelled. Customers can uncancel before that date from the portal.

Schedule finalize_cancel_at_period_end_subscriptions() daily (e.g. pg_cron) so period-end cancellations are applied automatically.

Security controls

  • Launch tokens are one-time and short-lived (15 min)
  • Tokens are hashed at rest
  • OTP/magic challenges are hashed, expiring, and attempt-limited
  • Portal sessions are hashed, revocable, and renewed on activity
  • Requests are organization/customer scoped at SQL function level
  • Audit events recorded for launch, challenge, session, and subscription actions

Production checklist

Required for the hosted portal app (apps/customers):

  • CUSTOMER_PORTAL_COOKIE_SECRET, seals handoff and flow cookies (min 16 characters)
  • NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SECRET_KEY, RPC access and magic-link email

Optional:

  • CUSTOMER_PORTAL_SMS_WEBHOOK_URL, POST { to, body } to your SMS gateway. Without it, OTPs are only logged in non-production.

Launch URLs and magic links default to https://customers.lomi.africa and the request origin respectively. No extra base-URL env vars are required for production.

Also:

  • Ensure merchant backend uses server-side API key storage.
  • Add merchant return URLs to the portal allowlist before using return_url in API calls.
  • Schedule finalize_cancel_at_period_end_subscriptions() daily (included in run_to_prod.sql cron).
  • Monitor launch/session/challenge failures and abuse rates.
  • Keep customer phone/email normalization consistent in your CRM.

Troubleshooting

  • 404 customer not found: wrong customer ID or organization mismatch.
  • 400 invalid flow: unsupported flow_type or missing flow_subscription_id.
  • Launch URL opens expired page: token reused or expired.
  • Customer cannot proceed after contact input: no eligible billing records.

On this page