Skip to main content

Command Palette

Search for a command to run...

Accept One-Time Payments in Next.js with Dodo Payments (TypeScript Example)

Published
4 min read
Accept One-Time Payments in Next.js with Dodo Payments (TypeScript 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.

Want to sell digital goods or accept one-time payments in your Next.js app? Dodo Payments makes it easy for Indian startups. Here’s a simple, step-by-step guide using TypeScript and a “Premium PDF Download” example.


1. Setup

First, install the required packages:

npx create-next-app@latest premium-pdf
cd premium-pdf
npm install dodopayments @prisma/client prisma
npx prisma init

Add these to your .env.local:

DODO_PAYMENT_API_KEY=your_dodo_api_key
DODO_WEBHOOK_SECRET=your_dodo_webhook_key
DODO_PDF_PRODUCT_ID=prod_premium_pdf
NEXTAUTH_URL=http://localhost:3000

2. Prisma Schema

For this example, let’s keep it simple:

model User {
  id         String   @id @default(cuid())
  email      String   @unique
  name       String?
  purchases  Purchase[]
}

model Purchase {
  id        String   @id @default(cuid())
  userId    String
  productId String
  paid      Boolean  @default(false)
  user      User     @relation(fields: [userId], references: [id])
}

3. Checkout API Route

When a user clicks “Buy PDF”, 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 purchase record (unpaid for now)
  const purchase = await prisma.purchase.create({
    data: {
      userId,
      productId: process.env.DODO_PDF_PRODUCT_ID!,
      paid: false,
    },
  });

  // 3. Create Dodo payment link (one-time payment)
  const payment = await dodo.payments.create({
    amount: 499, // Rs. 499
    currency: "INR",
    customer: { customer_id: customer.customer_id },
    product_id: process.env.DODO_PDF_PRODUCT_ID!,
    payment_link: true,
    return_url: `${process.env.NEXTAUTH_URL}/pdf/success?purchase_id=${purchase.id}`,
  });

  return NextResponse.json({ url: payment.payment_link });
}

4. Webhook Route: Mark Purchase as Paid

Dodo will call your webhook on payment events. Let’s mark the purchase as paid.

// 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, mark purchase as paid
  if (payload.event_type === "payment.succeeded") {
    // You may want to match by customer_id and product_id
    const purchase = await prisma.purchase.findFirst({
      where: {
        productId: payload.product_id,
        user: { email: payload.customer_email }
      }
    });
    if (purchase) {
      await prisma.purchase.update({
        where: { id: purchase.id },
        data: { paid: true }
      });
    }
  }

  return NextResponse.json({ received: true });
}

5. Frontend: Buy PDF Button

// components/BuyPDFButton.tsx
'use client';
import { useState } from 'react';

export default function BuyPDFButton({ userId, userEmail, userName }: { userId: string, userEmail: string, userName: string }) {
  const [loading, setLoading] = useState(false);

  const handleBuy = 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={handleBuy} disabled={loading}>
      {loading ? 'Redirecting...' : 'Buy Premium PDF (₹499)'}
    </button>
  );
}

6. Download Access: Only for Paid Users

// app/api/pdf/download/route.ts
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";

export async function GET(req: NextRequest) {
  const purchaseId = req.nextUrl.searchParams.get("purchase_id");
  if (!purchaseId) return NextResponse.json({ error: "No purchase ID." }, { status: 400 });

  const purchase = await prisma.purchase.findUnique({ where: { id: purchaseId } });
  if (!purchase?.paid) {
    return NextResponse.json({ error: "Payment not completed." }, { status: 403 });
  }

  // Here, send the PDF file or a secure download link
  return NextResponse.json({ success: true, downloadUrl: "/pdfs/premium.pdf" });
}

7. Testing Webhooks Locally

Use ngrok to expose your local webhook endpoint:

ngrok http 3000

Set your webhook URL in Dodo dashboard to https://YOUR_NGROK_URL/api/webhooks/dodo.


8. Summary

  • Checkout: Create Dodo customer, initiate one-time payment, redirect to payment page.

  • Webhook: On payment success, mark purchase as paid.

  • Download: Only paid users can download the PDF.

  • Frontend: “Buy PDF” button triggers the flow.

This pattern works for any digital product: PDFs, eBooks, templates, etc.


Happy selling! If you found this helpful, share or leave a comment.