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