Skip to main content

Command Palette

Search for a command to run...

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

Published
4 min read
Dodo Payments Subscription Integration in Next.js (TypeScript): Simple Example
S

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.