Webhook — thông báo đơn hàng
Đăng ký một endpoint HTTPS để nhận một webhook có chữ ký ngay thời điểm một trong các đơn hàng của bạn được hoàn thành và xác minh on-chain. Thay vì liên tục gửi yêu cầu kiểm tra (polling), bạn tiếp tục luồng xử lý của mình (ví dụ: giải phóng USDT) ngay khi thông báo đến.
Ba sự kiện được gửi đi:
| Sự kiện | Được gửi khi |
|---|---|
delegation.confirmed | Một lượt thuê energy (1h / 5m) được xác nhận on-chain |
bandwidth.delegated | Một đơn hàng bandwidth được hoàn thành |
activation.confirmed | Một lượt kích hoạt địa chỉ được thực thi on-chain |
Trang này trình bày về API quản trị (tạo / liệt kê / chỉnh sửa / xoay vòng secret / xóa các endpoint của bạn) và định dạng của các webhook mà chúng tôi gửi đến bạn.
ℹ️ Vai trò. Bạn quản lý các endpoint của mình tại đây. Việc phân phối được Netts thực hiện bất đồng bộ sau khi đơn hàng được xác minh — không cần phải poll. Chỉ các sự kiện thành công mới được gửi; các trường hợp thất bại và hết thời gian chờ (timeout) sẽ không bao giờ được gửi.
🔒 Mọi hash chúng tôi gửi đều được xác minh on-chain trước. Một webhook chỉ được gửi đi sau khi từng mã băm giao dịch (transaction hash) trong đó được tìm thấy trong một khối. Nếu một hash chưa nằm trong khối, việc phân phối sẽ bị tạm giữ và kiểm tra lại mỗi 30 giây trong tối đa 5 phút; nếu nó không bao giờ vào khối, sẽ không có gì được gửi cho đơn hàng đó. Bạn sẽ không bao giờ nhận được một hash không tồn tại on-chain.
URL cơ sở của endpoint
https://netts.io/apiv2/webhooksTiêu đề Yêu cầu (Request Headers)
| Tiêu đề | Bắt buộc | Mô tả |
|---|---|---|
| Content-Type | Có (đối với POST/PATCH) | application/json |
| X-API-KEY | Có | Khóa API của bạn từ bảng điều khiển Netts |
| X-Real-IP | Có | Địa chỉ IP từ danh sách trắng (whitelist) của bạn |
user_id của bạn được suy ra từ khóa API — bạn không bao giờ phải truyền nó. Bạn chỉ có thể xem và sửa đổi các endpoint của chính bạn.
Endpoint chính và dự phòng
Bạn đăng ký tối đa hai endpoint, và mỗi endpoint có một role:
| Vai trò | Mục đích |
|---|---|
primary | Địa chỉ mà mọi webhook được gửi tới. |
backup | Dự phòng. Chỉ được sử dụng khi việc gửi tới primary thất bại sau khi đã thử lại hết số lần quy định. |
Một đơn hàng được xác nhận duy nhất sẽ tạo ra một webhook duy nhất. Đây không phải là gửi đồng loạt (fan-out): cùng một sự kiện không bao giờ được gửi tới cả hai địa chỉ cùng lúc. Endpoint backup tồn tại để tăng khả năng chịu lỗi — nếu host chính của bạn không thể truy cập hoặc liên tục trả về mã không phải 2xx, việc gửi thông báo sẽ chuyển sang endpoint dự phòng thay vì bị hủy bỏ.
Endpoint đầu tiên bạn tạo sẽ trở thành primary, endpoint thứ hai trở thành backup. Bạn có thể truyền role một cách rõ ràng, hoặc hoán đổi chúng sau này bằng PATCH.
Tại sao không dùng URL riêng cho từng loại thao tác? Bởi vì loại sự kiện được truyền bên trong phần thân (body), trong trường
event. Một trình xử lý, một lần kiểm tra chữ ký, và các loại sự kiện mới sẽ bắt đầu được gửi đến mà không cần bạn phải đăng ký thêm bất cứ điều gì mới.
Quản lý endpoint
Tạo — POST /apiv2/webhooks
Đăng ký một endpoint mới và trả về một secret chỉ hiển thị một lần duy nhất (hãy lưu lại — nó dùng để ký mọi webhook bạn nhận được).
// request body — role is optional
{
"url": "https://your-server.example/netts/delegation-hook",
"role": "primary"
}Nếu bạn bỏ qua role, vai trò còn trống đầu tiên sẽ được chỉ định: primary, sau đó đến backup.
// response 201
{
"detail": {
"code": 10000,
"status": "created",
"data": {
"id": 1,
"url": "https://your-server.example/netts/delegation-hook",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"role": "primary",
"is_active": true,
"created_at": "2026-01-01T00:00:00"
}
}
}Yêu cầu về URL (được xác thực khi tạo và ở mỗi lần chỉnh sửa):
- phải là
https; - phải phân giải thành một địa chỉ công khai — loopback, mạng nội bộ (RFC1918), link-local (bao gồm
169.254.169.254), và các dải không thể định tuyến khác đều bị từ chối; - không chứa thông tin đăng nhập trong URL (
user:pass@…); - độ dài tối đa 2048 ký tự.
URL bị từ chối sẽ trả về 400.
Bạn có thể có hai endpoint — một primary và một backup. Endpoint thứ ba sẽ trả về 409 (4090). Yêu cầu một role đã được sử dụng sẽ trả về 409 (4091) — hãy hoán đổi vai trò bằng PATCH hoặc xóa endpoint hiện có trước.
curl -X POST https://netts.io/apiv2/webhooks \
-H "Content-Type: application/json" \
-H "X-API-KEY: your_api_key" \
-H "X-Real-IP: your_whitelisted_ip" \
-d '{"url": "https://your-server.example/netts/delegation-hook", "role": "primary"}'Liệt kê — GET /apiv2/webhooks
Trả về danh sách endpoint của bạn (secret không bao giờ được trả về ở đây).
{
"detail": {
"code": 10000,
"status": "ok",
"data": {
"endpoints": [
{
"id": 1,
"url": "https://your-server.example/netts/delegation-hook",
"role": "primary",
"is_active": true,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00"
},
{
"id": 2,
"url": "https://backup.example/netts/delegation-hook",
"role": "backup",
"is_active": true,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:00"
}
],
"count": 2,
"max_endpoints": 2,
"roles": ["primary", "backup"]
}
}
}Lấy thông tin một endpoint — GET /apiv2/webhooks/{id}
Cùng định dạng với một phần tử trong danh sách (không có secret). Một id của người khác hoặc không tồn tại sẽ trả về 404.
Chỉnh sửa — PATCH /apiv2/webhooks/{id}
Thay đổi url, is_active và/hoặc role. Gửi bất kỳ tập hợp con nào; phần thân rỗng sẽ trả về 422. Một url bị thay đổi sẽ được xác thực lại (https / SSRF). Một id của người khác hoặc không tồn tại sẽ trả về 404.
// request body (any subset)
{ "url": "https://your-server.example/netts/new-hook", "is_active": false }// response 200
{
"detail": {
"code": 10000,
"status": "updated",
"data": {
"id": 1,
"url": "https://your-server.example/netts/new-hook",
"role": "primary",
"is_active": false,
"created_at": "2026-01-01T00:00:00",
"updated_at": "2026-01-01T00:00:01"
}
}
}Nâng cấp endpoint dự phòng. Gửi {"role": "primary"} tới endpoint dự phòng của bạn sẽ hoán đổi hai vai trò trong một transaction duy nhất — primary cũ sẽ trở thành backup. Bạn sẽ không bao giờ bị rơi vào tình trạng thiếu địa chỉ primary, và không cần gọi API riêng cho endpoint còn lại.
curl -X PATCH https://netts.io/apiv2/webhooks/2 \
-H "Content-Type: application/json" \
-H "X-API-KEY: your_api_key" \
-H "X-Real-IP: your_whitelisted_ip" \
-d '{"role": "primary"}'Đặt
is_active: falseđể tạm dừng gửi mà không xóa endpoint; đặttrueđể tiếp tục. Tạm dừngprimarycủa bạn không làm nâng cấp endpoint backup — việc phân phối vẫn nhắm vào primary. Hãy hoán đổi vai trò nếu bạn muốn endpoint backup tiếp quản.
Xoay vòng secret — POST /apiv2/webhooks/{id}/rotate-secret
Tạo một secret mới và trả về nó một lần duy nhất. Secret mới có hiệu lực ngay lập tức cho các lần phân phối tiếp theo — không cần làm thêm thao tác nào khác. Mỗi endpoint có secret của chính nó: việc xoay vòng secret của primary không làm thay đổi secret của backup.
// response 200
{
"detail": {
"code": 10000,
"status": "rotated",
"data": {
"id": 1,
"secret": "whsec_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
}
}
}Xóa — DELETE /apiv2/webhooks/{id}
Xóa hoàn toàn endpoint và giải phóng vai trò của nó. Trả về 204 (không có body); một id của người khác hoặc không tồn tại sẽ trả về 404.
curl -X DELETE https://netts.io/apiv2/webhooks/1 \
-H "X-API-KEY: your_api_key" -H "X-Real-IP: your_whitelisted_ip"Các webhook chúng tôi phân phối
Khi một trong các đơn hàng của bạn được hoàn thành, Netts gửi một POST tới endpoint primary của bạn. Mọi phần thân đều là application/json (UTF-8); các địa chỉ và hash luôn là giá trị đầy đủ.
Các trường chung cho mọi sự kiện:
| Trường | Loại | Mô tả |
|---|---|---|
event | string | Loại sự kiện — khóa định tuyến cho trình xử lý của bạn |
delivery_id | int | ID phân phối — khóa loại bỏ trùng lặp (dedup key) ở phía bạn. Cũng được gửi trong tiêu đề X-Netts-Delivery. |
order_id | string | ID đơn hàng của bạn |
order_type | string | 1h, 5m, bandwidth hoặc activation |
tx_hashes | string[] | Tất cả các hash giao dịch của thao tác, mỗi hash đều được xác minh on-chain |
confirmed_at | string | Chuỗi UTC ISO-8601 |
delegation.confirmed — thuê energy
{
"event": "delegation.confirmed",
"delivery_id": 1,
"order_id": "1Hxxxxxxxxxx",
"order_type": "1h",
"receive_address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"energy_amount": 65000,
"tx_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"delegation_timestamp": 1700000000000,
"confirmed_at": "2026-01-01T00:00:00Z"
}| Trường | Loại | Mô tả |
|---|---|---|
order_type | string | 1h hoặc 5m |
receive_address | string | Địa chỉ TRON đã nhận energy |
energy_amount | int | Lượng Energy được ủy quyền |
tx_hash | string | Trường cũ (legacy), được giữ lại để tương thích: giống như tx_hashes[0] |
delegation_timestamp | int? | Tùy chọn — chỉ xuất hiện khi được xác nhận qua luồng Mongo |
Ưu tiên sử dụng
tx_hashestrong các tích hợp mới — về nguyên tắc, một đơn hàng có thể được hoàn thành bằng nhiều hơn một giao dịch.tx_hashvẫn sẽ tiếp tục hoạt động.
bandwidth.delegated — đơn hàng bandwidth
{
"event": "bandwidth.delegated",
"delivery_id": 2,
"order_id": "B1Hxxxxxxxxxxxxx",
"order_type": "bandwidth",
"rental_label": "1h",
"rental_seconds": 3600,
"receive_address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"bandwidth_amount": 400,
"fulfillment": "delegated",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"confirmed_at": "2026-01-01T00:00:00Z"
}| Trường | Loại | Mô tả |
|---|---|---|
rental_label / rental_seconds | string / int | Thời hạn thuê, ví dụ: 1h / 3600 |
receive_address | string | Địa chỉ TRON đã nhận bandwidth |
bandwidth_amount | int | Đơn vị Bandwidth (thực nhận) |
fulfillment | string | Cách đơn hàng được hoàn thành — xem bên dưới |
Các giá trị của fulfillment:
| Giá trị | Ý nghĩa | tx_hashes |
|---|---|---|
delegated | Bandwidth được ủy quyền từ pool của chúng tôi | 1+ hash |
trx_send | Được hoàn thành bằng cách gửi TRX tới địa chỉ thay vì ủy quyền | 1+ hash |
already_enough | Địa chỉ đã có sẵn đủ bandwidth khả dụng — không có gì được gửi on-chain | rỗng |
already_enough là trường hợp duy nhất mà tx_hashes rỗng: đơn hàng được đóng thành công, nhưng không có giao dịch nào vì không cần thiết.
activation.confirmed — kích hoạt địa chỉ
{
"event": "activation.confirmed",
"delivery_id": 3,
"order_id": "123456",
"order_type": "activation",
"address": "TXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"activation_type": "ACC_CREATE",
"source": "telegram_bot",
"tx_hashes": ["0000000000000000000000000000000000000000000000000000000000000000"],
"confirmed_at": "2026-01-01T00:00:00Z"
}| Trường | Loại | Mô tả |
|---|---|---|
order_id | string | ID đơn hàng kích hoạt (chuỗi số) |
address | string | Địa chỉ TRON đã được kích hoạt |
activation_type | string | ACC_CREATE (AccountCreateContract) hoặc DIRECT (chuyển khoản TRX) |
source | string | Đánh dấu nguồn gốc. Có thể là thẻ dịch vụ, hoặc ID của đơn hàng energy yêu cầu việc kích hoạt |
Chỉ các lần kích hoạt thực tế mới được gửi đi. Nếu địa chỉ hóa ra đã hoạt động từ trước và không có giao dịch nào được thực hiện, webhook sẽ hoàn toàn không được gửi.
Một đơn hàng energy đồng thời yêu cầu kích hoạt sẽ tạo ra hai webhook — một
activation.confirmedvà mộtdelegation.confirmed. Chúng là các sự kiện riêng biệt với cácdelivery_idriêng biệt; hãy định tuyến chúng dựa trên trườngevent.
Các tiêu đề chúng tôi gửi:
| Tiêu đề | Giá trị |
|---|---|
X-Netts-Event | Loại sự kiện: delegation.confirmed, bandwidth.delegated hoặc activation.confirmed |
X-Netts-Delivery | delivery_id (loại bỏ trùng lặp) |
X-Netts-Timestamp | unix timestamp tính bằng giây tại thời điểm gửi |
X-Netts-Signature | sha256=<hex>, hex = HMAC_SHA256(secret, "<timestamp>." + raw_body) |
User-Agent | netts-webhook/1.0 |
Xác minh chữ ký
Chữ ký tuân theo cơ chế của Stripe (timestamp.body), được tính toán dựa trên các byte thô (raw bytes) mà chúng tôi gửi. Hãy tính toán lại chữ ký bằng secret của bạn, so sánh theo thời gian không đổi (constant-time), và từ chối nếu X-Netts-Timestamp nằm ngoài khoảng thời gian ±5 phút (bảo vệ chống tấn công phát lại - replay protection).
Ký bằng secret của endpoint đã nhận yêu cầu: primary và backup có các secret riêng biệt. Nếu cả hai địa chỉ của bạn được phục vụ bởi cùng một trình xử lý, hãy chọn secret dựa trên URL mà yêu cầu được gửi đến.
import hmac, hashlib, time
def verify(raw_body: bytes, sig_header: str, ts_header: str, secret: str) -> bool:
# freshness (anti-replay)
if abs(time.time() - int(ts_header)) > 300:
return False
signed = f"{ts_header}.".encode() + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_header)
# Flask example:
# ok = verify(request.get_data(),
# request.headers["X-Netts-Signature"],
# request.headers["X-Netts-Timestamp"], SECRET)Ngữ nghĩa phân phối (quan trọng — at-least-once)
Phân phối theo cơ chế ít nhất một lần (at-least-once): một phản hồi bị rớt có thể kích hoạt việc thử lại, do đó bạn có thể nhận được cùng một sự kiện hai lần. Bởi vì hành động nghiệp vụ (giải phóng USDT) liên quan trực tiếp đến tiền tệ:
- Khử trùng lặp (dedup) là bắt buộc — xử lý mỗi sự kiện một cách idempotent dựa theo
delivery_id(và/hoặcorder_id); việc lặp lại không thực hiện thao tác nào (no-op). - Xác minh HMAC trước bất kỳ hành động chuyển tiền nào — không tin cậy phần thân cho đến khi chữ ký khớp và
X-Netts-Timestampcòn mới. - Chỉ trả về 2xx sau khi bạn đã lưu trữ sự kiện một cách an toàn và bền vững — nếu không, chúng tôi sẽ thử lại (một cách chính xác).
Phản hồi 2xx để xác nhận đã nhận; bất kỳ mã nào không phải 2xx / timeout sẽ kích hoạt thử lại.
Thứ tự các lần thử:
- Các lần thử lại sẽ gửi đến endpoint
primarycủa bạn. Khoảng thời gian phụ thuộc vào loại đơn hàng: các đơn hàng5mthử lại trong ~1 phút, tất cả các loại khác trong ~10 phút. - Nếu hết khoảng thời gian này và bạn đã đăng ký một
backup, việc phân phối sẽ chuyển sang đó và lịch trình thử lại bắt đầu lại từ đầu — được ký bằng secret của chính backup. - Chỉ sau khi endpoint backup cũng đã thử hết số lần, việc phân phối mới được đánh dấu là thất bại hoàn toàn (dead).
Cùng một delivery_id được sử dụng xuyên suốt, vì vậy một thông báo ban đầu thất bại trên primary và sau đó thành công trên backup vẫn là một sự kiện duy nhất đối với logic khử trùng lặp của bạn.
Tham chiếu Mã lỗi
| Mã | Mô tả | Mã trạng thái HTTP |
|---|---|---|
10000 | Thành công (created / ok / updated / rotated) | 200 / 201 |
- | Đã xóa (không có body) | 204 |
4000 | URL webhook không hợp lệ / không an toàn (không phải https, private/loopback, chứa thông tin xác thực, quá dài) | 400 |
-1 | Khóa API không hợp lệ / IP không nằm trong whitelist | 401 |
-1 | Không tìm thấy endpoint (hoặc không phải của bạn) | 404 |
4090 | Đã đạt giới hạn endpoint (tối đa 2: primary, backup) | 409 |
4091 | Vai trò được yêu cầu đã được sử dụng — hãy hoán đổi bằng PATCH hoặc xóa endpoint hiện có | 409 |
4220 | Không có gì để cập nhật (PATCH với phần thân rỗng) | 422 |
5003 | Không thể tạo endpoint (vui lòng thử lại) | 503 |
Giới hạn Tần suất (Rate Limits)
Bị giới hạn theo từng khóa API (tiêu đề X-API-KEY):
| Khoảng thời gian | Giới hạn |
|---|---|
| 1 giây | 5 yêu cầu |
| 1 phút | 150 yêu cầu |
Vượt quá Giới hạn Tần suất (429)
{ "message": "API rate limit exceeded" }Ghi chú
- Secret chỉ hiển thị một lần — khi tạo và khi xoay vòng. Nó không bao giờ được trả về qua
GET/LIST. Bị mất? Hãy xoay vòng để nhận một mã mới. - Hai endpoint, không phải fan-out: một
primaryvà mộtbackup. Mỗi đơn hàng được xác nhận sẽ tạo ra một webhook, được phân phối đến primary; backup chỉ được sử dụng nếu primary đã thử hết số lần quy định. - Thay đổi URL không gián đoạn (zero-downtime): đăng ký địa chỉ mới dưới dạng
backup, xác minh nó, sau đóPATCHnó thànhprimary— việc hoán đổi mang tính nguyên tử (atomic). - Tạm dừng:
PATCH … {"is_active": false}dừng phân phối mà không làm mất endpoint. - Chỉ các sự kiện thành công:
delegation.confirmed,bandwidth.delegated,activation.confirmed. Không có sự kiện thất bại — một đơn hàng thất bại hoặc hết thời gian chờ sẽ không tạo ra webhook nào. - Các loại sự kiện mới có thể được thêm vào theo thời gian. Hãy định tuyến theo trường
eventvà bỏ qua các loại bạn chưa xử lý — bạn không bao giờ cần phải đăng ký bất cứ điều gì mới để bắt đầu nhận chúng. - Các hash được xác minh on-chain trước khi gửi (xem ghi chú ở trên cùng): một webhook hoặc là mang các hash đều đã nằm trong một khối, hoặc sẽ không được gửi đi.
- Các URL được xác thực về tính an toàn SSRF khi đăng ký và ở mỗi lần chỉnh sửa; phía phân phối sẽ xác thực lại tại thời điểm gửi.