Skip to main content

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>
  );
}

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