Copy this prompt to your AI code editor (Cursor, Copilot, Claude Code, etc.) to set up the integration automatically: Integrate Waffo Pancake payments into my Next.js app using the official TypeScript SDK.
npm install @waffo/pancake-ts
Requirements:
1. Create /app/api/checkout/route.ts — use WaffoPancake client with client.checkout.createSession()
2. Create /app/api/webhooks/waffo/route.ts — use verifyWebhook() from SDK to verify x-waffo-signature
3. Add environment variables: WAFFO_MERCHANT_ID, WAFFO_PRIVATE_KEY, NEXT_PUBLIC_APP_URL
Read https://waffo.mintlify.app/llms-full.txt for full API reference.
What You’ll Build
A complete checkout integration in Next.js including:
Server-side checkout session creation
Client-side redirect to hosted checkout
Webhook handling for payment confirmation
Protected routes based on payment status
Prerequisites
Next.js 13+ with App Router
Waffo Pancake account with API keys
A product created in Dashboard
Project Setup
1. Install Dependencies
npm install @waffo/pancake-ts
2. Environment Variables
# .env.local
WAFFO_MERCHANT_ID = your-merchant-id
WAFFO_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\nMIIE..."
NEXT_PUBLIC_APP_URL = http://localhost:3000
The SDK accepts private keys in multiple formats: PEM, base64, or raw — it auto-normalizes at construction time. Literal \n in .env files works too.
Create Checkout API Route
Create an API route to generate checkout sessions using the SDK:
// app/api/checkout/route.ts
import { NextRequest , NextResponse } from "next/server" ;
import { WaffoPancake , CheckoutSessionProductType , WaffoPancakeError } from "@waffo/pancake-ts" ;
const client = new WaffoPancake ({
merchantId: process . env . WAFFO_MERCHANT_ID ! ,
privateKey: process . env . WAFFO_PRIVATE_KEY ! ,
});
export async function POST ( req : NextRequest ) {
try {
const { productId , email , metadata } = await req . json ();
const session = await client . checkout . createSession ({
storeId: "store_xxx" ,
productId ,
productType: CheckoutSessionProductType . Onetime ,
currency: "USD" ,
buyerEmail: email || undefined ,
metadata ,
successUrl: ` ${ process . env . NEXT_PUBLIC_APP_URL } /success` ,
});
return NextResponse . json ({ checkoutUrl: session . checkoutUrl });
} catch ( error ) {
if ( error instanceof WaffoPancakeError ) {
return NextResponse . json ({ error: error . errors [ 0 ]?. message }, { status: error . status });
}
return NextResponse . json ({ error: "Failed to create checkout" }, { status: 500 });
}
}
The SDK automatically handles request signing and deterministic idempotency keys — no manual header setup needed.
Pricing Page Component
Create a pricing page with checkout buttons:
// app/pricing/page.tsx
'use client' ;
import { useState } from 'react' ;
const plans = [
{
name: 'Starter' ,
price: '$9' ,
period: 'month' ,
productId: 'prod_starter' ,
features: [ '5 projects' , '10GB storage' , 'Email support' ],
},
{
name: 'Pro' ,
price: '$29' ,
period: 'month' ,
productId: 'prod_pro' ,
features: [ 'Unlimited projects' , '100GB storage' , 'Priority support' , 'API access' ],
popular: true ,
},
{
name: 'Enterprise' ,
price: '$99' ,
period: 'month' ,
productId: 'prod_enterprise' ,
features: [ 'Everything in Pro' , 'Custom integrations' , 'Dedicated support' , 'SLA' ],
},
];
export default function PricingPage () {
const [ loading , setLoading ] = useState < string | null >( null );
async function handleCheckout ( productId : string ) {
setLoading ( productId );
try {
const response = await fetch ( '/api/checkout' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ productId }),
});
const { checkoutUrl , error } = await response . json ();
if ( error ) {
alert ( error );
return ;
}
// Redirect to Waffo Pancake checkout
window . location . href = checkoutUrl ;
} catch ( error ) {
alert ( 'Something went wrong' );
} finally {
setLoading ( null );
}
}
return (
< div className = "py-12" >
< h1 className = "text-4xl font-bold text-center mb-12" >
Choose Your Plan
</ h1 >
< div className = "grid md:grid-cols-3 gap-8 max-w-5xl mx-auto px-4" >
{ plans . map (( plan ) => (
< div
key = { plan . name }
className = { `border rounded-lg p-6 ${
plan . popular ? 'border-green-500 ring-2 ring-green-500' : ''
} ` }
>
{ plan . popular && (
< span className = "bg-green-500 text-white text-sm px-3 py-1 rounded-full" >
Most Popular
</ span >
) }
< h2 className = "text-2xl font-bold mt-4" > { plan . name } </ h2 >
< p className = "text-4xl font-bold mt-2" >
{ plan . price }
< span className = "text-lg font-normal" > / { plan . period } </ span >
</ p >
< ul className = "mt-6 space-y-3" >
{ plan . features . map (( feature ) => (
< li key = { feature } className = "flex items-center" >
< CheckIcon className = "w-5 h-5 text-green-500 mr-2" />
{ feature }
</ li >
)) }
</ ul >
< button
onClick = { () => handleCheckout ( plan . productId ) }
disabled = { loading !== null }
className = "w-full mt-8 py-3 px-4 bg-black text-white rounded-lg hover:bg-gray-800 disabled:opacity-50"
>
{ loading === plan . productId ? 'Loading...' : 'Get Started' }
</ button >
</ div >
)) }
</ div >
</ div >
);
}
function CheckIcon ({ className } : { className : string }) {
return (
< svg className = { className } fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M5 13l4 4L19 7" />
</ svg >
);
}
Webhook Handler
Handle payment confirmations using the SDK’s built-in verifyWebhook() — it has embedded public keys for both test and production environments, so you don’t need to manage webhook secrets:
// app/api/webhooks/waffo/route.ts
import { NextResponse } from "next/server" ;
import { verifyWebhook , WebhookEventType } from "@waffo/pancake-ts" ;
export async function POST ( request : Request ) {
const body = await request . text ();
const signature = request . headers . get ( "x-waffo-signature" );
try {
const event = verifyWebhook ( body , signature );
// Respond immediately, process asynchronously
switch ( event . eventType ) {
case WebhookEventType . OrderCompleted :
console . log ( `Order completed: ${ event . data . orderId } ` );
// Update your database, grant access, etc.
break ;
case WebhookEventType . SubscriptionActivated :
console . log ( `Subscription activated: ${ event . data . buyerEmail } ` );
break ;
case WebhookEventType . SubscriptionCanceling :
console . log ( `Subscription canceling: ${ event . data . orderId } ` );
break ;
case WebhookEventType . SubscriptionCanceled :
console . log ( `Subscription canceled: ${ event . data . orderId } ` );
break ;
case WebhookEventType . RefundSucceeded :
console . log ( `Refund succeeded: ${ event . data . amount } ${ event . data . currency } ` );
break ;
}
return NextResponse . json ({ received: true });
} catch {
return new Response ( "Invalid signature" , { status: 401 });
}
}
The SDK’s verifyWebhook() uses embedded public keys — no WAFFO_WEBHOOK_SECRET environment variable needed. It also includes replay protection by default.
Success Page
Show confirmation after successful payment:
// app/success/page.tsx
import { Suspense } from 'react' ;
export default function SuccessPage () {
return (
< Suspense fallback = { < div > Loading... </ div > } >
< SuccessContent />
</ Suspense >
);
}
async function SuccessContent () {
return (
< div className = "min-h-screen flex items-center justify-center" >
< div className = "text-center" >
< div className = "w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4" >
< svg className = "w-8 h-8 text-green-500" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M5 13l4 4L19 7" />
</ svg >
</ div >
< h1 className = "text-2xl font-bold mb-2" > Payment Successful! </ h1 >
< p className = "text-gray-600 mb-6" >
Thank you for your purchase. You now have access to all features.
</ p >
< a
href = "/dashboard"
className = "inline-block bg-black text-white px-6 py-3 rounded-lg hover:bg-gray-800"
>
Go to Dashboard
</ a >
</ div >
</ div >
);
}
Protected Routes Middleware
Protect routes based on subscription status:
// middleware.ts
import { NextResponse } from 'next/server' ;
import type { NextRequest } from 'next/server' ;
export function middleware ( request : NextRequest ) {
// Get user session (implement your auth logic)
const session = request . cookies . get ( 'session' );
// Protected routes
const protectedPaths = [ '/dashboard' , '/settings' , '/projects' ];
const isProtectedPath = protectedPaths . some ( path =>
request . nextUrl . pathname . startsWith ( path )
);
if ( isProtectedPath && ! session ) {
return NextResponse . redirect ( new URL ( '/login' , request . url ));
}
return NextResponse . next ();
}
export const config = {
matcher: [ '/dashboard/:path*' , '/settings/:path*' , '/projects/:path*' ],
};
Server Component: Check Subscription
// app/dashboard/page.tsx
import { redirect } from 'next/navigation' ;
import { getServerSession } from 'your-auth-library' ;
export default async function DashboardPage () {
const session = await getServerSession ();
if ( ! session ) {
redirect ( '/login' );
}
// Get user's subscription from database
const user = await prisma . user . findUnique ({
where: { id: session . user . id },
select: {
plan: true ,
subscriptionActive: true ,
subscriptionEndsAt: true ,
},
});
if ( ! user ?. subscriptionActive ) {
redirect ( '/pricing' );
}
return (
< div >
< h1 > Welcome to your Dashboard </ h1 >
< p > Your current plan: { user . plan } </ p >
{ /* Dashboard content */ }
</ div >
);
}
Customer Portal Link
Let users manage their subscription:
// components/ManageSubscription.tsx
'use client' ;
export function ManageSubscriptionButton ({ email } : { email : string }) {
const portalUrl = `https://checkout.waffo.ai/your-store/portal?email= ${ encodeURIComponent ( email ) } ` ;
return (
< a
href = { portalUrl }
target = "_blank"
rel = "noopener noreferrer"
className = "text-blue-600 hover:underline"
>
Manage Subscription
</ a >
);
}
Complete File Structure
app/
├── api/
│ ├── checkout/
│ │ └── route.ts # Create checkout sessions
│ └── webhooks/
│ └── waffo/
│ └── route.ts # Handle webhooks
├── pricing/
│ └── page.tsx # Pricing page
├── success/
│ └── page.tsx # Success page
├── dashboard/
│ └── page.tsx # Protected dashboard
└── layout.tsx
middleware.ts # Route protection
.env.local # Environment variables
Testing Checklist
Before going live:
Next Steps
Handle Webhooks Deep dive into webhook handling
Subscription Management Advanced subscription features