Docs

Customer Portal

Create a Stripe customer portal session for managing billing.

Customer Portal

Creates a Stripe customer portal session that allows users to manage their subscription, payment methods, and billing history.

Endpoint

POST /api/billing/portal

Authentication

Required. Bearer token in Authorization header.

Request Body

FieldTypeRequiredDescription
returnUrlstringYesURL to return to after portal session
configurationstringNoStripe portal configuration ID (uses default if not provided)

Example Request

curl -X POST https://api.example.com/api/billing/portal \
  -H "Authorization: Bearer <session_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "returnUrl": "https://app.example.com/dashboard/settings/billing"
  }'

TypeScript Example

const response = await fetch('/api/billing/portal', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${sessionToken}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    returnUrl: `${window.location.origin}/dashboard/settings/billing`
  })
});

const { data } = await response.json();
// Redirect to Stripe customer portal
window.location.href = data.url;

Response

Success Response (200)

{
  "success": true,
  "data": {
    "url": "https://billing.stripe.com/session/_1234567890abcdef"
  }
}
FieldTypeDescription
urlstringStripe customer portal URL

Portal Features

The customer portal allows users to:

  • Update payment methods - Add, remove, or update credit cards
  • View billing history - See past invoices and payment receipts
  • Download invoices - Get PDF copies of invoices
  • Update subscription - Upgrade, downgrade, or cancel plans
  • Update billing info - Change billing address and contact details

Error Responses

No Customer Record (404)

{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "No billing customer record found"
  }
}

Unauthorized (401)

{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Authentication required"
  }
}

React Component Example

import { useState } from 'react';

function ManageBillingButton() {
  const [loading, setLoading] = useState(false);

  const openPortal = async () => {
    setLoading(true);
    try {
      const response = await fetch('/api/billing/portal', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${getToken()}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          returnUrl: window.location.href
        })
      });

      const { data, error } = await response.json();
      
      if (error) {
        throw new Error(error.message);
      }

      // Redirect to Stripe portal
      window.location.href = data.url;
    } catch (err) {
      console.error('Failed to open portal:', err);
      alert('Failed to open billing portal. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  return (
    <button 
      onClick={openPortal}
      disabled={loading}
      className="btn btn-primary"
    >
      {loading ? 'Loading...' : 'Manage Billing'}
    </button>
  );
}

Settings Page Integration

function BillingSettings() {
  const { subscription } = useSubscription();

  return (
    <div className="billing-settings">
      <h2>Billing</h2>
      
      <section className="current-plan">
        <h3>Current Plan</h3>
        <p>{subscription?.plan?.name || 'Free'}</p>
        {subscription?.status === 'active' && (
          <p>Renews on {new Date(subscription.currentPeriodEnd).toLocaleDateString()}</p>
        )}
      </section>

      <section className="actions">
        {subscription?.status === 'active' ? (
          <>
            <ManageBillingButton />
            <p className="text-sm text-gray-500">
              Manage payment methods, view invoices, and update your subscription.
            </p>
          </>
        ) : (
          <UpgradeButton />
        )}
      </section>
    </div>
  );
}

Flow Diagram

┌─────────────┐     POST /portal        ┌─────────────┐
│   Client    │ ───────────────────────>│   Server    │
│  Settings   │                         │             │
│    Page     │ <───────────────────────│             │
│             │      { url }            │             │
└─────────────┘                         └─────────────┘

       │ Redirect to Stripe

┌─────────────┐
│   Stripe    │
│   Portal    │
│  (Embedded) │
└─────────────┘

       │ Return to returnUrl

┌─────────────┐
│   Client    │
│  Settings   │
│    Page     │
└─────────────┘

Notes

  • Portal sessions are valid for 1 hour
  • Users without an active subscription can still access the portal to view past invoices
  • The portal URL is single-use; generate a new one for each visit
  • Custom branding can be configured in the Stripe Dashboard
  • Portal configuration controls which features are available (cancellations, plan switches, etc.)

Webhook Events

When users make changes in the portal, these webhooks are triggered:

EventDescription
customer.subscription.updatedPlan changed or subscription modified
customer.subscription.deletedSubscription cancelled
customer.updatedBilling details updated
payment_method.attachedNew payment method added
payment_method.detachedPayment method removed

On this page