Language guides

Node.js integration

Create charges from Express or Next.js, handle signed webhooks with a raw body and verify payments before fulfilment.

10 min read

What you will build

A small request helper plus two routes: one that creates a charge at checkout and one that receives webhooks. The examples use the built-in fetch in Node 18+, so there are no SDK dependencies to install.

  • Node.js 18 or newer (built-in fetch and crypto).
  • An API key and webhook secret from Developers in your workspace.
  • Once your webhook URL is live, add it in Developers → Webhooks and keep the secret server-side.

1. Configuration

lib/takaflow.js
const BASE = process.env.TAKAFLOW_BASE;      // https://your-domain.com
const KEY = process.env.TAKAFLOW_API_KEY;

export async function tf(path, body) {
  const res = await fetch(BASE + path, {
    method: "POST",
    headers: {
      "RT-TAKAFLOW-API-KEY": KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });
  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error(data.message || "TakaFlow request failed");
  }
  return data;
}

2. Create a charge

Call the API from your server, never from the browser, so the key stays private. Redirect the customer to the payment_url the API returns.

server.js
import express from "express";
import { tf } from "./lib/takaflow.js";

const app = express();

app.post("/checkout", express.json(), async (req, res, next) => {
  try {
    const order = await createOrder(req.body);          // your database

    const charge = await tf("/api/charge", {
      full_name: req.body.name,
      email: req.body.email,
      amount: Number(req.body.amount),
      description: "Order " + order.id,
      metadata: { order_id: order.id },
      redirect_url: process.env.SITE_URL + "/thanks?order=" + order.id,
      cancel_url: process.env.SITE_URL + "/cart",
    });

    await saveInvoice(order.id, charge.invoice_number);
    res.redirect(charge.payment_url);
  } catch (error) {
    next(error);
  }
});

3. Webhook handler

The signature is computed over the exact bytes NaklaPay sent. Register express.raw before express.json on this route, verify with a constant-time compare, respond 200, then queue your fulfilment work.

webhook.js
import crypto from "node:crypto";

app.post(
  "/webhooks/takaflow",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const expected = "sha256=" + crypto
      .createHmac("sha256", process.env.TAKAFLOW_WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

    const received = req.get("x-takaflow-signature") || "";
    const valid =
      expected.length === received.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));

    if (!valid) return res.status(401).send("invalid signature");

    res.status(200).send("ok");                          // acknowledge first

    const event = JSON.parse(req.body.toString());
    const deliveryId = req.get("x-takaflow-delivery");

    enqueue(function () {                                // your job queue
      if (event.event !== "payment.completed") return;
      return markOrderPaid(deliveryId, {
        orderId: event.metadata.order_id,
        trxId: event.transaction_id,
        amount: event.charged_amount,
      });
    });
  }
);

Keep a unique index on the delivery id (or order id + transaction id) in your database. Retries stop after five attempts, but a duplicate delivery must never fulfil an order twice.

4. Verify before you fulfil

On your success page, confirm the invoice state with the API and check that metadata.order_id belongs to the customer you are serving.

thanks.js
const state = await tf("/api/verify-payment", {
  invoice_id: req.query.invoice_id,
});

if (state.payment_status !== "COMPLETED") {
  return res.redirect("/payment-pending");
}

await markOrderPaid(state.metadata.order_id, {
  trxId: state.transaction_id,
  amount: state.charged_amount,
});
res.render("thanks", { order: state.metadata.order_id });

5. Next.js App Router

In a route handler, read the raw body with request.text() and keep the route on the Node.js runtime so node:crypto is available. The app already deploys to Vercel with this pattern.

src/app/api/takaflow/route.ts
import crypto from "node:crypto";

export const runtime = "nodejs";

export async function POST(request: Request) {
  const raw = await request.text();
  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.TAKAFLOW_WEBHOOK_SECRET!)
    .update(raw)
    .digest("hex");
  const received = request.headers.get("x-takaflow-signature") || "";

  const valid =
    expected.length === received.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
  if (!valid) return new Response("invalid signature", { status: 401 });

  const event = JSON.parse(raw);
  if (event.event === "payment.completed") {
    await markOrderPaid(event.metadata.order_id, event.transaction_id);
  }

  return Response.json({ ok: true });
}

6. Go-live checklist

  • API key and webhook secret exist only in server-side environment variables.
  • Webhook route uses the raw body and a timing-safe comparison.
  • Fulfilment is idempotent and runs off the request path.
  • Success page verifies with /api/verify-payment.
  • Failed webhooks are visible in Developers → Webhooks and retried automatically.