Customer portal

A hosted, token-authenticated portal where your customers manage payment methods, subscriptions, and invoices without signing in.

View as Markdown

Overview

The customer portal is a hosted page where your customers self-serve their billing: update payment methods, view and pay invoices, download invoice PDFs, and change or cancel subscriptions — all without an account or password.

Access is controlled by a portal session token. You create a session with your secret key (server-side), then redirect the customer to the returned URL. Every customer-facing request is authenticated by the session token embedded in the URL — never by your secret key. This makes the customer-facing endpoints safe to call from the browser.

There are two distinct API surfaces:

SurfaceAuthPrefixWho calls it
Session creationSecret key (Authorization: Bearer)/api/{account_id}/billing-portal-sessionsYour server
Customer-facingPortal session token (in the URL path)/api/billing-portal/token/{billing_portal_token}The customer’s browser / the hosted portal

The customer-facing endpoints are documented here for completeness and for teams building a fully custom portal front-end. Most integrations only need to create a session and redirect — PaymentKit’s hosted portal calls these token endpoints for you.


Creating a portal session

Create a session server-side with your secret key. The response contains a billing_portal_url you redirect the customer to, and the token used by every customer-facing endpoint.

$curl -X POST https://app.paymentkit.com/api/{account_id}/billing-portal-sessions \
> -H "Authorization: Bearer sk_live_..." \
> -H "Content-Type: application/json" \
> -d '{
> "customer_id": "cus_abc123",
> "expires_in_hours": 24
> }'

Request body

FieldTypeRequiredDescription
customer_idstringYesExternal ID of the customer (e.g. cus_abc123).
expires_in_hoursintegerNoSession lifetime in hours, between 1 and 720 (30 days). Defaults to 1.

Response

1{
2 "id": "bps_prod_...",
3 "customer_id": "cus_abc123",
4 "billing_portal_url": "https://app.paymentkit.com/customer/billing/BPT_SECURE_TOKEN",
5 "expires_in_hours": 24,
6 "is_expired": false,
7 "created_at": "2026-01-15T12:00:00Z"
8}

The portal token is the final path segment of billing_portal_url (a random, URL-safe string — shown as BPT_SECURE_TOKEN above). It is the {billing_portal_token} used in every customer-facing request below.

Portal links grant access to a customer’s billing data. Send them only through secure channels (email, in-app messages) and keep expires_in_hours as short as your flow allows.


Customer-facing API

All customer-facing endpoints are prefixed with:

https://app.paymentkit.com/api/billing-portal/token/{billing_portal_token}

No Authorization header is required — the token in the path is the credential. Requests against an expired or invalid token return 401, except the session-status endpoint, which stays accessible for expired tokens so the front-end can render an “expired” state with the merchant’s branding.

Session

Get session status

GET /session

Lightweight check that works even for expired tokens. Returns expiry state plus merchant branding so the front-end can decide whether to redirect and how to render the page.

1{
2 "is_expired": false,
3 "account_name": "Acme Inc.",
4 "account_logo": "https://...",
5 "support_email": "support@acme.com"
6}

Get account

GET /account

Returns the merchant account (branding, name, and public details) for the session.

Customer

Get customer

GET /customer

Returns the customer tied to the session.

Update customer

PUT /customer

Update the customer’s contact and billing details. All fields are optional; only the fields you send are changed.

FieldTypeDescription
emailstringPrimary email.
first_namestringFirst name.
last_namestringLast name.
phonestringPhone number.
billing_emailstringEmail invoices are sent to.
business_namestringBusiness/legal name.
languagestringPreferred language code.
tax_idsstring[]Tax identifiers.
addressobjectBilling address.
shipping_addressobjectShipping address.

Payment methods

List payment methods

GET /payment-methods

Returns the customer’s saved payment methods.

Add a payment method

POST /setup-checkout-session

Creates a setup checkout session for securely collecting a new payment method. The response returns a secure_token and the account’s tokenization configuration, which you pass to PaymentKit.js to render the card form.

1{
2 "secure_token": "...",
3 "card_tokenization_mode": "vgs",
4 "vgs_vault_id": "tnt...",
5 "vgs_environment": "sandbox",
6 "supported_payment_methods": ["card"]
7}

Delete a payment method

DELETE /payment-methods/{payment_method_id}

Removes a saved payment method. Returns 204 No Content on success.

Subscriptions

List subscriptions

GET /subscriptions

Get a subscription

GET /subscriptions/{subscription_id}

Get outstanding payment breakdown

GET /subscriptions/{subscription_id}/payment-breakdown

Aggregated totals (subtotal, discount, tax, amount due) across all outstanding invoices on the subscription, so the portal can show a single “amount due” figure before collecting.

Collect payment on a subscription

POST /subscriptions/{subscription_id}/collect

Attempts payment on all outstanding invoices for the subscription using the given payment method.

1{ "payment_method_id": "pm_abc123" }

The response reports an overall payment_status (paid, partially_paid, or failed) plus a per-invoice breakdown in invoice_results.

Update the subscription payment method

PUT /subscriptions/{subscription_id}/payment-method

Sets the default payment method used for the subscription’s future charges.

1{ "payment_method_id": "pm_abc123" }

Cancel a subscription

POST /subscriptions/{subscription_id}/cancel

Cancels the subscription. Returns the updated subscription.

Undo a scheduled cancellation

POST /subscriptions/{subscription_id}/undo-cancel

Reverses a pending cancellation (a subscription set to cancel at period end), keeping it active. Returns the updated subscription.

Changing a subscription (preview then apply)

Plan and quantity changes use a change request lifecycle so the customer sees the exact proration before anything is charged: preview to compute the cost, then apply to collect payment and commit the change.

Preview changes

POST /subscriptions/{subscription_id}/change-requests/preview

Creates (or reuses) a change-request draft, applies the requested changes to it, and returns a full proration preview alongside a change_request_id to apply later.

FieldTypeDescription
change_request_idstringReuse an existing draft instead of creating a new one.
item_changesobject[]Add, update, or remove subscription items.
coupon_changesobject[]Apply or remove coupons.
promotion_codestringPromotion code to apply to the preview.

The response includes prorated line items, discount and tax breakdowns, total_due_atom (the amount that will be charged), and the change_request_id.

Apply a change request

POST /subscriptions/{subscription_id}/change-requests/{request_id}/apply

Applies a previously previewed change request using the charge-first pattern: payment is collected before the change is committed. Returns the updated subscription(s) — a billing-cycle change may split into new subscriptions, which are included in the response.

1{ "payment_method_id": "pm_abc123" }

Notable error responses:

StatusMeaning
402Payment failed — the change was not applied.
409 (STALE_PREVIEW_PERIOD_CHANGED)The billing period moved since the preview; re-preview before applying.
409The change request is in a state that cannot be applied.
404Subscription, change request, or payment method not found.

Cancel a change request

DELETE /subscriptions/{subscription_id}/change-requests/{request_id}

Discards a previewed change request without applying it. Returns 204 No Content.

Invoices

List invoices

GET /invoices

Returns the customer’s invoices, paginated.

Get an invoice

GET /invoices/{invoice_id}

Returns a single invoice with its line items.

Download the invoice PDF

GET /invoices/{invoice_id}/pdf

Returns the PDF URL for a finalized invoice, triggering generation if it isn’t ready yet.

1{ "pdf_url": "https://...", "status": "available" }

When status is generating, the PDF is still being produced — poll again shortly.

Pay an invoice

POST /invoices/{invoice_id}/collect

Collects payment on a single invoice. The response reports payment_status (paid, failed, no_payment_method, or already_paid) and, on success, the invoice PDF URL.


Security

Portal sessions are designed to be safe to expose in the browser:

  • Token-scoped — each session is bound to exactly one customer and one account. The token cannot reach any other customer’s data.
  • Time-limited — sessions expire after expires_in_hours (default 1 hour, max 30 days). Expired tokens return 401 on every endpoint except session-status.
  • No secret key in the browser — customer-facing endpoints authenticate solely via the URL token, so your secret key never leaves your server.
  • Actor attribution — actions taken in the portal (e.g. a self-service cancel) are attributed to the customer in the event log, not to your integration.

Webhooks

Actions taken in the portal emit the same webhook events as any other change, so you can keep your systems in sync:

EventFires when
customer.updatedThe customer updates their information.
customer.subscription.updatedA subscription’s plan or items change.
customer.subscription.cancelledA subscription is cancelled.
invoice.paidAn invoice is paid from the portal.
invoice.payment_failedA portal payment attempt fails.

See Webhooks for the full event list, setup, and signature verification.


Best practices

Link from your app

Add a “Manage billing” button that generates a fresh portal link on demand rather than reusing an old one.

Keep sessions short-lived

Set expires_in_hours to the shortest window your flow needs.

Use webhooks

Listen for portal-driven changes to sync subscriptions and payment methods.

Preview before applying

For plan changes, always preview so the customer sees the exact proration before paying.