Skip to main content

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

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/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

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?