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.

bash
npm install @watenga/node @watenga/js

Add environment variables to .env.local. The NEXT_PUBLIC_ prefix exposes a variable to the browser — use it only for the public key.

bash
WATENGA_SECRET_KEY=sk_test_...
NEXT_PUBLIC_WATENGA_PUBLIC_KEY=pk_test_...
WATENGA_WEBHOOK_SECRET=whsec_...

Never expose your secret key

Do not add 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.

typescript
// 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.

typescript
'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.

typescript
// 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()

You must use 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.

typescript
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.

bash
ngrok http 3000
# Set webhook URL in Developer Dashboard:
# https://YOUR_SUBDOMAIN.ngrok-free.app/api/webhooks/watenga

Get notified when InnBucks and OneMoney go live.