Next.js Integration
Complete guide to integrating ZendFi payments into your Next.js application.
Quick Start
# Option 1: Use our template (fastest)
npx create-zendfi-app my-store --template nextjs-ecommerce
# Option 2: Add to existing Next.js app
cd my-nextjs-app
zendfi init --framework nextjs
Installation
npm install @zendfi/sdk
Environment Setup
Create .env.local:
# Get your keys from https://dashboard.zendfi.tech
ZENDFI_API_KEY=zfi_test_your_key_here
ZENDFI_WEBHOOK_SECRET=your_webhook_secret
# For production
# ZENDFI_API_KEY=zfi_live_your_key_here
Basic Setup
Create SDK Instance
// lib/zendfi.ts
import { ZendFiClient } from '@zendfi/sdk';
export const zendfi = new ZendFiClient({
apiKey: process.env.ZENDFI_API_KEY!,
});
App Router (Next.js 13+)
Create Payment (Server Action)
// app/checkout/actions.ts
'use server';
import { zendfi } from '@/lib/zendfi';
export async function createCheckout(formData: FormData) {
const amount = parseFloat(formData.get('amount') as string);
const email = formData.get('email') as string;
const payment = await zendfi.createPayment({
amount,
currency: 'USD',
token: 'USDC',
customer_email: email,
success_url: `${process.env.NEXT_PUBLIC_URL}/success`,
metadata: {
source: 'nextjs_app',
},
});
return payment.payment_url;
}
Checkout Page
// app/checkout/page.tsx
import { createCheckout } from './actions';
import { redirect } from 'next/navigation';
export default function CheckoutPage() {
async function handleCheckout(formData: FormData) {
'use server';
const paymentUrl = await createCheckout(formData);
redirect(paymentUrl);
}
return (
<form action={handleCheckout}>
<input
type="number"
name="amount"
placeholder="Amount (USD)"
required
/>
<input
type="email"
name="email"
placeholder="Email"
required
/>
<button type="submit">Pay with Crypto</button>
</form>
);
}
API Route
// app/api/checkout/route.ts
import { zendfi } from '@/lib/zendfi';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { amount, email } = await request.json();
const payment = await zendfi.createPayment({
amount,
currency: 'USD',
customer_email: email,
});
return Response.json({
payment_url: payment.payment_url,
payment_id: payment.id,
});
}
Webhooks Handler
// app/api/webhooks/zendfi/route.ts
import { createNextWebhookHandler } from '@zendfi/sdk/nextjs';
import { fulfillOrder } from '@/lib/orders';
export const POST = createNextWebhookHandler({
secret: process.env.ZENDFI_WEBHOOK_SECRET!,
handlers: {
'payment.confirmed': async (payment) => {
// Payment successful - fulfill order
await fulfillOrder({
payment_id: payment.id,
amount: payment.amount,
email: payment.customer_email,
metadata: payment.metadata,
});
console.log(`✅ Order fulfilled for payment ${payment.id}`);
},
'payment.failed': async (payment) => {
// Payment failed - notify user
console.log(`Payment failed: ${payment.id}`);
},
},
});
Pages Router (Next.js 12)
API Route for Payment
// pages/api/create-payment.ts
import { zendfi } from '@/lib/zendfi';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { amount, email } = req.body;
try {
const payment = await zendfi.createPayment({
amount: parseFloat(amount),
currency: 'USD',
customer_email: email,
});
res.status(200).json({
payment_url: payment.payment_url,
payment_id: payment.id,
});
} catch (error) {
res.status(500).json({ error: 'Failed to create payment' });
}
}
Checkout Component
// components/Checkout.tsx
'use client';
import { useState } from 'react';
export function Checkout() {
const [amount, setAmount] = useState('');
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
try {
const res = await fetch('/api/create-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount, email }),
});
const { payment_url } = await res.json();
window.location.href = payment_url;
} catch (error) {
alert('Payment failed');
} finally {
setLoading(false);
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount (USD)"
required
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Processing...' : 'Pay with Crypto'}
</button>
</form>
);
}
Webhooks Handler (Pages Router)
// pages/api/webhooks/zendfi.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { zendfi } from '@/lib/zendfi';
import { getRawBody } from '@/lib/raw-body';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
const signature = req.headers['x-zendfi-signature'] as string;
const rawBody = await getRawBody(req);
// Verify webhook signature
const isValid = zendfi.verifyWebhook({
payload: rawBody,
signature,
secret: process.env.ZENDFI_WEBHOOK_SECRET!,
});
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(rawBody);
// Handle webhook events
switch (event.event_type) {
case 'PaymentConfirmed':
await handlePaymentConfirmed(event.data);
break;
case 'PaymentFailed':
await handlePaymentFailed(event.data);
break;
}
res.status(200).json({ received: true });
}
export const config = {
api: {
bodyParser: false,
},
};
// lib/raw-body.ts
import type { NextApiRequest } from 'next';
export async function getRawBody(req: NextApiRequest): Promise<string> {
return new Promise((resolve, reject) => {
let data = '';
req.on('data', chunk => { data += chunk; });
req.on('end', () => resolve(data));
req.on('error', reject);
});
}
Real-World Examples
E-commerce Product Page
// app/products/[id]/page.tsx
import { zendfi } from '@/lib/zendfi';
import { getProduct } from '@/lib/products';
import { redirect } from 'next/navigation';
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id);
async function buyNow() {
'use server';
const payment = await zendfi.createPayment({
amount: product.price,
description: product.name,
metadata: {
product_id: product.id,
product_name: product.name,
},
success_url: `${process.env.NEXT_PUBLIC_URL}/orders/success?payment_id={PAYMENT_ID}`,
});
redirect(payment.payment_url);
}
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<form action={buyNow}>
<button type="submit">Buy Now</button>
</form>
</div>
);
}
Subscription Checkout
// app/subscribe/[plan]/page.tsx
import { zendfi } from '@/lib/zendfi';
import { PLANS } from '@/lib/plans';
import { redirect } from 'next/navigation';
export default async function SubscribePage({
params
}: {
params: { plan: string }
}) {
const plan = PLANS[params.plan];
async function subscribe(formData: FormData) {
'use server';
const email = formData.get('email') as string;
const subscription = await zendfi.createSubscription({
plan_id: plan.id,
customer_email: email,
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard`,
});
redirect(subscription.payment_url);
}
return (
<div>
<h1>Subscribe to {plan.name}</h1>
<p>${plan.price}/month</p>
<form action={subscribe}>
<input
type="email"
name="email"
placeholder="Your email"
required
/>
<button type="submit">Subscribe</button>
</form>
</div>
);
}
Shopping Cart
// app/cart/checkout/route.ts
import { zendfi } from '@/lib/zendfi';
import { getCart } from '@/lib/cart';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { userId } = await request.json();
const cart = await getCart(userId);
const total = cart.items.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
const payment = await zendfi.createPayment({
amount: total,
description: `Order from My Store`,
metadata: {
user_id: userId,
cart_id: cart.id,
items: JSON.stringify(cart.items),
},
});
return Response.json({ payment_url: payment.payment_url });
}
Testing Locally
1. Start Development Server
npm run dev
2. Listen for Webhooks
# In a new terminal
zendfi webhooks --forward-to http://localhost:3000/api/webhooks/zendfi
3. Create Test Payment
zendfi payment create --amount 10 --open
Deployment Checklist
Vercel
-
Add environment variables in Vercel dashboard:
ZENDFI_API_KEY=zfi_live_your_key_here
ZENDFI_WEBHOOK_SECRET=your_webhook_secret
NEXT_PUBLIC_URL=https://yourapp.com -
Deploy:
vercel --prod -
Configure webhook URL in ZendFi Dashboard:
https://yourapp.com/api/webhooks/zendfi
Other Platforms
Railway, Render, Fly.io:
- Set same environment variables
- Deploy your app
- Update webhook URL in dashboard
Best Practices
1. Error Handling
import { ZendFiError } from '@zendfi/sdk';
try {
const payment = await zendfi.createPayment({
amount: 50,
currency: 'USD',
});
} catch (error) {
if (error instanceof ZendFiError) {
if (error.type === 'validation_error') {
// Handle validation error
} else if (error.type === 'rate_limit_error') {
// Handle rate limit
}
} else {
// Handle other errors
}
}
2. Loading States
'use client';
export function CheckoutButton() {
const [isLoading, setIsLoading] = useState(false);
async function handleClick() {
setIsLoading(true);
try {
const res = await fetch('/api/checkout', { method: 'POST' });
const { payment_url } = await res.json();
window.location.href = payment_url;
} catch (error) {
alert('Failed to create payment');
setIsLoading(false);
}
}
return (
<button onClick={handleClick} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Checkout'}
</button>
);
}
3. TypeScript Types
import type { Payment, Subscription } from '@zendfi/sdk';
interface OrderData {
payment: Payment;
items: CartItem[];
total: number;
}
async function processOrder(data: OrderData) {
// TypeScript knows the shape of Payment
console.log(data.payment.id);
console.log(data.payment.status);
}
Troubleshooting
Webhooks Not Working
# Test webhook locally
zendfi webhooks --forward-to http://localhost:3000/api/webhooks/zendfi
# Check webhook signature in production
# Make sure ZENDFI_WEBHOOK_SECRET is set correctly
Payment Not Creating
# Check API key is set
echo $ZENDFI_API_KEY
# Enable debug mode
DEBUG=zendfi* npm run dev
TypeScript Errors
# Make sure @zendfi/sdk is installed
npm install @zendfi/sdk
# Regenerate types
rm -rf node_modules/.cache
npm run dev
Next Steps
- Complete E-commerce Example
- SaaS Subscription Guide
- TypeScript Guide - Type-safe SDK patterns
- Webhook Events