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
/v1/portal-sessionsAuth: 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)
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
| Field | Type | Default | Description |
|---|---|---|---|
| allowCancel | boolean | true | Customers can cancel their subscriptions |
| allowPause | boolean | true | Customers can pause and resume their subscriptions |
| allowPaymentMethodUpdate | boolean | true | Customers can add or replace a subscription's card |
| returnUrl | string | null | Where the portal's “Back” link goes (http or https) |
| branding | string (JSON) | null | companyName and logoUrl (https) for the portal header; otherwise your merchant name |
Integration Examples
Web (redirect)
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.
