End-to-End Integration Example
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
Copy
npm install @paymint/nextjs
Project Structure
After integration, your project will look like this:Copy
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.Copy
// app/api/billing/[...path]/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
Copy
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
Copy
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 thePaymintProvider.
Copy
// 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>
);
}
Copy
// 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 theuseBilling and useCheckout hooks to display products and handle checkout.
Copy
// 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.Copy
// 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).Copy
// 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:
Copy
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!
