Webhook là một thông báo đã được ký xác nhận rằng một tác vụ đã đạt đến trạng thái cuối cùng. Nó không phải là bản ghi dữ liệu thực tế. Vì vậy, quy tắc rất ngắn gọn: xác thực các byte thô, loại bỏ trùng lặp theo ID sự kiện, phản hồi 2xx nhanh chóng, sau đó đọc GET /v1/tasks/{id} để lấy kết quả và trạng thái thanh toán.
Webhook tác vụ không gian làm việc (workspace) đã được ra mắt vào ngày 27-09-2026. Bạn có một Management API để quản lý vòng đời webhook, gửi thử nghiệm, xoay vòng secret và xem lịch sử gửi. Bảng điều khiển (Dashboard) và quản lý qua MCP cũng đã khả dụng.
Một đính chính trước: hướng dẫn tạo ảnh bất đồng bộ trước đây của chúng tôi nói rằng TokenLab không có callback tác vụ; điều đó đúng trước ngày 27-09-2026, và hướng dẫn đó đã được cập nhật cùng với bài viết này.
Webhook hay polling? Hãy dùng cả hai
Chúng giải quyết các vấn đề khác nhau và không cái nào thay thế cái nào.
| Tình huống | Nên chọn |
|---|---|
| Bạn muốn phản ứng ngay khi tác vụ kết thúc | Webhook |
| Bạn cần kết quả hoặc chi phí chính xác | GET /v1/tasks/{id} |
| Bộ thu của bạn bị gián đoạn một thời gian | Polling với các ID tác vụ đã lưu |
| Bạn muốn phương án dự phòng khi việc gửi bị mất | Polling với khoảng thời gian chậm |
Webhook không loại bỏ các truy vấn trạng thái và cũng không thêm giới hạn polling. Hãy giữ cả hai. Ngay cả khi đã bật webhook, một vòng lặp đối soát chậm đọc các ID tác vụ đã lưu của bạn là một sự bảo hiểm giá rẻ.
Nếu bạn thực hiện polling, hãy sử dụng poll_url, lùi thời gian (back off) khi tác vụ đang chờ xử lý và dừng lại ở các trạng thái cuối cùng. Dừng lại khi gặp 401, 403, 404, hoặc khi error.retryable == false. Thử lại 503 async_task_owner_unavailable với cơ chế backoff. Tác vụ bị thiếu hoặc hết hạn sẽ trả về 404 async_task_not_found. Xem Hướng dẫn về tác vụ bất đồng bộ và polling để biết hợp đồng polling.
Ba loại thông tin xác thực, ba công việc
Nhầm lẫn giữa các loại này là cách nhanh nhất để làm hỏng bộ thu của bạn.
| Thông tin xác thực | Tiền tố | Chức năng | Ghi chú |
|---|---|---|---|
| Management Token | mt-… |
Tạo, liệt kê, cập nhật, xóa, kiểm tra và xoay vòng webhook trên /v1/management/webhooks* |
Gửi dưới dạng Authorization: Bearer mt-…. Phạm vi không gian làm việc |
| API key | sk-… |
Gửi yêu cầu mô hình và đọc trạng thái tác vụ qua GET /v1/tasks/{id} |
Bị Management API từ chối |
| Signing secret | whsec_… |
Xác thực các lượt gửi trên bộ thu của bạn | Không bao giờ là Bearer token |
Hai điều cần lưu ý về Management Token. Thứ nhất, nó cũng ủy quyền cho các thao tác quản lý không gian làm việc khác, vì vậy nó không phải là thông tin xác thực chỉ dành cho webhook. Hãy chọn cùng không gian làm việc với API key gửi tác vụ của bạn. Thứ hai, bạn tạo nó tại Dashboard → API → Management Tokens. Xem một ví dụ khác về Management API.
Chỉ giữ mt-… và whsec_… trên backend của bạn. Không bao giờ gửi chúng tới trình duyệt hoặc ứng dụng di động.
Tạo endpoint và lưu trữ secret ngay lập tức
Lệnh tạo trả về 201 cùng với id của webhook và một secret dùng một lần bắt đầu bằng whsec_…. Các lệnh liệt kê, lấy thông tin và cập nhật sẽ không bao giờ hiển thị lại secret đó. Hãy lưu nó ngay khi bạn thấy.
export TOKENLAB_MANAGEMENT_TOKEN="mt-your-management-token"
curl https://api.tokenlab.sh/v1/management/webhooks \
-H "Authorization: Bearer $TOKENLAB_MANAGEMENT_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"url":"https://your-app.example/webhooks/tokenlab","events":["task.completed","task.failed","task.timeout"],"description":"Production task results"}'
Các endpoint tương tự có thể được quản lý theo ba cách và cả ba đều chỉnh sửa cùng một đối tượng:
- Dashboard → API → Webhooks
- Management API
- MCP
Các quy tắc về URL rất nghiêm ngặt. Endpoint phải là HTTPS công khai. Không được chứa thông tin xác thực, query string hoặc fragment trong URL. Các lệnh chuyển hướng (redirect) sẽ không được theo sau, vì vậy 301 được tính là một lần gửi thất bại.
Bạn có thể có tối đa 10 endpoint cho mỗi không gian làm việc. Lệnh tạo thứ 11 sẽ trả về 409 webhook_limit_reached.
| Phương thức | Đường dẫn | Mục đích |
|---|---|---|
GET |
/v1/management/webhooks |
Liệt kê các endpoint |
POST |
/v1/management/webhooks |
Tạo một endpoint |
GET |
/v1/management/webhooks/{webhookId} |
Đọc một endpoint |
PATCH |
/v1/management/webhooks/{webhookId} |
Cập nhật, tạm dừng hoặc tiếp tục |
DELETE |
/v1/management/webhooks/{webhookId} |
Xóa |
POST |
/v1/management/webhooks/{webhookId}/rotate-secret |
Xoay vòng signing secret |
POST |
/v1/management/webhooks/{webhookId}/test |
Gửi một webhook.test |
GET |
/v1/management/webhooks/{webhookId}/deliveries?page=1&limit=50 |
Lịch sử gửi, giới hạn tối đa 100 |
Tạm dừng bằng PATCH {"is_active": false}. Tiếp tục bằng PATCH {"is_active": true}. Việc tiếp tục sẽ đặt lại số lần thất bại liên tiếp, điều này rất quan trọng sau khi xảy ra sự cố.
Những gì thực sự đến nơi
Mỗi lần gửi là một POST với một phong bì JSON. Các trường của Management API ở dạng snake_case, nhưng các trường callback ở dạng camelCase. Đừng giả định rằng một kiểu định dạng sẽ được áp dụng cho cả hai.
| Trường | Ý nghĩa |
|---|---|
id |
ID sự kiện. Sử dụng nó để loại bỏ trùng lặp |
type |
Loại sự kiện |
created |
Giây Unix |
data |
Payload sự kiện, hình dạng phụ thuộc vào sự kiện |
| Sự kiện | Kích hoạt khi |
|---|---|
task.completed |
Tác vụ hoàn thành thành công |
task.failed |
Tác vụ kết thúc trong thất bại |
task.timeout |
Tác vụ đạt giới hạn thời gian |
webhook.test |
Chỉ được gửi bởi thao tác kiểm tra |
task.completed chứa taskType (ví dụ: video hoặc image), taskId, model tùy chọn, durationMs, resultUrls và settledCost.
task.failed chứa taskType, taskId, error, errorCode, retryable và refundOutcome.
task.timeout chứa taskType, taskId, refundOutcome và các trường thời gian chờ. Đọc bản ghi tác vụ để biết các giá trị đó; tập hợp trường phụ thuộc vào tác vụ.
Các gói đăng ký bao gồm các sự kiện cuối cùng trong tương lai cho các tác vụ bất đồng bộ trong không gian làm việc. Các kết quả đồng bộ và tác vụ lịch sử không được phát lại. Bạn nhận được mọi tác vụ không gian làm việc cho các loại sự kiện bạn đã chọn, vì vậy hãy khớp data.taskId với ID bạn đã lưu khi tạo tác vụ.
Các trường có thể vắng mặt tùy thuộc vào tác vụ. Đó là lý do tại sao GET /v1/tasks/{id} với key sk-… của không gian làm việc gốc vẫn là nguồn sự thật cho kết quả và trạng thái thanh toán. Sự kiện cho bạn biết điều gì đó đã hoàn thành. Bản ghi tác vụ cho bạn biết nó đã tạo ra những gì và chi phí bao nhiêu.
Một điều nữa về retryable trên sự kiện thất bại. Nó mô tả lỗi tạo, không phải là hướng dẫn để tự động gửi lại. Một lần gửi mới là một tác vụ tính phí mới.
Xác thực các byte thô, sau đó xử lý một lần
Mỗi POST mang theo ba header:
X-Webhook-IDX-Webhook-Timestamp, giây UnixX-Webhook-Signature, định dạngsha256=
Chữ ký là HMAC-SHA256 trên chuỗi timestamp chính xác, một dấu chấm và các byte body yêu cầu thô, được khóa bằng secret whsec_… đầy đủ. Thứ tự rất quan trọng, và body cũng vậy.
Hai sai lầm làm hỏng kiểm tra chữ ký nhiều nhất:
- Xác thực JSON đã phân tích cú pháp. Nếu bạn phân tích body và tuần tự hóa lại nó, các byte sẽ thay đổi và HMAC sẽ không khớp. Hãy đọc body thô. Giữ nó dưới dạng byte cho đến khi xác thực thành công.
- Xác thực chỉ với một secret trong quá trình xoay vòng. Sau khi bạn xoay vòng, các lượt gửi đang trên đường truyền vẫn có thể mang chữ ký trước đó. Hãy chấp nhận một danh sách các secret trong một khoảng thời gian ngắn.
Bộ thu Node dưới đây không có phụ thuộc và sử dụng node:http. Nó đọc body thô, xác thực dựa trên danh sách các secret, kiểm tra cửa sổ 300 giây, so sánh id trong body với X-Webhook-ID, loại bỏ trùng lặp theo ID sự kiện, đưa vào hàng đợi và trả về 204. Việc loại bỏ trùng lặp trong mẫu là một tập hợp trong bộ nhớ; hãy sử dụng ràng buộc cơ sở dữ liệu duy nhất trong môi trường production.
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';
// Trong quá trình xoay vòng, liệt kê cả secret whsec_ mới và cũ.
const SECRETS = (process.env.TOKENLAB_WEBHOOK_SECRETS ?? '').split(',').filter(Boolean);
const TOLERANCE_SECONDS = 300;
const seen = new Set(); // Sử dụng ràng buộc DB duy nhất trong production, không phải bộ nhớ.
function verify(rawBody, headers) {
const timestamp = headers['x-webhook-timestamp'];
const signature = headers['x-webhook-signature'];
if (typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false;
if (typeof signature !== 'string' || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
const received = Buffer.from(signature.slice(7), 'hex');
return SECRETS.some((secret) => {
const expected = createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest();
return timingSafeEqual(expected, received);
});
}
const server = createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/webhooks/tokenlab') {
res.writeHead(404).end();
return;
}
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => {
const rawBody = Buffer.concat(chunks); // xác thực các byte chính xác, trước JSON.parse
if (!verify(rawBody, req.headers)) {
res.writeHead(401).end();
return;
}
const event = JSON.parse(rawBody.toString('utf8'));
if (event.id !== req.headers['x-webhook-id']) {
res.writeHead(400).end();
return;
}
if (!seen.has(event.id)) {
seen.add(event.id);
enqueue(event); // bàn giao; thực hiện công việc chậm bên ngoài yêu cầu
}
res.writeHead(204).end();
});
});
function enqueue(event) {
console.log('queued', event.type, event.data?.taskId);
}
server.listen(Number(process.env.PORT ?? 3000));
Bộ thu đã được kiểm tra cục bộ vào ngày 28-09-2026 với các yêu cầu được ký chính xác như người gửi production: gửi hợp lệ, gửi trùng lặp, secret trước đó trong quá trình xoay vòng, sai secret, timestamp cũ, ID header và body không khớp, body bị giả mạo và JSON được tuần tự hóa lại. Tám trường hợp, tất cả đều vượt qua. Một bản trùng lặp đã được xếp hàng một lần.
Phía Python là một hàm xác thực duy nhất. Nó so sánh các chữ ký với hmac.compare_digest và mong đợi các byte body thô từ request.get_data() của Flask hoặc await request.body() của FastAPI.
import hashlib
import hmac
import re
import time
TOLERANCE_SECONDS = 300
SIGNATURE_RE = re.compile(r"^sha256=[a-f0-9]{64}$")
def verify_webhook(raw_body: bytes, headers, secrets: list[str]) -> bool:
"""Kiểm tra webhook TokenLab dựa trên một hoặc nhiều secret whsec_.
raw_body phải là các byte yêu cầu chính xác (Flask: request.get_data(),
FastAPI/Starlette: await request.body()), đọc trước bất kỳ phân tích JSON nào.
"""
timestamp = headers.get("x-webhook-timestamp", "")
signature = headers.get("x-webhook-signature", "")
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
if not SIGNATURE_RE.match(signature):
return False
received = signature.removeprefix("sha256=")
for secret in secrets:
expected = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
if hmac.compare_digest(expected, received):
return True
return False
Đã kiểm tra vào ngày 28-09-2026: hợp lệ, secret trước đó, sai secret, timestamp cũ, body bị giả mạo và body được tuần tự hóa lại với khoảng cách json.dumps mặc định. Sáu trường hợp, tất cả đều vượt qua.
Ngoài chữ ký, hãy thực hiện ba điều trên mỗi yêu cầu:
- Từ chối các timestamp cách thời điểm hiện tại hơn 300 giây. Đó là 5 phút, và nó giới hạn độ cũ của một lần phát lại.
- Xác nhận
idtrong body bằng vớiX-Webhook-ID. - Lưu ID sự kiện cùng với mục công việc của bạn trong một lần ghi nguyên tử, được hỗ trợ bởi ràng buộc duy nhất. Sau đó trả về
2xxnhanh chóng và thực hiện công việc nặng nhọc từ hàng đợi của riêng bạn.
Các lượt gửi có thể lặp lại và thứ tự không được đảm bảo. Cửa sổ timestamp giới hạn độ tuổi phát lại. Việc loại bỏ trùng lặp ID sự kiện ngăn chặn việc xử lý hai lần.
Thử lại, tự động tạm dừng và quy trình phục hồi
Mỗi chu kỳ gửi thực hiện tối đa ba lần thử.
| Lần thử | Thời gian chờ trước đó | Thời gian chờ thử |
|---|---|---|
| 1 | không có | 10 s |
| 2 | 1 s | 10 s |
| 3 | 4 s | 10 s |
Nguồn: Hướng dẫn webhook TokenLab, quan sát ngày 28-09-2026.
Mỗi lần thử nhận được một timestamp và chữ ký mới. Điều đó có nghĩa là kiểm tra chữ ký của bạn phải sử dụng timestamp từ cùng một yêu cầu, không phải giá trị được lưu trong bộ nhớ đệm.
Các phản hồi có thể thử lại: lỗi mạng, 429 và 5xx. Không thử lại trong chu kỳ: các lỗi 4xx khác, chuyển hướng và mục tiêu mạng không hợp lệ. Các lỗi tạm thời có thể kích hoạt các lần thử lại sau đó của cùng một sự kiện với cùng một ID gửi, đây là một lý do khác khiến việc loại bỏ trùng lặp không phải là tùy chọn.
Mười chu kỳ thất bại liên tiếp sẽ tự động tạm dừng endpoint.
Khi bộ thu của bạn bị gián đoạn, hãy thực hiện theo thứ tự này:
- Sửa bộ thu. Xác nhận nó đọc các byte thô và trả về
2xxnhanh chóng. - Tiếp tục endpoint với
PATCH {"is_active": true}. Điều này đặt lại số lần thất bại. - Gửi thử nghiệm với
POST …/test. Một200từ API kiểm tra chỉ có nghĩa là lần thử đã được ghi lại. Kiểm tra lịch sử gửi và xác nhậnoutcome == "delivered". - Đối soát khoảng trống. Lấy các ID tác vụ bạn đã lưu trong khi endpoint bị tạm dừng và gọi
GET /v1/tasks/{id}cho từng ID. - Chỉ sau đó mới tin tưởng lại luồng webhook.
Lịch sử gửi cung cấp cho bạn outcome, http_status, attempts và delivered_at. Nó chỉ lưu trữ siêu dữ liệu, không có payload. Các sự kiện cũ không thể được phát lại thủ công, vì vậy bước 4 là bắt buộc. Các ID tác vụ đã lưu của bạn là con đường phục hồi.
Xoay vòng secret mà không làm mất sự kiện
Việc xoay vòng không thể đảo ngược, vì vậy hãy lập kế hoạch cho cửa sổ thời gian trước khi bạn bắt đầu.
- Gọi
POST /v1/management/webhooks/{webhookId}/rotate-secret. Phản hồi trả về secret mới một lần. - Thêm secret mới vào danh sách xác thực của bạn trên bộ thu. Giữ cả secret cũ trong danh sách đó.
- Triển khai thay đổi bộ thu trước khi bạn bỏ bất cứ thứ gì. Danh sách phải giữ cả hai secret cùng một lúc.
- Gửi thử nghiệm và xác nhận
outcome == "delivered"trong lịch sử. - Sau một khoảng thời gian ngắn, xóa secret cũ và triển khai lại.
Các lượt gửi đang trên đường truyền vẫn có thể mang chữ ký trước đó. Nếu bạn hoán đổi secret trong một bước, bạn sẽ làm mất các sự kiện đó. Một bộ xác thực chỉ giữ một secret có thể từ chối các lượt gửi được ký ngay trước khi xoay vòng.
Quản lý webhook từ MCP
Nếu bạn điều khiển TokenLab từ một tác nhân, máy chủ MCP sẽ hiển thị cùng một vòng đời. Sử dụng @tokenlabai/mcp-server với cấu hình full. Các công cụ là list_webhooks, create_webhook, get_webhook, update_webhook, delete_webhook, rotate_webhook_secret, test_webhook và list_webhook_deliveries.
Máy chủ đọc Management Token từ TOKENLAB_MANAGEMENT_TOKEN. Gói đã xuất bản mới nhất được quan sát vào ngày 28-09-2026 là 0.6.24. MCP chỉnh sửa cùng các endpoint bạn thấy trong Dashboard, vì vậy không có trạng thái riêng biệt nào cần đối soát.
Câu hỏi thường gặp
Các tác vụ ảnh có gửi webhook không?
Có. Mọi tác vụ bất đồng bộ trong không gian làm việc, bao gồm cả tác vụ ảnh, đều gửi sự kiện cuối cùng của nó đến các endpoint đã đăng ký loại sự kiện đó. Trường taskType trên payload cho bạn biết đó là loại tác vụ nào, ví dụ: video hoặc image. Các kết quả đồng bộ không được bao gồm.
Điều gì xảy ra nếu endpoint của tôi bị gián đoạn?
Mỗi chu kỳ thử lại tối đa ba lần. Mười chu kỳ thất bại liên tiếp sẽ tự động tạm dừng endpoint. Các lỗi gửi tạm thời có thể được thử lại sau đó với cùng một ID gửi. Khi endpoint đã bị tạm dừng, các sự kiện từ thời điểm tạm dừng sẽ không được gửi sau đó và không thể phát lại thủ công. Hãy sửa bộ thu, tiếp tục endpoint, gửi thử nghiệm, sau đó đối soát các tác vụ bạn đã tạo trong thời gian gián đoạn bằng cách gọi GET /v1/tasks/{id} với các ID tác vụ đã lưu của bạn.
Tôi có thể phát lại một sự kiện cũ không?
Không. Lịch sử gửi chỉ giữ siêu dữ liệu, không có payload và không có phát lại thủ công. Cửa sổ timestamp cũng từ chối bất cứ thứ gì cũ hơn 300 giây. Đối soát thông qua API tác vụ là cách được hỗ trợ để bắt kịp.
task.failed với retryable: true có an toàn để tự động gửi lại không?
Không. retryable mô tả lỗi tạo. Nó không phải là hướng dẫn để gửi lại. Một lần gửi mới là một tác vụ tính phí mới, vì vậy hãy tự quyết định việc thử lại và tính toán chi phí.
API tương thích Seedance có sử dụng các webhook này không?
Không. callback_url theo yêu cầu của nó là một hợp đồng riêng biệt với payload riêng. Nó không sử dụng các sự kiện không gian làm việc hoặc các header HMAC này, vì vậy đừng trỏ một bộ xác thực vào cả hai.
Hãy bắt đầu với hợp đồng đầy đủ trong hướng dẫn webhook, sau đó tạo API key và bật endpoint đầu tiên của bạn trong không gian làm việc gửi các tác vụ của bạn.
Nguồn
- https://docs.tokenlab.sh/guides/webhooksQuan sát ngày 2026-09-28
- https://docs.tokenlab.sh/guides/async-jobs-pollingQuan sát ngày 2026-09-28
- https://www.npmjs.com/package/@tokenlabai/mcp-serverQuan sát ngày 2026-09-28



