While building Die0.com — a digital farewell and time capsule platform — we needed to integrate a clean, modern payment experience. We chose Creem.io as our billing engine.
This guide walks through exactly how we connected Creem to a Next.js 14 app, using Prisma, API routes, and secure Webhooks — including all the places we stumbled and recovered.
🎯 Goal & Tech Stack
Objective: Let users purchase paid features on our site.
Stack:
- Frontend: Next.js 14, React, TypeScript
- Backend: Next.js API Routes
- Database: MySQL + Prisma ORM
- Payment: Creem.io
- Deployment: Linux + PM2
⚙️ Initial Setup
1. Install dependencies
npm install @prisma/client
npm install crypto # Node.js built-in for signature verification
`
2. .env configuration
env
CREEM_SECRET_KEY=creem_xxxxxxxxxxxxxx
CREEM_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxx
CREEM_PRODUCT_ID=prod_xxxxxxxxxxxxxx
DATABASE_URL=mysql://user:pass@localhost:3306/db
NEXTAUTH_URL=https://www.die0.com
💡 PM2 Note: If using PM2, remember to use pm2 restart app --update-env.
🧭 Setting Up Creem.io
Create a Product
Copy the product ID into your.env.Configure Webhook
URL:https://www.die0.com/api/webhook/creem
Copy the Webhook Secret to.env.Get API Key
Copy Secret Key to.env.
💾 Prisma Models
`prisma
model ProductOrder {
id String @id @default(uuid())
orderId String @unique
checkoutId String
customerId String
productId String
userId String
amount Float?
status String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id
email String @unique
featureQuota Int @default(0)
productOrders ProductOrder[]
}
`
Then:
bash
npx prisma generate
npx prisma db push
🔌 Backend API: Create Checkout Session
`ts
// app/api/create-checkout-session/route.ts
export async function POST(req: NextRequest) {
const session = { user: { id: 'user_test_id', email: 'test@example.com' } };
const { itemId } = await req.json();
const creemPayload = {
product_id: process.env.CREEM_PRODUCT_ID,
units: 1,
request_id: ${session.user.id}_${Date.now()},
customer: {
email: session.user.email,
},
success_url: https://www.die0.com/payment-status?session_id={SESSION_ID},
metadata: {
user_id: session.user.id,
itemId,
},
};
const response = await fetch('https://api.creem.io/v1/checkout_sessions', {
method: 'POST',
headers: {
Authorization: Bearer ${process.env.CREEM_SECRET_KEY},
'Content-Type': 'application/json',
},
body: JSON.stringify(creemPayload),
});
const responseBody = await response.json();
return NextResponse.json({ payment_url: responseBody.payment_url });
}
`
📡 Webhook Handler
`ts
// app/api/webhook/creem/route.ts
function verifySignature(sig: string, raw: string, secret: string): boolean {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(raw, 'utf8');
return crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(hmac.digest('hex'))
);
}
export async function POST(req: NextRequest) {
const rawBody = await req.text();
const signature = req.headers.get('Creem-Signature') || '';
const secret = process.env.CREEM_WEBHOOK_SECRET;
if (!verifySignature(signature, rawBody, secret)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
const event = JSON.parse(rawBody);
if (event?.eventType === 'checkout.completed') {
const order = event.object.order;
const metadata = event.object.metadata;
const userId = metadata?.user_id;
await prisma.$transaction(async (tx) => {
await tx.productOrder.create({
data: {
orderId: order.id,
checkoutId: event.object.id,
customerId: event.object.customer?.id || '',
productId: process.env.CREEM_PRODUCT_ID!,
userId,
status: 'completed',
},
});
await tx.user.update({
where: { id: userId },
data: {
featureQuota: { increment: 10 },
},
});
});
return NextResponse.json({ message: 'Processed' });
}
return NextResponse.json({ message: 'Ignored' });
}
`
🖥 Frontend Integration (Button & Call)
tsx
const handlePurchase = async (itemId: string) => {
const res = await fetch('/api/create-checkout-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemId }),
});
const data = await res.json();
if (data.payment_url) {
window.location.href = data.payment_url;
}
};
🧪 Debugging & Deployment Checklist
- ✅ Creem Test Mode enabled
- ✅ Webhook tested via
ngrok - ✅ Prisma schema + db migration OK
- ✅ PM2 restarted with
--update-env - ✅
.envproduction values set
🔚 Wrap-up
This payment system now powers Die0’s paid feature unlocks — safely, cleanly, and reliably. If you’re building a digital product with emotional context, visit:
👉 https://www.die0.com
A place for digital farewells that actually last.
Hope this helps you avoid the pitfalls I hit. Let me know what you're building!
Top comments (0)