Skip to main content
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>
  );
}

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:
  • Test checkout flow with test card 4576 7500 0000 0110
  • Verify webhooks are received (check Dashboard logs)
  • Test declined payment with 4576 7500 0000 0220
  • Confirm success page displays correctly
  • Switch to live API keys
  • Update webhook URL to production endpoint

Next Steps

Handle Webhooks

Deep dive into webhook handling

Subscription Management

Advanced subscription features