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.
Luồng tổng thể
Phần tiêu đề “Luồng tổng thể”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-Keykhi thử lại cùng một lần tạo.
2. Hiển thị cho khách
Phần tiêu đề “2. Hiển thị cho khách”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.
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.
4. Các trường hợp không suôn sẻ
Phần tiêu đề “4. Các trường hợp không suôn sẻ”| 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ỳ |