Next.js integration
Build a full checkout flow with Next.js 16 App Router: server-side session creation, a client-side Watenga.js widget, and webhook verification.
Installation
Install the server SDK and the browser bundle helper. The Node package runs only on the server; Watenga.js loads from the CDN in client components.
npm install @watenga/node @watenga/jsAdd environment variables to .env.local. The NEXT_PUBLIC_ prefix exposes a variable to the browser — use it only for the public key.
WATENGA_SECRET_KEY=sk_test_...
NEXT_PUBLIC_WATENGA_PUBLIC_KEY=pk_test_...
WATENGA_WEBHOOK_SECRET=whsec_...Never expose your secret key
NEXT_PUBLIC_ to WATENGA_SECRET_KEY. Anything prefixed with NEXT_PUBLIC_ is bundled into client JavaScript and visible to visitors.Server-side: create checkout
Create a Route Handler that accepts checkout details from your frontend, calls the Watenga API with your secret key, and returns the session identifiers. Initialise the client outside the handler so it is reused across requests in the same process.
// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import Watenga from '@watenga/node';
const watenga = new Watenga(process.env.WATENGA_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.json();
const checkout = await watenga.checkout.create({
amount: body.amount,
currency: body.currency,
merchantTransactionId: `INV-${body.orderId}`,
returnUrl: `${process.env.NEXT_PUBLIC_URL}/thank-you`,
notificationUrl: `${process.env.NEXT_PUBLIC_URL}/api/webhooks/watenga`,
customerEmail: body.email,
});
return NextResponse.json({
checkoutId: checkout.checkoutId,
paymentUrl: checkout.paymentUrl,
});
}Client-side: embed the widget
Use a client component to load Watenga.js and mount the payment UI. On success, call your own API to record intent — but always wait for the signed webhook before fulfilling the order.
'use client';
import { useEffect, useRef } from 'react';
declare global {
interface Window {
Watenga: (key: string) => {
mount: (
selector: string,
opts: {
amount: number;
currency: string;
reference: string;
onSuccess: (txn: { id: string }) => void;
onError: (err: { message: string }) => void;
},
) => { destroy: () => void };
};
}
}
export function PaymentForm({
amount,
currency,
reference,
}: {
amount: number;
currency: string;
reference: string;
}) {
const mounted = useRef(false);
useEffect(() => {
if (mounted.current) return;
mounted.current = true;
const script = document.createElement('script');
script.src = 'https://js.watenga.africa/v1/watenga.js';
script.async = true;
script.onload = () => {
const w = window.Watenga(process.env.NEXT_PUBLIC_WATENGA_PUBLIC_KEY!);
w.mount('#watenga-payment', {
amount,
currency,
reference,
onSuccess: async () => {
await fetch('/api/orders/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reference }),
});
},
onError: (err) => console.error(err.message),
});
};
document.body.appendChild(script);
}, [amount, currency, reference]);
return <div id="watenga-payment" className="min-h-[320px]" />;
}Handle webhooks
Add a dedicated Route Handler for Watenga events. Read the raw request body as text; parsing JSON before signature verification will break HMAC validation.
// app/api/webhooks/watenga/route.ts
import { Webhooks } from '@watenga/node';
export async function POST(req: Request) {
const rawBody = await req.text();
const signature = req.headers.get('watenga-signature');
const event = Webhooks.constructEvent(
rawBody,
signature,
process.env.WATENGA_WEBHOOK_SECRET!,
);
// Envelope: { id, object: 'event', type, created, data }
if (event.type === 'payment.capture.completed') {
// await db.orders.markPaid(event.data.id)
}
return new Response('ok', { status: 200 });
}Use req.text(), not req.json()
req.text() for signature verification. Parsing the body as JSON before verifying will cause signature failures.TypeScript types
The @watenga/node package ships with full TypeScript types. Import them directly for order models, webhook payloads, and API responses.
import type { Transaction, PaymentLink } from '@watenga/node';Testing in development
Watenga must reach your machine over HTTPS to deliver webhooks. Use ngrok or Cloudflare Tunnel to expose your local Next.js server, then paste the public URL into the Developer Dashboard webhook settings.
ngrok http 3000
# Set webhook URL in Developer Dashboard:
# https://YOUR_SUBDOMAIN.ngrok-free.app/api/webhooks/watengaGet notified when InnBucks and OneMoney go live.
