Customer Portal

Let customers see and manage their subscriptions and billing history through a hosted portal.

Overview

The Customer Portal is a hosted page where your customers can self-manage their subscriptions without you building custom UI. Create a portal session to generate a short-lived URL, then redirect (or open) the customer to it. Your server creates the session; the customer only ever sees the link.

Create a Portal Session

POST/v1/portal-sessions

Auth: Server key required

Request Body

{
  "clientId": 31,
  "returnUrl": "https://yoursite.com/account",
  "allowCancel": true,
  "allowPause": true,
  "allowPaymentMethodUpdate": true,
  "branding": "{\"companyName\":\"Acme Inc.\",\"logoUrl\":\"https://yoursite.com/logo.png\"}"
}

clientIdis your customer's id (from the Customers API). The session opens in the key's mode: a sk_test_ key shows only test subscriptions and invoices.

Response

{
  "portalSessionId": "ps_3f2c9a…",
  "portalUrl": "https://pay.example.com/portal/ps_3f2c9a…?secret=…",
  "expiresAt": "2026-10-10T11:40:00Z"
}

Example (Node.js)

portal-route.ts
app.post('/api/create-portal-session', async (req, res) => {
  const response = await fetch(`${GATEWAY_URL}/v1/portal-sessions`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.QUICKPAY_SECRET_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      clientId: req.user.quickpayClientId,
      returnUrl: 'https://yoursite.com/account',
    }),
  });
  const { portalUrl } = await response.json();
  res.json({ url: portalUrl });
});

The Portal Link

Send the customer to portalUrl straight away. It must be opened within 10 minutes; once opened it stays usable for 30 minutes. The page keeps the secret for that browser tab and removes it from the address bar. When the customer needs the portal again, create a new session.

Security

Anyone holding the link can manage that customer's subscriptions until it expires. Create it only for a signed-in customer, never log it, and never email it: create a fresh session each time the customer asks.

What Customers Can Do

View Subscriptions

Customers see their subscriptions with the plan, amount, status, the card on file and the next payment date.

Add or Update a Card

The customer enters a new card on a hosted card form with 3-D Secure. To check the card, a small temporary hold (1.00 in the plan's currency) is placed and released straight away; it is never charged and never counts as a payment to you. The new card is used for that subscription's renewals, and the customer returns to the portal. The card itself never reaches your systems.

Cancel, Pause and Resume

When you allow it, customers can cancel (the subscription ends at the close of the current billing period), pause billing, or resume a paused subscription.

Billing History

Customers see their past invoices with the date, status and amount.

Options

FieldTypeDefaultDescription
allowCancelbooleantrueCustomers can cancel their subscriptions
allowPausebooleantrueCustomers can pause and resume their subscriptions
allowPaymentMethodUpdatebooleantrueCustomers can add or replace a subscription's card
returnUrlstringnullWhere the portal's “Back” link goes (http or https)
brandingstring (JSON)nullcompanyName and logoUrl (https) for the portal header; otherwise your merchant name

Integration Examples

Web (redirect)

account-page.tsx
function AccountPage() {
  const handleManageSubscription = async () => {
    const res = await fetch('/api/create-portal-session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ customerId: currentUser.customerId }),
    });
    const { url } = await res.json();
    window.location.href = url;
  };

  return (
    <button onClick={handleManageSubscription}>
      Manage Subscription
    </button>
  );
}

Mobile (in-app browser)

import QuickPay

func openPortal() async {
    let session = try await api.createPortalSession(customerId: currentUser.customerId)
    QuickPay.presentPortal(url: session.url) { action in
        switch action {
        case .dismissed:
            refreshAccountData()
        default:
            break
        }
    }
}

Tip

After the customer returns from the portal, refresh your local subscription state by calling the Subscriptions API or relying on webhook events (e.g.subscription.cancelled,subscription.updated) to stay in sync.