Documentation Index
Fetch the complete documentation index at: https://docs.paymint.dev/llms.txt
Use this file to discover all available pages before exploring further.
@paymint/nextjs
Next.js SDK for Paymint - subscription billing made simple.
Installation
npm install @paymint/nextjs
Quick Start
1. Create API Route
// 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) => {
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:
| Route | Method | Description |
|---|
/api/billing/products | GET | List all products |
/api/billing/subscriptions | GET | Get customer subscriptions |
/api/billing/billing | GET | Get full billing state |
/api/billing/initialize | POST | Initialize Paddle checkout |
/api/billing/cancel/{id} | POST | Cancel subscription |
/api/billing/pause/{id} | POST | Pause subscription |
/api/billing/resume/{id} | POST | Resume subscription |
/api/billing/activate/{id} | POST | Activate 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('user@example.com');
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 Concern | Status |
|---|
| 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 Concern | Requirement |
|---|
getCustomerEmail implementation | Must return email from auth session |
| API key storage | Keep server-side only |
| Auth session security | Use Clerk/NextAuth/Supabase properly |
| HTTPS | Enable 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