End-to-End Integration Example
π Ship Faster with Vibe Coding Why write boilerplate? Use our ready-made AI Prompts with Cursor, Windsurf, Antigravity, Bolt, or Lovable to generate this entire integration in seconds.Get the Vibe Coding Prompts β
This guide walks through a complete Next.js integration with Paymint, showing both server-side and client-side code.
Prerequisites
A Next.js 13+ app with App Router
Paymint API key (get from dashboard )
An authentication system (Clerk, NextAuth, or Supabase)
Installation
npm install @paymint/nextjs
Project Structure
After integration, your project will look like this:
app/
βββ api/
β βββ billing/
β βββ [...path]/
β βββ route.ts # Server-side API route
βββ providers.tsx # PaymintProvider wrapper
βββ layout.tsx # Root layout with providers
βββ pricing/
β βββ page.tsx # Pricing page with checkout
βββ checkout/
β βββ success/
β βββ page.tsx # Post-checkout success page
βββ account/
βββ page.tsx # Subscription management
Step 1: Create the API Route (Server-Side)
This catch-all route handles all billing operations securely on the server.
// app/api/paddle/route.ts
import { nextRouteHandler } from '@paymint/nextjs/server' ;
import { auth , clerkClient } from '@clerk/nextjs/server' ;
export const { GET , POST } = nextRouteHandler ({
apiKey: process . env . PAYMINT_API_KEY ! ,
getCustomerEmail : async ( req ) => {
// Extract email from authenticated session
const { userId } = await auth ();
if ( ! userId ) return null ;
const client = await clerkClient ();
const user = await client . users . getUser ( userId );
return user . emailAddresses [ 0 ]?. emailAddress || null ;
},
});
Security : Always get the email from your auth session, never from request body or query params!
Alternative: NextAuth
import { getServerSession } from 'next-auth' ;
import { authOptions } from '@/lib/auth' ;
getCustomerEmail : async ( req ) => {
const session = await getServerSession ( authOptions );
return session ?. user ?. email || null ;
}
Alternative: Supabase
import { createClient } from '@/lib/supabase/server' ;
getCustomerEmail : async ( req ) => {
const supabase = createClient ();
const { data : { user } } = await supabase . auth . getUser ();
return user ?. email || null ;
}
Step 2: Set Up the Provider (Client-Side)
Create a providers wrapper that includes the PaymintProvider.
// app/providers.tsx
'use client' ;
import { PaymintProvider } from '@paymint/nextjs' ;
export function Providers ({
children ,
email
} : {
children : React . ReactNode ;
email ?: string ;
}) {
return (
< PaymintProvider
apiRoute = "/api/billing"
customerEmail = { email }
onCheckoutComplete = { ( transactionId ) => {
// Redirect to success page after payment
window . location . href = `/checkout/success?txn= ${ transactionId } ` ;
} }
>
{ children }
</ PaymintProvider >
);
}
Add the provider to your root layout:
// app/layout.tsx
import { Providers } from './providers' ;
import { currentUser } from '@clerk/nextjs/server' ;
export default async function RootLayout ({
children ,
} : {
children : React . ReactNode ;
}) {
const user = await currentUser ();
const email = user ?. emailAddresses [ 0 ]?. emailAddress ;
return (
< html lang = "en" >
< body >
< Providers email = { email } >
{ children }
</ Providers >
</ body >
</ html >
);
}
Step 3: Build the Pricing Page
Use the useBilling and useCheckout hooks to display products and handle checkout.
// app/pricing/page.tsx
'use client' ;
import { useBilling , useCheckout } from '@paymint/nextjs' ;
export default function PricingPage () {
const { products , subscription , loading , hasActiveSubscription } = useBilling ();
const { openCheckout , ready } = useCheckout ();
if ( loading ) {
return (
< div className = "flex items-center justify-center min-h-screen" >
< div className = "animate-spin h-8 w-8 border-4 border-blue-500 rounded-full border-t-transparent" />
</ div >
);
}
// Already subscribed - show current plan
if ( hasActiveSubscription && subscription ) {
return (
< div className = "max-w-2xl mx-auto p-8 text-center" >
< h1 className = "text-2xl font-bold text-green-600" > You're Subscribed! </ h1 >
< p className = "mt-4 text-gray-600" >
Current plan: < strong > { subscription . productName } </ strong >
</ p >
< a
href = "/account"
className = "mt-4 inline-block text-blue-600 hover:underline"
>
Manage Subscription β
</ a >
</ div >
);
}
// Show pricing options
return (
< div className = "max-w-4xl mx-auto p-8" >
< h1 className = "text-3xl font-bold text-center mb-8" > Choose Your Plan </ h1 >
< div className = "grid md:grid-cols-3 gap-6" >
{ products . map ( product => (
< div
key = { product . id }
className = "border rounded-xl p-6 shadow-sm hover:shadow-md transition-shadow"
>
< h2 className = "text-xl font-semibold" > { product . name } </ h2 >
< p className = "text-gray-500 mt-2" > { product . description } </ p >
< div className = "mt-6 space-y-3" >
{ product . prices . map ( price => (
< button
key = { price . id }
onClick = { () => openCheckout ( price . id ) }
disabled = { ! ready }
className = "w-full py-2 px-4 bg-blue-600 text-white rounded-lg
hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
$ { ( price . unitPrice . amount / 100 ). toFixed ( 2 ) } / { price . billingCycle . interval }
</ button >
)) }
</ div >
</ div >
)) }
</ div >
</ div >
);
}
Step 4: Handle Post-Checkout Success
Create a server component to verify the subscription after successful payment.
// app/checkout/success/page.tsx
import { PaymintServer } from '@paymint/nextjs/server' ;
import { auth , clerkClient } from '@clerk/nextjs/server' ;
import { redirect } from 'next/navigation' ;
export default async function CheckoutSuccessPage () {
const { userId } = await auth ();
if ( ! userId ) redirect ( '/sign-in' );
const client = await clerkClient ();
const user = await client . users . getUser ( userId );
const email = user . emailAddresses [ 0 ]?. emailAddress ;
if ( ! email ) redirect ( '/' );
// Fetch latest subscription status
const paymint = new PaymintServer ({
apiKey: process . env . PAYMINT_API_KEY ! ,
});
const billing = await paymint . getBilling ( email );
return (
< div className = "max-w-md mx-auto p-8 text-center" >
< div className = "text-6xl mb-4" > π </ div >
< h1 className = "text-2xl font-bold text-green-600" > Payment Successful! </ h1 >
{ billing . hasActiveSubscription ? (
< div className = "mt-6" >
< p className = "text-gray-600" >
You're now subscribed to { ' ' }
< strong > { billing . activeSubscription ?. items [ 0 ]?. productName } </ strong >
</ p >
< p className = "text-sm text-gray-500 mt-2" >
Status: { billing . activeSubscription ?. status }
</ p >
< a
href = "/dashboard"
className = "mt-6 inline-block bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
>
Go to Dashboard
</ a >
</ div >
) : (
< div className = "mt-6" >
< p className = "text-gray-600" > Processing your subscription... </ p >
< p className = "text-sm text-gray-400 mt-2" >
This may take a few moments. Please refresh the page.
</ p >
</ div >
) }
</ div >
);
}
Subscriptions are created via webhooks. There may be a 1-5 second delay between payment and subscription appearing in the database.
Step 5: Subscription Management
Allow users to manage their subscription (cancel, pause, resume).
// app/account/page.tsx
'use client' ;
import { useSubscription } from '@paymint/nextjs' ;
export default function AccountPage () {
const {
subscription ,
loading ,
hasActiveSubscription ,
isPaused ,
isCanceling ,
cancel ,
pause ,
resume ,
isMutating
} = useSubscription ();
if ( loading ) {
return < div className = "p-8 text-center" > Loading... </ div > ;
}
if ( ! hasActiveSubscription ) {
return (
< div className = "max-w-md mx-auto p-8 text-center" >
< h1 className = "text-xl font-semibold" > No Active Subscription </ h1 >
< a
href = "/pricing"
className = "mt-4 inline-block text-blue-600 hover:underline"
>
View Plans β
</ a >
</ div >
);
}
const handleCancel = async () => {
if ( confirm ( 'Cancel your subscription at the end of this billing period?' )) {
await cancel ({ effectiveFrom: 'next_billing_period' });
}
};
const handlePause = async () => {
if ( confirm ( 'Pause your subscription?' )) {
await pause ({ effectiveFrom: 'next_billing_period' });
}
};
return (
< div className = "max-w-md mx-auto p-8" >
< h1 className = "text-2xl font-bold mb-6" > Your Subscription </ h1 >
< div className = "bg-gray-50 rounded-xl p-6" >
< div className = "flex justify-between items-center" >
< div >
< h2 className = "font-semibold" > { subscription ?. productName } </ h2 >
< p className = "text-sm text-gray-500 capitalize" >
Status: { subscription ?. status }
{ isCanceling && ' (cancels at period end)' }
</ p >
</ div >
</ div >
< div className = "mt-6 space-y-3" >
{ isPaused ? (
< button
onClick = { resume }
disabled = { isMutating }
className = "w-full py-2 px-4 bg-green-600 text-white rounded-lg
hover:bg-green-700 disabled:opacity-50"
>
{ isMutating ? 'Resuming...' : 'Resume Subscription' }
</ button >
) : (
<>
< button
onClick = { handlePause }
disabled = { isMutating || isCanceling }
className = "w-full py-2 px-4 bg-yellow-500 text-white rounded-lg
hover:bg-yellow-600 disabled:opacity-50"
>
Pause Subscription
</ button >
< button
onClick = { handleCancel }
disabled = { isMutating || isCanceling }
className = "w-full py-2 px-4 bg-red-500 text-white rounded-lg
hover:bg-red-600 disabled:opacity-50"
>
{ isCanceling ? 'Cancellation Scheduled' : 'Cancel Subscription' }
</ button >
</>
) }
</ div >
</ div >
</ div >
);
}
Environment Variables
Add your API key to .env.local:
PAYMINT_API_KEY = paymint_test_xxxxx # Use paymint_live_xxx for production
The SDK auto-detects sandbox vs production from your API key prefix - no need for additional configuration!
Whatβs Next?
Next.js SDK Reference Complete API reference for hooks and server utilities
Server SDK Reference Server-side utilities for advanced use cases