Bỏ qua để đến nội dung
Beta · Đặc tả có thể còn thay đổi trước khi ra mắt chính thức. Tên miền API api.hensa.example là tạm thời. Xem thay đổi →

Nhận thanh toán cho website & app

Hướng dẫn này dành cho trang thanh toán của website bán hàng, app đặt món, hệ thống đặt lịch… – bất kỳ nơi nào khách chuyển khoản để trả tiền một đơn.

Khách bấm "Thanh toán"
→ Máy chủ của bạn: POST /payment_requests (amount, externalOrderRef = mã đơn của bạn)
→ Trang thanh toán: hiển thị QR + người nhận + số tiền + đồng hồ đếm ngược
→ Khách chuyển khoản bằng app ngân hàng
→ Hensa: webhook payment_request.paid → máy chủ của bạn xác nhận đơn
→ Trang thanh toán: chuyển sang "Đã thanh toán" (trình duyệt hỏi máy chủ của bạn, không hỏi Hensa)

1. Tạo yêu cầu khi khách chọn chuyển khoản

Phần tiêu đề “1. Tạo yêu cầu khi khách chọn chuyển khoản”
// 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)
  • Đặt externalOrderRef = mã đơn của bạn – nó quay lại trong webhook để bạn tìm đơn ngay.
  • Một đơn = một yêu cầu. Khách tải lại trang? Dùng lại yêu cầu còn hạn thay vì tạo mới.
  • Dùng cùng Idempotency-Key khi thử lại cùng một lần tạo.

Cách nhanh nhất: chuyển khách sang trang thanh toán Hensa ở payUrl – đã có QR, nút mở app ngân hàng trên điện thoại, đếm ngược, trạng thái trực tiếp và biên nhận. Bạn vẫn xác nhận đơn bằng webhook như bước 3.

res.redirect(303, paymentRequest.payUrl)

Tự hiển thị trên trang của bạn: một trang thanh toán chuyển khoản tốt luôn có:

  • Mã QR vẽ từ qrPayload, đủ lớn để quét trên màn hình điện thoại khác.
  • Tên người nhận (receiver.name) và ngân hàng – khách kiểm tra trước khi chuyển, chống tráo mã.
  • Số tiền và nội dung chuyển khoản (referenceCode) kèm nút sao chép – cho khách không quét được mã.
  • Đồng hồ đếm ngược tới expiresAt.

3. Xác nhận đơn bằng webhook – không phải bằng trình duyệt

Phần tiêu đề “3. Xác nhận đơn bằng webhook – không phải bằng trình duyệt”

Chỉ coi đơn là đã thanh toán khi máy chủ của bạn nhận payment_request.paid có chữ ký hợp lệ. Không tin vào việc trình duyệt báo “đã chuyển” – khách có thể đóng trang, mạng có thể chập chờn, và trình duyệt có thể bị giả mạo.

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
})

Trang thanh toán chỉ cần hỏi máy chủ của bạn mỗi vài giây (hoặc dùng WebSocket/SSE của bạn) xem đơn đã chuyển trạng thái chưa.

Tình huống Bạn nhận được Nên làm
Hết 15 phút chưa trả payment_request.expired Nhả hàng đang giữ; cho khách tạo mã mới
Khách chuyển thiếu payment_request.partially_paid (kèm bank_transaction.created) Báo khách chuyển phần còn lại, hoặc xử lý thủ công
Khách chuyển sau khi hết hạn bank_transaction.created (pending) Khớp thủ công với đơn
Khách ghi sai nội dung bank_transaction.created (pending) Khớp thủ công
Webhook của bạn sập vài giờ Hensa thử lại tới ~45 giờ Kết hợp đối soát định kỳ