Bắt đầu nhanh
Mục tiêu: từ con số 0 tới webhook payment_request.paid đầu tiên trong 5 phút, hoàn toàn trong test mode – không cần liên hệ ai, không cần chuyển tiền thật.
-
Lấy khóa test
Phần tiêu đề “Lấy khóa test”Vào Dashboard › Cài đặt › Developer › Tạo khóa, chọn Thử nghiệm. Khóa có dạng
hk_test_…và chỉ hiện một lần – lưu vào biến môi trường:export HENSA_API_KEY="hk_test_..." -
Tạo yêu cầu thanh toán
Phần tiêu đề “Tạo yêu cầu thanh toán”Mỗi đơn hàng một yêu cầu. Gửi kèm
Idempotency-Keyđể lỡ mạng chập chờn gửi lại cũng không tạo trùng.curl https://api.hensa.example/v1/payment_requests \-H "Authorization: Bearer $HENSA_API_KEY" \-H "Idempotency-Key: $(uuidgen)" \-H "Content-Type: application/json" \-d '{ "amount": 150000, "externalOrderRef": "DH-1024" }'// Node.js 18+ (built-in fetch)const res = await fetch('https://api.hensa.example/v1/payment_requests', {method: 'POST',headers: {Authorization: `Bearer ${process.env.HENSA_API_KEY}`,'Idempotency-Key': crypto.randomUUID(), // reuse the same key when you retry'Content-Type': 'application/json'},body: JSON.stringify({ amount: 150000, externalOrderRef: 'DH-1024' })})if (!res.ok) throw new Error((await res.json()).error.code)const paymentRequest = await res.json()console.log(paymentRequest.referenceCode, paymentRequest.qrPayload)<?php$ch = curl_init('https://api.hensa.example/v1/payment_requests');curl_setopt_array($ch, [CURLOPT_POST => true,CURLOPT_RETURNTRANSFER => true,CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('HENSA_API_KEY'),'Idempotency-Key: ' . bin2hex(random_bytes(16)), // reuse the same key when you retry'Content-Type: application/json',],CURLOPT_POSTFIELDS => json_encode(['amount' => 150000, 'externalOrderRef' => 'DH-1024']),]);$paymentRequest = json_decode(curl_exec($ch), true);echo $paymentRequest['referenceCode'];import os, uuid, requestsres = requests.post("https://api.hensa.example/v1/payment_requests",headers={"Authorization": f"Bearer {os.environ['HENSA_API_KEY']}","Idempotency-Key": str(uuid.uuid4()), # reuse the same key when you retry},json={"amount": 150000, "externalOrderRef": "DH-1024"},timeout=10,)res.raise_for_status()payment_request = res.json()print(payment_request["referenceCode"])Kết quả (
201 Created):{"id": "prq_01JD7Q8M4K2X9V3T6B5N0R1W8Y","amount": 150000,"currency": "VND","referenceCode": "LK7F3K9","status": "pending","qrPayload": "00020101021238…","receiver": { "name": "CA PHE MOC", "bankName": "MB Bank", "accountMasked": "•••• 6789" },"externalOrderRef": "DH-1024","expiresAt": "2026-11-03T08:15:00Z","createdAt": "2026-11-03T08:00:00Z"} -
Hiển thị mã QR cho khách
Phần tiêu đề “Hiển thị mã QR cho khách”Vẽ
qrPayloadthành mã QR bằng bất kỳ thư viện QR nào, kèm tên người nhận (receiver.name) và số tiền để khách yên tâm. Mã hết hạn lúcexpiresAt(15 phút). -
Đăng ký webhook
Phần tiêu đề “Đăng ký webhook”Dashboard › Developer › Thêm endpoint → nhập URL HTTPS của bạn, chọn sự kiện
payment_request.paid. Lưu secret kýwhsec_…(chỉ hiện một lần).Chạy trên máy cá nhân? Dùng một công cụ tạo đường hầm HTTPS (vd. ngrok, Cloudflare Tunnel) để có URL công khai trỏ về
localhost. -
Nhận và xác thực webhook
Phần tiêu đề “Nhận và xác thực webhook”server.js import express from 'express'import { verifyHensaSignature } from './verify-hensa-signature.js'const app = express()// Use the RAW body – parsing JSON first changes the bytes and breaks the signature.app.post('/webhooks/hensa', express.raw({ type: 'application/json' }), async (req, res) => {const raw = req.body.toString('utf8')if (!verifyHensaSignature(raw, req.get('Hensa-Signature'), process.env.HENSA_WEBHOOK_SECRET)) {return res.sendStatus(400)}const event = JSON.parse(raw)// Idempotent: the same event id can arrive more than once.if (await db.processedEvents.exists(event.id)) return res.sendStatus(200)if (event.type === 'payment_request.paid') {await orders.markPaid(event.data.externalOrderRef, event.data.id)}await db.processedEvents.insert(event.id)res.sendStatus(200) // answer within 10 s – do slow work in a queue})webhooks/hensa.php <?php// webhooks/hensa.php – plain PHP (works in Laravel/WordPress too: read the raw body, not the parsed array)$payload = file_get_contents('php://input');$header = $_SERVER['HTTP_HENSA_SIGNATURE'] ?? '';$secret = getenv('HENSA_WEBHOOK_SECRET');$t = null; $signatures = [];foreach (explode(',', $header) as $part) {[$k, $v] = array_pad(explode('=', trim($part), 2), 2, null);if ($k === 't') { $t = $v; }if ($k === 'v1') { $signatures[] = $v; }}if (!$t || abs(time() - (int) $t) > 300) { http_response_code(400); exit; }$expected = hash_hmac('sha256', $t . '.' . $payload, $secret);$valid = false;foreach ($signatures as $s) { if (hash_equals($expected, $s)) { $valid = true; break; } }if (!$valid) { http_response_code(400); exit; }$event = json_decode($payload, true);// Idempotent: skip if $event['id'] was already processed.if ($event['type'] === 'payment_request.paid') {mark_order_paid($event['data']['externalOrderRef'], $event['data']['id']);}http_response_code(200);app.py # Flaskimport hashlib, hmac, os, timefrom flask import Flask, request, abortapp = Flask(__name__)SECRET = os.environ["HENSA_WEBHOOK_SECRET"].encode()def verify(raw: bytes, header: str, tolerance: int = 300) -> bool:items = [p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p]t = next((v for k, v in items if k == "t"), None)signatures = [v for k, v in items if k == "v1"]if not t or abs(time.time() - int(t)) > tolerance:return Falseexpected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()return any(hmac.compare_digest(expected, s) for s in signatures)@app.post("/webhooks/hensa")def hensa_webhook():raw = request.get_data() # raw bytes, before any JSON parsingif not verify(raw, request.headers.get("Hensa-Signature", "")):abort(400)event = request.get_json()# Idempotent: skip if event["id"] was already processed.if event["type"] == "payment_request.paid":mark_order_paid(event["data"]["externalOrderRef"], event["data"]["id"])return "", 200Hàm
verifyHensaSignatureở Xác thực chữ ký. -
Mô phỏng khách chuyển khoản
Phần tiêu đề “Mô phỏng khách chuyển khoản”Trong test mode, bạn tự “chuyển tiền” bằng một lệnh – ghi mã tham chiếu vào nội dung để Hensa tự khớp:
curl https://api.hensa.example/v1/test/bank_transfers \-H "Authorization: Bearer $HENSA_TEST_KEY" \-H "Idempotency-Key: $(uuidgen)" \-H "Content-Type: application/json" \-d '{ "amount": 150000, "description": "LK7F3K9 thanh toan don hang", "counterpartyName": "NGUYEN VAN AN" }'Trong vài giây, máy chủ của bạn nhận
bank_transaction.createdrồipayment_request.paid. ĐơnDH-1024đã được thanh toán.
Lên production
Phần tiêu đề “Lên production”- Đổi sang khóa
hk_live_…(biến môi trường riêng cho production) - Tạo endpoint webhook production với URL thật, lưu secret mới
- Xử lý chống trùng theo
event.id– xem Webhook - Chạy đối soát định kỳ làm lưới an toàn
- Xử lý
payment_request.expired(nhả hàng đang giữ)