Documentation Index Fetch the complete documentation index at: https://docs.waffo.ai/llms.txt
Use this file to discover all available pages before exploring further.
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