Dodo Payments Subscription Integration in Next.js (TypeScript): Simple Example

I'm a passionate developer who loves turning unique ideas into real-life solutions through code. Competitive programming and globe-trotting keep my creativity alive.
If you want to add recurring payments to your Next.js app, Dodo Payments offers a simple API for Indian startups. Let’s see how to set up a monthly subscription for a “Pro Notes” app, where only paid users can save unlimited notes.
1. Setup
Install dependencies:
npx create-next-app@latest pro-notes
cd pro-notes
npm install @prisma/client prisma dodopayments standardwebhooks
npx prisma init
Add these to your .env.local:
DODO_PAYMENT_API_KEY=your_dodo_api_key
DODO_WEBHOOK_SECRET=your_webhook_secret
DODO_PRO_PLAN_ID=prod_pro_notes
NEXTAUTH_URL=http://localhost:3000
2. Prisma Schema
Let’s keep it simple:
model User {
id String @id @default(cuid())
email String @unique
name String?
isPro Boolean @default(false)
dodoCustomerId String?
notes Note[]
}
model Note {
id String @id @default(cuid())
userId String
content String
user User @relation(fields: [userId], references: [id])
}
3. Checkout API Route
When a user clicks “Go Pro”, create a Dodo customer and payment link:
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import DodoPayments from "dodopayments";
import { prisma } from "@/lib/prisma";
export async function POST(req: NextRequest) {
const { userId, userEmail, userName } = await req.json();
// 1. Create Dodo customer
const dodo = new DodoPayments({
bearerToken: process.env.DODO_PAYMENT_API_KEY,
baseURL: "https://test.dodopayments.com",
});
const customer = await dodo.customers.create({ email: userEmail, name: userName });
// 2. Save customer ID to user
await prisma.user.update({
where: { id: userId },
data: { dodoCustomerId: customer.customer_id }
});
// 3. Create Dodo subscription with payment link
const subscription = await dodo.subscriptions.create({
billing: {
city: "City", country: "IN", state: "State", street: "123 Street", zipcode: "123456"
},
customer: { customer_id: customer.customer_id },
product_id: process.env.DODO_PRO_PLAN_ID!,
payment_link: true,
return_url: `${process.env.NEXTAUTH_URL}/pro/success?customer_id=${customer.customer_id}`,
quantity: 1,
});
return NextResponse.json({ url: subscription.payment_link });
}
4. Webhook Route: Activate Pro on Payment
Dodo will call your webhook on payment events. Let’s upgrade the user to Pro when payment succeeds.
// app/api/webhooks/dodo/route.ts
import { NextRequest, NextResponse } from "next/server";
import { Webhook } from "standardwebhooks";
import { prisma } from "@/lib/prisma";
import { headers } from "next/headers";
export async function POST(request: NextRequest) {
const webhook = new Webhook(process.env.DODO_WEBHOOK_SECRET!);
const body = await request.text();
const headersList = await headers();
const webhookHeaders = {
"webhook-id": headersList.get("webhook-id") || "",
"webhook-signature": headersList.get("webhook-signature") || "",
"webhook-timestamp": headersList.get("webhook-timestamp") || "",
};
const payload = await webhook.verify(body, webhookHeaders);
// If payment succeeded, set user as Pro
if (payload.event_type === "subscription.payment_succeeded") {
// Find user by customer_id
const user = await prisma.user.findFirst({
where: { dodoCustomerId: payload.customer_id }
});
if (user) {
await prisma.user.update({
where: { id: user.id },
data: { isPro: true }
});
}
}
return NextResponse.json({ received: true });
}
5. Restrict Notes to Pro Users
Here’s a simple API route to save a note, only if the user is Pro:
// app/api/note/save/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function POST(req: NextRequest) {
const { userId, content } = await req.json();
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user?.isPro) {
return NextResponse.json({ error: "Upgrade to Pro to save unlimited notes." }, { status: 403 });
}
const note = await prisma.note.create({
data: { userId, content }
});
return NextResponse.json({ success: true, note });
}
6. Frontend: Go Pro Button
//components/GoProButton.tsx
'use client';
import { useState } from 'react';
export default function GoProButton({ userId, userEmail, userName }: { userId: string, userEmail: string, userName: string }) {
const [loading, setLoading] = useState(false);
const handleGoPro = async () => {
setLoading(true);
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({ userId, userEmail, userName }),
headers: { 'Content-Type': 'application/json' }
});
const data = await res.json();
window.location.href = data.url; // Redirect to payment page
};
return (
<button onClick={handleGoPro} disabled={loading}>
{loading ? 'Redirecting...' : 'Go Pro'}
</button>
);
}
7. Testing Webhooks Locally
Use ngrok to test webhooks:
ngrok http 3000
Set your webhook URL in Dodo dashboard to https://YOUR_NGROK_URL/api/webhooks/dodo.
8. Summary
Checkout: Create Dodo customer and subscription, redirect to payment.
Webhook: On payment, mark user as Pro.
API: Only Pro users can save unlimited notes.
Frontend: “Go Pro” button triggers the flow.
This pattern works for any SaaS feature gating—swap “notes” for “videos”, “projects”, or anything else!
9. Conclusion
Dodo Payments is a powerful option for Indian SaaS billing, but real-world integration requires careful handling of subscriptions, webhooks, and usage enforcement. With Next.js and Prisma, you can build a robust, scalable system—and now you have a practical, production-ready guide to get started!
Have questions or want to see more? Let me know in the comments or connect with me on LinkedIn!
Happy coding! If you found this helpful, share or leave a comment.





