# Nhận kết quả

Hạ Tầng báo cho server của bạn về payment theo ba cách: lời gọi **IPN** tới `ipn_url` của từng payment, **webhook** cho mọi thứ trong doanh nghiệp, và **polling** API bất cứ lúc nào. Trang này so sánh ba cách, cho thấy chính xác một lời gọi IPN trông thế nào, và liệt kê những gì handler của bạn phải kiểm tra.

## Chọn kênh

| | IPN | Webhook | Polling |
|---|---|---|---|
| Phạm vi | Một payment (`ipn_url` khai báo khi tạo) | Mọi thứ trong doanh nghiệp, cả hai môi trường | Bất kỳ thứ gì, bất cứ lúc nào |
| Gửi khi | Mọi thay đổi `payment.*` trừ `created`, và mọi thay đổi `invoice.*` của payment đó | Các loại sự kiện endpoint đăng ký | – |
| Body | `{ id, event_id, type, environment, created_at, data: <payment> }` | `{ type, created_at, data }` | Object |
| Chữ ký | `Hatang-Signature`: HMAC-SHA256 của body với **secret key** | Svix: `svix-id`, `svix-timestamp`, `svix-signature` | API key |
| Thử lại | 1 phút, 5 phút, 15 phút, 1 giờ, 3 giờ, 6 giờ, 12 giờ (8 lần) | 5 giây, 5 phút, 30 phút, 2 giờ, 5 giờ, 10 giờ, 10 giờ (8 lần) | – |
| Timeout | 10 giây | 15 giây | – |
| Nhật ký gửi | `GET /payments/{id}/ipn-deliveries` | `GET /webhooks/{id}/deliveries` | – |

Phần lớn cửa hàng dùng IPN cho đơn hàng, vì IPN mang theo toàn bộ payment và chỉ cần khai báo `ipn_url`. Dùng webhook cho giao dịch không qua đơn (`transaction.received`), sự kiện tài khoản ngân hàng, và mọi thứ bạn muốn nhận cho cả doanh nghiệp. Polling là phương án dự phòng.

## Yêu cầu với endpoint

- URL IPN và webhook phải là **https** trên internet công khai. Các địa chỉ như `localhost`, `10.x`, `192.168.x` hay `169.254.169.254` bị từ chối, cả khi lưu URL lẫn khi Hạ Tầng kết nối.
- Lời gọi được tính là đã gửi khi endpoint trả **2xx**. Redirect không được theo, nên `301` hay `302` đều tính là lỗi.
- Trả lời nhanh và xử lý sau (qua queue). Trả lời chậm bị tính là lỗi và lời gọi sẽ được gửi lại.

## Handler của bạn phải

1. **Xác minh chữ ký trên body gốc**, trước khi parse JSON ([IPN](https://hatang.dev/vi/docs/guides/payments/verify-ipn.md), [webhook](https://hatang.dev/vi/docs/guides/payments/webhooks.md)).
2. **Chống trùng.** `id` (và `event_id`) của IPN và `svix-id` của webhook giữ nguyên qua mọi lần thử lại.
3. **Tin trạng thái, không tin thứ tự đến.** Lời gọi có thể đến sai thứ tự hoặc hai lần. Đọc `data.status` (hoặc gọi `GET /payments/{id}`), và chỉ đưa đơn hàng tiến lên.
4. **Kiểm tra số tiền và môi trường.** So `data.paid_amount` với đơn, và kiểm tra `environment`: payment development không bao giờ được hoàn tất đơn thật.

```ts
// Chỉ đưa đơn tiến lên; bỏ qua mọi lời gọi khiến đơn lùi lại.
const RANK = { pending: 0, expired: 1, partially_paid: 2, paid: 3, overpaid: 3, canceled: 0 } as const;

async function applyIpn(ipn: IpnEvent) {
  if (ipn.environment !== process.env.EXPECTED_ENVIRONMENT) return;
  const order = await db.orders.find(ipn.data.metadata.order_id);
  if (!order || RANK[ipn.data.status] <= RANK[order.paymentStatus]) return;
  if ((ipn.data.status === "paid" || ipn.data.status === "overpaid") && ipn.data.paid_amount >= order.total) {
    await db.orders.markPaid(order.id, ipn.data.id);
  } else {
    await db.orders.setPaymentStatus(order.id, ipn.data.status);
  }
}
```

## Một lời gọi IPN

Đây là những gì endpoint của bạn nhận được:

```http
POST /hatang/ipn HTTP/1.1
Content-Type: application/json
User-Agent: Hatang-IPN/1.0
Hatang-Access-Key: ak_Example0AccessKey0000000
Hatang-Signature: 8f0c3b1e5d7a9c2f4e6b8a0d1c3e5f7a9b2d4f6e8a0c2e4f6b8d0a2c4e6f8b0d
Hatang-Signature-Algorithm: HMAC-SHA256
Hatang-Event: payment.succeeded
Hatang-Delivery-Id: ipn_01a0b90429d87cf89e37fc2f66040ec5
Hatang-Environment: production

{"id":"ipn_01a0b90429d87cf89e37fc2f66040ec5","event_id":"de_01a0b90429b57b41a2ef3510d6bf8036","type":"payment.succeeded","environment":"production","created_at":"2026-09-19T09:34:12.952Z","data":{"object":"payment","id":"pay_01a0b904297873e8b3524a26fdca0ab5","status":"paid","amount":150000,"paid_amount":150000,"code":"HTBF7RDGVQ","metadata":{"order_id":"1001"},"invoice":null, …}}
```

| Header | Ý nghĩa |
|---|---|
| `Hatang-Access-Key` | Lời gọi thuộc tài khoản và môi trường nào (`ak_…`, hoặc `ak_test_…` ở development) |
| `Hatang-Signature` | HMAC-SHA256 dạng hex thường của body gốc |
| `Hatang-Signature-Algorithm` | Luôn là `HMAC-SHA256` |
| `Hatang-Event` | Loại sự kiện, giống `type` trong body |
| `Hatang-Delivery-Id` | Giống `id` trong body; dùng để chống trùng |
| `Hatang-Environment` | `development` hoặc `production` |

| Trường trong body | Ý nghĩa |
|---|---|
| `id` | Mã lần gửi (`ipn_…`), giữ nguyên qua mọi lần thử lại |
| `event_id` | Sự kiện gây ra lời gọi |
| `type` | `payment.succeeded`, `payment.partially_paid`, `payment.overpaid`, `payment.expired`, `payment.canceled`, hoặc một sự kiện `invoice.*` của payment này |
| `environment` | `development` hoặc `production` |
| `created_at` | Thời điểm tạo lời gọi |
| `data` | Toàn bộ [object payment](https://hatang.dev/vi/docs/guides/payments/create-payment.md#response), gồm cả tóm tắt `invoice` |

`data` là payment tại thời điểm tạo lời gọi. Với sự kiện `invoice.*`, `data` vẫn là payment, với trạng thái mới nằm trong `data.invoice`.

## Thử lại và nhật ký gửi

Lời gọi không nhận được `2xx` trong 10 giây sẽ được thử lại sau 1 phút, 5 phút, 15 phút, 1 giờ, 3 giờ, 6 giờ và 12 giờ: 8 lần trong khoảng một ngày. Sau lần cuối, lần gửi chuyển sang `exhausted`. Các lần thử lại được ký bằng secret key hiện hành tại thời điểm gửi.

`GET /payments/{id}/ipn-deliveries` liệt kê 100 lời gọi gần nhất của một payment, mới nhất trước, kèm `status` (`pending`, `success`, `failed`, `exhausted`), `attempt`, `response_status`, `response_ms`, `last_error` và `next_attempt_at`. Trang payment trên dashboard hiển thị cùng danh sách. Nếu endpoint của bạn bị lỗi lâu hơn thời gian thử lại, hãy polling các payment bị ảnh hưởng.

## Polling API

Đọc payment khi bạn cần trạng thái hiện tại, ví dụ khi người mua quay lại `return_url`, hoặc trong job định kỳ rà soát đơn chưa thanh toán.

**cURL**

```bash
curl https://api.hatang.dev/payments/pay_123 \
  -H "Authorization: Bearer $HATANG_API_KEY"
```

**Node.js**

```ts
const { data: check } = await hatang.payments.verify("pay_123", { order_id: "1001", amount: 150_000, environment: "production" });
if (check?.verified) await fulfil("1001");
```

**Python**

```python
p = hatang.payments.get("pay_123")
if p["status"] in ("paid", "overpaid") and p["paid_amount"] >= 150000 and p["environment"] == "production":
    fulfil("1001")
```

**PHP**

```php
$p = json_decode($client->get('/payments/pay_123')->getBody(), true);
if (in_array($p['status'], ['paid', 'overpaid'], true) && $p['paid_amount'] >= 150000 && $p['environment'] === 'production') {
    fulfil('1001');
}
```

Để rà soát nhiều payment, dùng `GET /payments?status=paid,overpaid` và phân trang bằng `after`. Polling được tính vào giới hạn 20 request Thanh toán mỗi giây.

## Bước tiếp theo

- [Xác minh IPN](https://hatang.dev/vi/docs/guides/payments/verify-ipn.md) bằng Node.js, PHP, Python và C#.
- [Webhook](https://hatang.dev/vi/docs/guides/payments/webhooks.md): tạo endpoint và danh sách sự kiện.
- [Lên production](https://hatang.dev/vi/docs/guides/payments/go-live.md).

Source: https://hatang.dev/vi/docs/guides/payments/payment-results
