Skip to main content

@paymint/nextjs

Next.js SDK for Paymint - subscription billing made simple.

Installation

npm install @paymint/nextjs

Quick Start

1. Create API Route

// 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) => {
    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;
  },
});

2. Add Provider

// 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 or refresh data
        window.location.href = `/checkout/success?txn=${transactionId}`;
      }}
    >
      {children}
    </PaymintProvider>
  );
}

3. Use Hooks

'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>Loading...</div>;

  return (
    <div>
      {products.map(product => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          {product.prices.map(price => (
            <button
              key={price.id}
              onClick={() => openCheckout(price.id)}
              disabled={!ready}
            >
              Subscribe - ${price.unitPrice.amount / 100}/{price.billingCycle.interval}
            </button>
          ))}
        </div>
      ))}
      
      {hasActiveSubscription && (
        <p>Current plan: {subscription?.productName}</p>
      )}
    </div>
  );
}

4. Create Success Page

// 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
  const paymint = new PaymintServer({
    apiKey: process.env.PAYMINT_API_KEY!,
  });

  const billing = await paymint.getBilling(email);

  return (
    <div className="p-8 text-center">
      <h1 className="text-2xl font-bold text-green-600">Payment Successful!</h1>
      
      {billing.hasActiveSubscription ? (
        <div className="mt-4">
          <p>You're now subscribed to: <strong>{billing.currentSubscription?.items[0]?.priceId}</strong></p>
          <p>Status: {billing.currentSubscription?.status}</p>
          <a href="/dashboard" className="mt-4 inline-block bg-blue-600 text-white px-4 py-2 rounded">
            Go to Dashboard
          </a>
        </div>
      ) : (
        <div className="mt-4">
          <p>Processing your subscription...</p>
          <p className="text-sm text-gray-500">This may take a few moments. Please refresh.</p>
        </div>
      )}
    </div>
  );
}

Imports

// Client-side (hooks, provider)
import { 
  PaymintProvider, 
  useBilling, 
  useProducts, 
  useSubscription, 
  useCheckout 
} from '@paymint/nextjs';

// Server-side (API routes, server components)
import { PaymintServer, nextRouteHandler } from '@paymint/nextjs/server';

// Types
import type { Product, Subscription, Price } from '@paymint/nextjs';

API Routes Created

The nextRouteHandler creates these endpoints automatically:
RouteMethodDescription
/api/billing/productsGETList all products
/api/billing/subscriptionsGETGet customer subscriptions
/api/billing/billingGETGet full billing state
/api/billing/initializePOSTInitialize Paddle checkout
/api/billing/cancel/{id}POSTCancel subscription
/api/billing/pause/{id}POSTPause subscription
/api/billing/resume/{id}POSTResume subscription
/api/billing/activate/{id}POSTActivate trial

Hooks

useBilling()

const {
  products,              // Product[]
  subscription,          // Subscription | null
  loading,               // boolean
  hasActiveSubscription, // boolean
  isOnTrial,             // boolean
  refresh,               // () => Promise<void>
} = useBilling();

useCheckout()

const { openCheckout, ready, loading } = useCheckout();

// Simple
openCheckout('pri_xxx');

// With options
openCheckout({
  items: [{ priceId: 'pri_xxx', quantity: 1 }],
  discountCode: 'SAVE20',
  settings: { theme: 'dark', successUrl: '/checkout/success' },
});

useSubscription()

const { 
  subscription,
  cancel,    // (options?) => Promise<void>
  pause,     // (options?) => Promise<void>
  resume,    // () => Promise<void>
  isMutating,
} = useSubscription();

await cancel({ effectiveFrom: 'next_billing_period' });
await pause({ effectiveFrom: 'next_billing_period' });
await resume();

Post-Checkout Success Handling

After a successful checkout, you have two options:

Option 1: Use onCheckoutComplete Callback

<PaymintProvider 
  apiRoute="/api/billing" 
  customerEmail={email}
  onCheckoutComplete={(transactionId) => {
    // Redirect to success page
    window.location.href = `/checkout/success?txn=${transactionId}`;
  }}
>

Option 2: Use Paddle’s successUrl

openCheckout({
  items: [{ priceId: 'pri_xxx' }],
  settings: { 
    successUrl: '/checkout/success',  // Paddle redirects here after payment
  },
});

Fetching Latest Subscription on Success Page

// app/checkout/success/page.tsx (Server Component)
import { PaymintServer } from '@paymint/nextjs/server';

export default async function SuccessPage() {
  const email = /* get from auth session */;
  
  const paymint = new PaymintServer({
    apiKey: process.env.PAYMINT_API_KEY!,
  });

  // Fetch latest subscription data
  const billing = await paymint.getBilling(email);

  if (billing.hasActiveSubscription) {
    return <div>Welcome! Your subscription is active.</div>;
  }

  // Webhook may not have processed yet - show loading
  return <div>Processing your payment...</div>;
}
Note: Subscriptions are created via Paddle webhooks. There may be a brief delay (1-5 seconds) between payment completion and subscription appearing in the database.

Server-Side Usage

import { PaymintServer } from '@paymint/nextjs/server';

const paymint = new PaymintServer({
  apiKey: process.env.PAYMINT_API_KEY!,
});

// In server components or API routes
const products = await paymint.getProducts();
const billing = await paymint.getBilling('[email protected]');

Auth Integration

Clerk

getCustomerEmail: async (req) => {
  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;
}

NextAuth

import { getServerSession } from 'next-auth';

getCustomerEmail: async (req) => {
  const session = await getServerSession(authOptions);
  return session?.user?.email || null;
}

Supabase

import { createClient } from '@supabase/supabase-js';

getCustomerEmail: async (req) => {
  const supabase = createClient(/* ... */);
  const { data: { user } } = await supabase.auth.getUser();
  return user?.email || null;
}

Security

Developer Responsibilities

The SDK handles most security concerns, but developers must ensure:

1. getCustomerEmail - Get Email from Auth Session (CRITICAL)

// ✅ CORRECT - Email from authenticated session
getCustomerEmail: async (req) => {
  const { userId } = await auth();  // Clerk verifies session
  if (!userId) return null;
  const user = await clerkClient.users.getUser(userId);
  return user.emailAddresses[0]?.emailAddress || null;
}

// ❌ WRONG - Email from request body (VULNERABLE!)
getCustomerEmail: async (req) => {
  const body = await req.json();
  return body.email;  // Attacker can impersonate any user!
}

// ❌ WRONG - Email from query params (VULNERABLE!)
getCustomerEmail: async (req) => {
  return req.nextUrl.searchParams.get('email');  // Attacker can impersonate!
}

2. Keep API Key Server-Side Only

// ✅ CORRECT - Server-side only
apiKey: process.env.PAYMINT_API_KEY!,

// ❌ WRONG - Exposed to client
apiKey: process.env.NEXT_PUBLIC_PAYMINT_API_KEY!,

3. Use HTTPS in Production

Always deploy with HTTPS to protect session cookies.

What Paymint Handles

Security ConcernStatus
API key encryption at rest✓ Handled
Subscription ownership verification✓ Handled
Customer email validation✓ Handled
Paddle API authentication✓ Handled
Webhook signature verification✓ Handled

What Developers Must Handle

Security ConcernRequirement
getCustomerEmail implementationMust return email from auth session
API key storageKeep server-side only
Auth session securityUse Clerk/NextAuth/Supabase properly
HTTPSEnable in production

Environment Variables

PAYMINT_API_KEY=paymint_test_xxx  # or paymint_live_xxx
The SDK auto-detects sandbox vs production from your API key:
  • paymint_test_xxx → Paddle Sandbox
  • paymint_live_xxx → Paddle Production
No need for NEXT_PUBLIC_PADDLE_ENVIRONMENT!

License

MIT