> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eholdings.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Nhận và xử lý tin

> Yêu cầu gửi tới bạn, kiểm tra chữ ký, phản hồi, gửi lại.

## Yêu cầu gửi tới bạn

```http theme={null}
POST /webhook HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: webhook-delivery/1
webhook-id: 1470508744885708765
webhook-timestamp: 1791802467
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2/3QMJkp3t1Vsms=

{"type":"payment.transaction.received","id":"1470508744885708765","timestamp":"2026-10-12T17:21:07+07:00","data":{ … }}
```

| Header | Ý nghĩa |
| - | - |
| `webhook-id` | Mã của sự kiện (chuỗi số), **không đổi** qua các lần gửi lại. Dùng để loại tin trùng. |
| `webhook-timestamp` | Thời điểm gửi lần này (giây Unix), **đổi mỗi lần** gửi. |
| `webhook-signature` | Chữ ký, dạng `v1,<base64>`. Khi vừa đổi khoá bí mật, có thể có hai chữ ký cách nhau một dấu cách. |

Thân yêu cầu luôn là JSON `{"type","id","timestamp","data"}`, nhỏ hơn 20 KB. `id` bằng `webhook-id`; `timestamp` là ISO 8601 có múi giờ; số tiền là số nguyên đồng.

> [!WARNING]
>
> `id` và các mã định danh (`transaction_id`, `bank_account_id`) là **chuỗi** số dài tới 19 chữ số. Lưu và so sánh chúng dưới dạng chuỗi: đổi sang kiểu số (JavaScript, Excel) sẽ bị làm tròn thành một mã khác.

## Kiểm tra chữ ký

Luôn kiểm tra chữ ký trên **nội dung gốc** của yêu cầu (chưa qua phân tích JSON) trước khi xử lý. Dùng thư viện Standard Webhooks có sẵn cho nhiều ngôn ngữ; thư viện cũng từ chối tin có `webhook-timestamp` lệch quá 5 phút so với giờ máy bạn.

```js Node.js theme={null}
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…
// rawBody: nội dung gốc, chưa qua JSON.parse
const event = wh.verify(rawBody, {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
});
```

```python Python theme={null}
import os
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["WEBHOOK_SECRET"])  # whsec_…
# raw_body: nội dung gốc, chưa qua json.loads
event = wh.verify(raw_body, request.headers)
```

```go Go theme={null}
import standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"

wh, err := standardwebhooks.NewWebhook(os.Getenv("WEBHOOK_SECRET")) // whsec_…
if err != nil { /* khoá sai định dạng */ }
// rawBody: nội dung gốc, chưa qua json.Unmarshal
if err := wh.Verify(rawBody, r.Header); err != nil {
	http.Error(w, "invalid signature", http.StatusUnauthorized)
	return
}
```

Nếu không dùng thư viện: chữ ký là HMAC-SHA256, mã hoá base64, trên chuỗi `webhook-id + "." + webhook-timestamp + "." + thân yêu cầu`, với khoá là phần base64 giải mã sau tiền tố `whsec_`. So sánh theo thời gian hằng (constant-time).

## Phản hồi của bạn

| Bạn trả về | Hệ thống làm gì |
| - | - |
| Mọi mã `2xx` | Coi là đã nhận. Thân phản hồi không được đọc. |
| `410 Gone` | Coi địa chỉ đã gỡ vĩnh viễn: **tắt webhook ngay**. |
| `429`, `502`, `503`, `504` kèm `Retry-After` | Thử lại, không sớm hơn thời gian bạn yêu cầu (tối đa 1 giờ). |
| Mã khác (`4xx`, `5xx`), hết thời gian chờ, lỗi kết nối hay lỗi chứng chỉ TLS | Thất bại, thử lại theo lịch ở dưới. |
| Chuyển hướng `3xx` | Thất bại: hệ thống **không** đi theo chuyển hướng. Hãy đăng ký địa chỉ cuối cùng. |

Hãy trả `2xx` nhanh (thời gian chờ tối đa 15 giây cho mỗi lượt) và xử lý nặng ở hàng đợi của bạn. Hệ thống không giữ kết nối để dùng lại và không hiển thị nội dung bạn trả về; nhật ký chỉ cho thấy mã HTTP và loại lỗi.

## Gửi lại, trùng tin và thứ tự

* Tin được giao **ít nhất một lần**. Cùng một sự kiện có thể đến nhiều lần (bạn trả lỗi, hết giờ chờ, hay mạng ngắt sau khi bạn đã xử lý). Hệ thống của bạn phải **khử trùng** theo `id` (`webhook-id`), và với giao dịch thì theo `data.transaction_id`. Hãy lưu các mã đã xử lý bền vững: tin có thể được gửi lại sau nhiều ngày.
* **Không bảo đảm thứ tự.** Một sự kiện đến sau có thể xảy ra trước. Dùng `timestamp` trong thân hay `data.transacted_at` để sắp xếp nếu cần.
* Hiếm khi một sự kiện không được ghi ngay lúc xảy ra; hệ thống tự ghi bù trong một lượt đối soát (thường dưới 15 phút, tối đa khoảng hai ngày). Tin bù có `timestamp` là **lúc sự kiện xảy ra**, không phải lúc gửi, và chỉ gửi tới webhook đã có từ trước lúc đó.
* **Lịch thử lại:** lần đầu ngay lập tức, rồi sau 5 giây, 5 phút, 30 phút, 2 giờ, 5 giờ, 10 giờ, 14 giờ, 20 giờ và 24 giờ (từ 5 phút trở đi cộng trừ ngẫu nhiên tới 20%). Tổng cộng **10 lượt trong khoảng 3 ngày**; sau lượt thứ 10, tin chuyển sang **Thất bại**.
* Trong tab **Nhật ký gửi**, dưới trạng thái của tin chưa gửi được có kết quả lượt gửi cuối: mã HTTP kèm ý nghĩa ("429 · Quá nhiều yêu cầu"), hoặc lý do khi máy chủ không trả lời ("Quá thời gian chờ", "Không phân giải được tên miền").
* **Tin thất bại không mất.** Ở tab **Nhật ký gửi** bạn xem được nội dung tin đã gửi và từng lượt gửi (thời gian, mã HTTP hoặc loại lỗi, thời lượng). Bấm **Gửi lại** cho riêng một tin, hoặc **Gửi lại tin thất bại** cho mọi tin "Thất bại" của một webhook đang bật (trong 30 ngày gần nhất, mỗi lần tối đa 1000 tin; tin "Đã huỷ" không được gửi lại). Tin gửi lại giữ nguyên `webhook-id`, có `webhook-timestamp` mới.
* Mỗi lần gửi lại chỉ thử **một lượt**: nếu lượt đó lỗi, tin trở lại Thất bại, không vào lại lịch thử lại.
* **Gửi lại tin thất bại** gửi lần lượt khoảng **1 tin mỗi giây**, để sự kiện mới phát sinh trong lúc đó không phải chờ sau cả đợt. Bấm thêm khi đợt trước chưa xong thì đợt sau xếp tiếp phía sau.
* **Giới hạn mỗi webhook trong một giờ:** **60 lần** gửi lại từng tin (tính cả **Gửi lại tới**, vào webhook nhận) và **5 lần** **Gửi lại tin thất bại** (lần bấm không có tin nào để gửi thì không tính). Vượt giới hạn thì hệ thống từ chối kèm lời nhắc thử lại sau.
* Sự kiện chỉ được tạo cho webhook **đã có** lúc nó xảy ra: địa chỉ mới thêm không tự nhận lại lịch sử. Tin của một webhook **đã xoá** vẫn gửi được bằng tay: mở tin trong **Nhật ký gửi**, chọn ở **Gửi lại tới** một webhook đang bật có nhận loại sự kiện đó, rồi bấm **Gửi lại** (chỉ với sự kiện trong 30 ngày gần nhất). Tin giữ nguyên `webhook-id` và được ký bằng khoá bí mật của webhook nhận. Tin thử không gửi sang webhook khác được, và sự kiện xảy ra lúc cửa hàng chưa có webhook nào thì không được lưu nên không gửi lại được.
* **Thời hạn lưu:** chi tiết từng lượt thử giữ **30 ngày**; tin, trạng thái và kết quả lượt gửi cuối giữ **90 ngày** kể từ khi tin gửi xong hoặc thất bại, sau đó bị xoá và không gửi lại được nữa. Tin còn đang chờ gửi không bị xoá.

## Danh sách kiểm tra khi viết bộ nhận

1. Chỉ đăng ký địa chỉ `https` có chứng chỉ TLS hợp lệ (chứng chỉ tự ký bị từ chối), cổng 443 hoặc 8443.
2. Đọc **thân gốc**, kiểm tra chữ ký, rồi mới phân tích JSON.
3. Trả `2xx` nhanh, xử lý nặng ở nền.
4. Khử trùng theo `webhook-id` hoặc `data.transaction_id`, lưu bền vững.
5. Không giả định thứ tự tin.
6. Không ghi khoá bí mật hay nội dung `content` vào log; giữ khoá trong kho bí mật.
7. Theo dõi tỉ lệ lỗi 24 giờ trên trang Webhook; sửa lỗi trước khi webhook bị tắt tự động.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.