Skip to main content

Accept VietQR bank transfers in your app

Ha Tang watches your own bank account. You create a payment with an amount and a transfer code, the buyer scans the VietQR code in their banking app, and Ha Tang matches the incoming transfer and tells your server. No card processor, no money held by a third party.

npm install @hatang/sdk

1. Create the payment and send the buyer to checkout

import { Hatang } from "@hatang/sdk";

const hatang = new Hatang(); // HATANG_API_KEY; HATANG_ENVIRONMENT=development while you build

const { data: payment, error } = await hatang.payments.createOrder({
order_id: "SO-1001", // also the idempotency key
amount: 150_000,
description: "Order #1001",
return_url: "https://shop.example.com/orders/SO-1001",
ipn_url: "https://shop.example.com/api/hatang/ipn",
});
if (error) throw new Error(`${error.name}: ${error.message}`);
redirect(payment.checkout_url); // or draw payment.qr.image_url yourself

2. Receive the IPN (app/api/hatang/ipn/route.ts)

import { constructIpnEvent, HatangSignatureError } from "@hatang/sdk";

export async function POST(req: Request) {
const raw = await req.text(); // the raw body, before any JSON parsing
try {
const ipn = await constructIpnEvent(raw, req.headers, process.env.HATANG_SECRET_KEY!);
if (ipn.data.status === "paid" || ipn.data.status === "overpaid") {
await markOrderPaid(ipn.data.metadata.order_id as string); // idempotently: an IPN can repeat
}
return new Response(null, { status: 200 });
} catch (err) {
if (err instanceof HatangSignatureError) return new Response(null, { status: 401 });
throw err; // a 5xx makes Ha Tang retry
}
}

3. Confirm on the return page

const { data: check } = await hatang.payments.verify(paymentId, { order_id: "SO-1001", amount: 150_000 });
if (check?.verified) await fulfil("SO-1001");

In the development environment no money moves: the checkout page has a button that simulates the transfer, or call hatang.payments.simulateTransfer({ payment_id }).

Go further​

View this page as Markdown