The customer portal endpoint allows authenticated users to access a billing portal where they can manage their subscription, update payment methods, view invoices, and cancel their subscription.
Endpoint
POST /api/payments/portal
Authentication
This endpoint requires authentication. The user must have an active session with a valid JWT token.
Request Body
Optional URL to redirect the user after they’re done in the portal. Defaults to the dashboard.
Example Request
const response = await fetch ( '/api/payments/portal' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ({
returnUrl: 'https://yourdomain.com/dashboard'
})
})
const data = await response . json ()
// Redirect user to portal
window . location . href = data . url
Response
The URL to redirect the user to the billing portal
Success Response (200)
{
"url" : "https://billing.stripe.com/p/session_abc123..."
}
Error Responses
Returned when the user is not authenticated. {
"error" : "Unauthorized"
}
Returned when no customer record exists for the authenticated user. {
"error" : "No customer found"
}
Returned when the customer’s payment provider doesn’t match the active provider or invalid request body. {
"error" : "Customer provider (stripe) does not match active provider (polar)"
}
500 Internal Server Error
Returned when an unexpected error occurs. {
"error" : "Internal Server Error"
}
Implementation Details
The endpoint follows this workflow:
Authentication Check - Verifies the user has an active session
Find Customer - Queries the database for the customer record using userId
Provider Validation - Ensures the customer’s provider matches the currently active payment provider
Create Portal Session - Calls the payment adapter’s createPortal() method
Return URL - Returns the portal URL for client-side redirect
Source Code Reference
The implementation can be found in src/app/api/payments/portal/route.ts:
export async function POST ( req : Request ) {
const session = await auth . api . getSession ({ headers: await headers () })
if ( ! session ) {
return NextResponse . json ({ error: 'Unauthorized' }, { status: 401 })
}
const { returnUrl } = await req . json ()
const adapter = getPaymentAdapter ()
const userCustomer = await db . query . customer . findFirst ({
where: eq ( customer . userId , session . user . id ),
})
if ( ! userCustomer ) {
return NextResponse . json ({ error: 'No customer found' }, { status: 404 })
}
const portalSession = await adapter . createPortal (
userCustomer . providerCustomerId ,
returnUrl
)
return NextResponse . json ( portalSession )
}
Provider Support
All payment providers implement the createPortal() method:
Stripe - Creates a Stripe Billing Portal session
Polar - Returns the Polar dashboard URL
Lemon Squeezy - Creates a customer portal session
Usage Example
Here’s how to use the portal endpoint in a React component:
'use client'
import { useState } from 'react'
export function ManageBillingButton () {
const [ loading , setLoading ] = useState ( false )
const openPortal = async () => {
setLoading ( true )
try {
const response = await fetch ( '/api/payments/portal' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
returnUrl: window . location . href
})
})
const data = await response . json ()
if ( data . url ) {
window . location . href = data . url
}
} catch ( error ) {
console . error ( 'Portal error:' , error )
} finally {
setLoading ( false )
}
}
return (
< button onClick = { openPortal } disabled = { loading } >
{ loading ? 'Loading...' : 'Manage Billing' }
</ button >
)
}
Next Steps