# Cách người mua thanh toán

Sau khi bạn [tạo payment](https://hatang.dev/vi/docs/guides/payments/create-payment.md), người mua có thể quét VietQR, dùng trang checkout có sẵn, hoặc chuyển khoản thủ công. Trang này mô tả từng cách, các endpoint công khai để tự xây trang thanh toán, và điều gì xảy ra khi người mua chuyển sai.

## Quét VietQR

Mọi app ngân hàng tại Việt Nam đều quét được (NAPAS 247: tức thì, 24/7). QR điền sẵn ngân hàng, số tài khoản, số tiền và mã thanh toán; người mua chỉ cần xác nhận.

<figure className="qr-demo">
  <img src="/img/thanh-toan/vietqr-hatang.svg" alt="Mã VietQR mẫu có logo hatang.dev" width="240" />
  <figcaption>VietQR mẫu: 910452658 · 150.000 ₫ · HTBF7RDGVQ</figcaption>
</figure>

- `qr.image_url`: ảnh SVG, đặt thẳng vào thẻ `<img>`. Ảnh có logo hatang.dev ở giữa và được vẽ ở mức sửa lỗi H, nên logo không ảnh hưởng việc quét. Thêm `?logo=0` để lấy mã trơn, ví dụ khi in kèm thương hiệu của bạn.
- `qr.payload`: chuỗi EMVCo. Render bằng thư viện QR của bạn để tùy biến giao diện.

## Dùng trang checkout có sẵn

<figure className="checkout-single">
  <img src="/img/thanh-toan/checkout.png" alt="Trang checkout Hạ Tầng" width="420" />
</figure>

Chuyển người mua tới `checkout_url`. Trang hiển thị QR, thông tin ngân hàng kèm nút sao chép, và đồng hồ đếm ngược tới `expires_at`. Trang cập nhật trực tiếp khi tiền về (kèm số hóa đơn khi đã có), rồi chuyển người mua về `return_url`. Không cần đăng nhập: payment id là thông tin duy nhất. Ở development, trang có thêm nút giả lập chuyển khoản.

> **Note**
>
> Người mua quay lại `return_url` chỉ có nghĩa là họ đã rời trang checkout. Hãy xác nhận thanh toán ở server trước khi giao hàng (xem [Xác nhận trước khi giao hàng](https://hatang.dev/vi/docs/guides/payments/sdk-javascript.md#confirm-before-you-fulfil)).

## Chuyển khoản thủ công

Hiển thị `bank.name`, `bank.account_number`, `bank.account_name`, số tiền và `code`, rồi nhắc người mua ghi mã vào nội dung chuyển khoản. Cách này dùng được từ mọi ngân hàng, kể cả internet banking trên máy tính. Hãy hiển thị mã thật nổi bật: thiếu mã, giao dịch không khớp được với payment.

## Tự xây trang thanh toán

Các endpoint này không cần API key, nên gọi được từ trình duyệt:

| Endpoint | Trả về |
|---|---|
| `GET /pay/{id}` | Những gì trang checkout hiển thị, và chỉ những gì người mua cần |
| `GET /pay/{id}/qr.svg` | Ảnh VietQR (`?logo=0` để lấy mã trơn) |
| `GET /pay/{id}/events` | Server-sent events, một sự kiện mỗi khi trạng thái đổi |

`GET /pay/{id}` trả về:

```json
{
  "object": "checkout",
  "id": "pay_01a0b904297873e8b3524a26fdca0ab5",
  "environment": "production",
  "status": "pending",
  "amount": 150000,
  "paid_amount": 0,
  "currency": "VND",
  "code": "HTBF7RDGVQ",
  "description": "Đơn hàng #1001",
  "merchant": { "name": "Cửa hàng mẫu" },
  "bank": { "bin": "970422", "name": "MB Bank", "account_number": "910452658", "account_name": "NGUYEN VAN A", "simulated": false },
  "qr": {
    "payload": "00020101021238530010A0000007270123000697042201099104526580208QRIBFTTA530370454061500005802VN62140810HTBF7RDGVQ63044DEC",
    "image_url": "https://api.hatang.dev/pay/pay_01a0b904297873e8b3524a26fdca0ab5/qr.svg"
  },
  "return_url": "https://shop.example.vn/orders/1001",
  "expires_at": "2026-09-19T10:34:12.856Z",
  "paid_at": null,
  "invoice": null
}
```

`bank.simulated` là `true` với tài khoản development tự sinh. `invoice`, khi đơn có yêu cầu hóa đơn, chỉ gồm `requested`, `status`, `number`, `series`, `lookup_code` và `pdf_url`.

Lắng nghe thay đổi trạng thái:

```js
const es = new EventSource(`https://api.hatang.dev/pay/${paymentId}/events`);
es.addEventListener("status", (e) => {
  const { status, paid_amount } = JSON.parse(e.data);
  if (status === "paid" || status === "overpaid") showThankYou(); // chỉ để hiển thị
  if (status === "partially_paid") showRemaining(paid_amount);
});
```

Stream gửi trạng thái hiện tại trước, sau đó một sự kiện `status` cho mỗi lần thay đổi, và một dòng keep-alive mỗi 15 giây. Stream đóng sau 15 phút; mở lại nếu người mua vẫn ở trên trang. Nếu stream bị từ chối (mỗi payment và mỗi server có giới hạn số kết nối trực tiếp), hãy chuyển sang gọi `GET /pay/{id}` vài giây một lần.

| Giới hạn | Giá trị |
|---|---|
| Request tới `/pay/*` mỗi địa chỉ client | 600 mỗi phút (vượt quá nhận `429 rate_limit_exceeded`, kèm `Retry-After`) |
| Module Thanh toán bị tắt | `403 product_disabled` |

> **Danger: Không giao hàng theo trình duyệt**
>
> Ai cũng có thể mở một trang ghi "đã thanh toán". Hãy xác nhận ở server bằng IPN, webhook hoặc `GET /payments/{id}`.

## Khi người mua chuyển sai

| Tình huống | Kết quả |
|---|---|
| Sai số tiền | Payment thành `partially_paid` hoặc `overpaid`. Giao dịch tiếp theo cùng mã sẽ cộng dồn |
| Quên ghi mã | Giao dịch đến dưới dạng `transaction.received` với `payment_id: null`. Kiểm tra rồi [đánh dấu payment đã thanh toán](https://hatang.dev/vi/docs/guides/payments/create-payment.md) |
| Trả sau `expires_at` | Tiền vẫn được ghi nhận và payment chuyển sang `paid` |
| Trả hai lần | Payment thành `overpaid`; hoàn phần chênh lệch từ ngân hàng của bạn |
| Chuyển nhầm tài khoản | Hạ Tầng chỉ thấy tài khoản đã liên kết; payment vẫn chưa thanh toán |

## Bước tiếp theo

- [Nhận kết quả](https://hatang.dev/vi/docs/guides/payments/payment-results.md) trên server.
- [Tạo thanh toán](https://hatang.dev/vi/docs/guides/payments/create-payment.md): trạng thái và tình huống đặc biệt.
- [Hóa đơn điện tử](https://hatang.dev/vi/docs/guides/payments/einvoices.md): trang checkout hiển thị gì về hóa đơn.

Source: https://hatang.dev/vi/docs/guides/payments/buyer-flow
