Quickstart
Goal: from zero to your first payment_request.paid webhook in 5 minutes, entirely in test mode – no one to contact, no real transfer.
-
Get a test key
Section titled “Get a test key”Go to Dashboard › Settings › Developer › Create key and choose Test. The key looks like
hk_test_…and is shown once – put it in an environment variable:export HENSA_API_KEY="hk_test_..." -
Create a payment request
Section titled “Create a payment request”One request per order. Send an
Idempotency-Keyso a flaky-network retry never creates a duplicate.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"])Response (
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"} -
Show the QR to the customer
Section titled “Show the QR to the customer”Render
qrPayloadas a QR code with any QR library, together with the receiver name (receiver.name) and the amount so the customer can check before paying. It expires atexpiresAt(15 minutes). -
Register a webhook
Section titled “Register a webhook”Dashboard › Developer › Add endpoint → enter your HTTPS URL and pick
payment_request.paid. Store the signing secretwhsec_…(shown once).Working locally? Use an HTTPS tunnel (e.g. ngrok, Cloudflare Tunnel) to get a public URL pointing at
localhost. -
Receive and verify the webhook
Section titled “Receive and verify the 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 "", 200verifyHensaSignatureis in Verify signatures. -
Simulate the customer’s transfer
Section titled “Simulate the customer’s transfer”In test mode you “transfer” with one command – put the reference code in the description so Hensa matches it:
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" }'Within seconds your server receives
bank_transaction.createdand thenpayment_request.paid. OrderDH-1024is now paid.
Going live
Section titled “Going live”- Switch to an
hk_live_…key (separate environment variable for production) - Create a production webhook endpoint with your real URL and store the new secret
- Handle duplicates by
event.id– see Webhooks - Run periodic reconciliation as a safety net
- Handle
payment_request.expired(release reserved stock)