Webhooks
Đăng ký endpoint để nhận sự kiện order/tracking theo thời gian thực — quickstart, receiver mẫu, xác thực chữ ký, local testing, retry và troubleshooting.
Webhooks là gì
Thay vì poll API liên tục để biết đơn hàng thay đổi, bạn đăng ký một URL (endpoint) để hệ thống chủ động POST sự kiện tới bạn ngay khi có thay đổi: đơn được tạo, đổi trạng thái, bị hủy, hoặc có cập nhật tracking.
Mỗi sự kiện là một request POST với body JSON và các header chữ ký để bạn xác thực request
thật sự đến từ chúng tôi.
Note: Toàn bộ endpoint quản lý webhook nằm dưới
/v1/me/webhooksvà dùng client token để xác thực — giống các API client khác. Base URL production:https://api.eod-infinitee.com.
Client token có prefix sqok_. Đây khác với webhook signing secret (prefix whsec_) — secret
dùng để ký payload, không phải để gọi API.
Quickstart
Từ zero tới một receiver đã verify được, trong 5 bước. Đặt hai biến môi trường trước:
export SOQ_BASE="https://api.eod-infinitee.com"
export SOQ_TOKEN="sqok_..." # client token của bạn (lấy trong Client Portal)1. Đăng ký endpoint — POST /v1/me/webhooks. URL phải là https public (xem
Local testing nếu đang chạy máy local):
curl -sS -X POST "$SOQ_BASE/v1/me/webhooks" \
-H "Authorization: Bearer $SOQ_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/soq",
"topics": ["order.created", "order.status_updated", "tracking.updated"],
"description": "Production endpoint"
}'2. Lưu lại secret — response 201 trả về secret (prefix whsec_) đúng một lần.
Copy ngay và lưu vào nơi an toàn (biến môi trường / secret manager). Mất thì phải
rotate:
{
"id": "b2f8c0a1-3d4e-4f5a-9b6c-7d8e9f0a1b2c",
"url": "https://your-app.example.com/webhooks/soq",
"topics": ["order.created", "order.status_updated", "tracking.updated"],
"is_active": true,
"secret": "whsec_ZmFrZXNlY3JldGV4YW1wbGVvbmx5bm90cmVhbHZhbHVl"
}export WEBHOOK_SECRET="whsec_..." # secret vừa nhận được3. Chạy một receiver — copy một trong 3 receiver bên dưới, verify
chữ ký trên raw body và trả 2xx nhanh.
4. Gửi sự kiện test — thay :id bằng id ở bước 1:
curl -sS -X POST "$SOQ_BASE/v1/me/webhooks/b2f8c0a1-3d4e-4f5a-9b6c-7d8e9f0a1b2c/test" \
-H "Authorization: Bearer $SOQ_TOKEN"Trả về 202 Accepted kèm bản ghi delivery. Endpoint phải đang active, nếu không sẽ nhận
400 WEBHOOK_INACTIVE.
5. Kiểm tra delivery — xem sự kiện test đã tới success chưa:
curl -sS "$SOQ_BASE/v1/me/webhooks/b2f8c0a1-3d4e-4f5a-9b6c-7d8e9f0a1b2c/deliveries?limit=5" \
-H "Authorization: Bearer $SOQ_TOKEN"Mỗi bản ghi có status (pending/success/failed), attempts, last_status_code,
last_error — dùng để debug ngay.
Tip: Toàn bộ endpoint + payload của từng event có trang reference riêng dưới section Webhooks ở sidebar — xem danh sách bên dưới.
Topics
Chọn ít nhất 1 topic khi đăng ký. Danh sách topic hợp lệ (lấy động qua
GET /v1/me/webhooks/topics):
| Topic | Khi nào bắn | Reference |
|---|---|---|
order.created | Đơn hàng được tạo. | order.created |
order.status_updated | Trạng thái đơn thay đổi. | order.status_updated |
order.cancelled | Đơn hàng bị hủy. | order.cancelled |
tracking.created | Đơn được gán mã tracking. | tracking.created |
tracking.updated | Nhận được cập nhật tracking từ nhà vận chuyển. | tracking.updated |
Note: Ngoài ra có topic
webhook.test— chỉ dùng cho sự kiện test thủ công. Bạn không đăng ký đượcwebhook.test; nó chỉ xuất hiện khi bạn gọi endpoint test.
Ràng buộc URL
| Rule | Chi tiết |
|---|---|
| Scheme | https:// (bắt buộc trên production). http:// chỉ được phép ở môi trường non-production. |
| Độ dài | Tối đa 2048 ký tự |
| Host | Không được là localhost, IP private/loopback/link-local/CGNAT (chống SSRF) |
| Số lượng | Tối đa 5 webhook mỗi client |
URL không hợp lệ trả về 400 INVALID_WEBHOOK_URL. Vượt quá 5 webhook trả về
400 WEBHOOK_LIMIT_REACHED.
Cấu trúc payload (envelope)
Mọi sự kiện đều được bọc trong một envelope chung:
{
"id": "e7c1a2b3-4d5e-6f70-8192-a3b4c5d6e7f8",
"topic": "order.created",
"created_at": "2026-09-23T08:05:12.345Z",
"data": { }
}| Field | Kiểu | Mô tả |
|---|---|---|
id | UUID | ID sự kiện. Giống nhau cho mọi endpoint nhận cùng một sự kiện và giữ nguyên qua các lần retry — dùng để dedupe. |
topic | string | Topic của sự kiện. |
created_at | ISO-8601 | Thời điểm sự kiện phát sinh. |
data | object | Nội dung sự kiện (xem trang reference của từng event). |
Payload cho order.* và tracking.*
Tất cả topic order.* và tracking.* mang cùng một shape data — snapshot của đơn hàng, nên
bạn chỉ cần parse một cấu trúc cho mọi loại sự kiện:
{
"id": "e7c1a2b3-4d5e-6f70-8192-a3b4c5d6e7f8",
"topic": "order.created",
"created_at": "2026-09-23T08:05:12.345Z",
"data": {
"order": {
"id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"reference_id": "MY-ORDER-001",
"store_id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"client_status": "processing",
"shipment_status": null,
"created_at": "2026-09-23T08:05:12.000Z",
"updated_at": "2026-09-23T08:05:12.000Z"
},
"tracking": null
}
}| Field | Kiểu | Mô tả |
|---|---|---|
data.order.id | UUID | ID đơn hàng nội bộ. |
data.order.reference_id | string | Reference ID bạn gửi khi tạo đơn. |
data.order.store_id | UUID | Store của đơn. |
data.order.client_status | string | null | Trạng thái hiển thị cho client, null khi chưa có. |
data.order.shipment_status | string | null | Trạng thái giao hàng suy ra từ tracking, null khi chưa ship. |
data.order.created_at | ISO-8601 | Thời điểm tạo đơn. |
data.order.updated_at | ISO-8601 | Thời điểm cập nhật gần nhất. |
data.tracking | object | null | Snapshot tracking, null khi đơn chưa có mã tracking. |
Khi đơn đã có tracking, data.tracking là:
{
"data": {
"order": {
"id": "0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9",
"reference_id": "MY-ORDER-001",
"store_id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"client_status": "shipping",
"shipment_status": "in_transit",
"created_at": "2026-09-23T08:05:12.000Z",
"updated_at": "2026-09-24T10:20:00.000Z"
},
"tracking": {
"tracking_number": "9400111899223456789012",
"carrier": "USPS",
"tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223456789012",
"shipment_status": "in_transit"
}
}
}Field previous
Với các topic dạng *_updated (order.status_updated,
tracking.updated), payload có thêm data.previous chứa các
field đã thay đổi:
{
"topic": "order.status_updated",
"data": {
"order": { "client_status": "shipping" },
"tracking": null,
"previous": { "client_status": "processing" }
}
}Payload webhook.test
Sự kiện test có data riêng (xem reference):
{
"id": "11112222-3333-4444-5555-666677778888",
"topic": "webhook.test",
"created_at": "2026-09-23T08:10:00.000Z",
"data": {
"message": "This is a test event from Shopify Order Queue.",
"webhook_id": "b2f8c0a1-3d4e-4f5a-9b6c-7d8e9f0a1b2c"
}
}Headers
Mỗi request delivery mang các header sau:
| Header | Ví dụ | Mô tả |
|---|---|---|
Content-Type | application/json | Body luôn là JSON. |
User-Agent | ShopifyOrderQueue-Webhook/1.0 | Nhận diện request của chúng tôi. |
X-Webhook-Id | e7c1a2b3-... | ID sự kiện (bằng id trong envelope). Dùng để dedupe. |
X-Webhook-Topic | order.created | Topic của sự kiện. |
X-Webhook-Timestamp | 1758614712 | Unix timestamp (giây) lúc ký. |
X-Webhook-Signature | sha256=<hex> | HMAC-SHA256 của payload (xem bên dưới). |
Xác thực chữ ký
Chữ ký được tính bằng HMAC-SHA256 với key là signing secret, trên chuỗi ghép
"<timestamp>.<raw body>":
signature = "sha256=" + HMAC_SHA256(secret, `${timestamp}.${rawBody}`).hex()Trong đó:
timestamp= giá trị headerX-Webhook-Timestamp(unix giây).rawBody= đúng byte thô của request body (chưa qua parse/re-serialize). Đừng parse rồi stringify lại — thứ tự key có thể đổi và chữ ký sẽ không khớp.- Kết quả so sánh với header
X-Webhook-Signaturebằng so sánh hằng thời gian (constant-time).
Warning: Luôn xác thực chữ ký trên raw body trước khi parse JSON. Trong Express, dùng
express.raw()cho route webhook, hoặc lưu raw body bằngverifycallback củaexpress.json(). Trong FastAPI, dùngawait request.body(). Trong PHP, đọcfile_get_contents('php://input').
Receiver hoàn chỉnh
Ba receiver dưới đây chạy được ngay: verify chữ ký trên raw body, kiểm tra timestamp trong
5 phút, dedupe theo X-Webhook-Id, rồi trả 2xx nhanh trước khi xử lý nặng.
Node.js / Express
import express from 'express'
import crypto from 'crypto'
const app = express()
const SECRET = process.env.WEBHOOK_SECRET // whsec_...
const TOLERANCE_SEC = 300 // 5 phút
const seen = new Set() // production: dùng Redis/DB, không dùng Set trong RAM
function verify(headers, rawBody) {
const timestamp = headers['x-webhook-timestamp']
const signature = headers['x-webhook-signature']
if (!timestamp || !signature) return false
// Chống replay: từ chối request quá cũ.
const age = Math.floor(Date.now() / 1000) - Number(timestamp)
if (!Number.isFinite(age) || age > TOLERANCE_SEC) return false
const body = Buffer.isBuffer(rawBody) ? rawBody.toString('utf8') : rawBody
const expected =
'sha256=' + crypto.createHmac('sha256', SECRET).update(`${timestamp}.${body}`).digest('hex')
const a = Buffer.from(signature)
const b = Buffer.from(expected)
// timingSafeEqual ném lỗi nếu độ dài khác nhau — check trước.
return a.length === b.length && crypto.timingSafeEqual(a, b)
}
// QUAN TRỌNG: express.raw() để có đúng byte thô, không phải express.json().
app.post('/webhooks/soq', express.raw({ type: '*/*' }), (req, res) => {
if (!verify(req.headers, req.body)) {
return res.status(401).send('invalid signature')
}
const eventId = req.headers['x-webhook-id']
if (seen.has(eventId)) return res.sendStatus(200) // đã xử lý — bỏ qua
seen.add(eventId)
const event = JSON.parse(req.body.toString('utf8'))
// Trả 2xx NGAY, đẩy xử lý nặng sang hàng đợi nền.
res.sendStatus(200)
queueMicrotask(() => handleEvent(event))
})
function handleEvent(event) {
switch (event.topic) {
case 'order.created':
case 'order.status_updated':
console.log('order', event.data.order.reference_id, event.data.order.client_status)
break
case 'tracking.updated':
console.log('tracking', event.data.tracking?.tracking_number)
break
}
}
app.listen(3000)Python / FastAPI
import hashlib
import hmac
import json
import time
from fastapi import FastAPI, Request, Response
app = FastAPI()
SECRET = "whsec_..." # đọc từ env trong thực tế
TOLERANCE_SEC = 300 # 5 phút
seen: set[str] = set() # production: dùng Redis/DB
def verify(headers, raw_body: bytes) -> bool:
timestamp = headers.get("x-webhook-timestamp")
signature = headers.get("x-webhook-signature")
if not timestamp or not signature:
return False
# Chống replay: từ chối request quá cũ.
try:
age = int(time.time()) - int(timestamp)
except ValueError:
return False
if age > TOLERANCE_SEC:
return False
signed = f"{timestamp}.".encode("utf-8") + raw_body
digest = hmac.new(SECRET.encode("utf-8"), signed, hashlib.sha256).hexdigest()
expected = f"sha256={digest}"
# So sánh hằng thời gian.
return hmac.compare_digest(signature, expected)
@app.post("/webhooks/soq")
async def receive(request: Request):
raw_body = await request.body() # đúng byte thô, chưa parse
if not verify(request.headers, raw_body):
return Response(status_code=401, content="invalid signature")
event_id = request.headers.get("x-webhook-id")
if event_id in seen:
return Response(status_code=200) # đã xử lý — bỏ qua
seen.add(event_id)
event = json.loads(raw_body)
# Trả 2xx nhanh; đẩy xử lý nặng sang background task / hàng đợi.
handle_event(event)
return Response(status_code=200)
def handle_event(event: dict) -> None:
topic = event["topic"]
if topic in ("order.created", "order.status_updated"):
order = event["data"]["order"]
print("order", order["reference_id"], order["client_status"])
elif topic == "tracking.updated":
tracking = event["data"].get("tracking") or {}
print("tracking", tracking.get("tracking_number"))PHP (plain)
<?php
// webhook.php — trỏ endpoint của bạn tới file này.
$secret = getenv('WEBHOOK_SECRET'); // whsec_...
$toleranceSec = 300; // 5 phút
$rawBody = file_get_contents('php://input'); // đúng byte thô
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$eventId = $_SERVER['HTTP_X_WEBHOOK_ID'] ?? '';
function verify(string $secret, string $timestamp, string $signature, string $rawBody, int $tolerance): bool
{
if ($timestamp === '' || $signature === '') {
return false;
}
// Chống replay: từ chối request quá cũ.
if (time() - (int) $timestamp > $tolerance) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
// So sánh hằng thời gian.
return hash_equals($expected, $signature);
}
if (!verify($secret, $timestamp, $signature, $rawBody, $toleranceSec)) {
http_response_code(401);
echo 'invalid signature';
exit;
}
// Dedupe theo X-Webhook-Id (ví dụ đơn giản: file lock; production dùng Redis/DB).
$marker = sys_get_temp_dir() . '/soq_wh_' . preg_replace('/[^a-zA-Z0-9_-]/', '', $eventId);
if ($eventId !== '' && file_exists($marker)) {
http_response_code(200); // đã xử lý — bỏ qua
exit;
}
if ($eventId !== '') {
touch($marker);
}
// Trả 2xx NGAY; đẩy xử lý nặng ra tiến trình nền (queue/cron).
http_response_code(200);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
$event = json_decode($rawBody, true);
switch ($event['topic'] ?? '') {
case 'order.created':
case 'order.status_updated':
error_log('order ' . $event['data']['order']['reference_id'] . ' ' . $event['data']['order']['client_status']);
break;
case 'tracking.updated':
error_log('tracking ' . ($event['data']['tracking']['tracking_number'] ?? ''));
break;
}Note: Nếu bạn rotate secret, hệ thống dùng secret mới cho các delivery kế tiếp. Trong giai đoạn chuyển đổi, hãy chấp nhận cả secret cũ và mới cho tới khi chắc chắn không còn delivery ký bằng secret cũ.
Local testing
URL đăng ký phải là https public — hệ thống từ chối localhost và mọi IP
private/loopback/link-local/CGNAT (chống SSRF), nên trỏ thẳng vào máy dev sẽ nhận
400 INVALID_WEBHOOK_URL. Cách chuẩn khi phát triển: expose port local ra một URL https tạm
thời rồi đăng ký URL đó.
Cloudflare Tunnel (cloudflared)
# receiver đang chạy ở localhost:3000
cloudflared tunnel --url http://localhost:3000
# → in ra một URL dạng https://<random>.trycloudflare.comDùng URL https://<random>.trycloudflare.com/webhooks/soq khi POST /v1/me/webhooks.
ngrok
ngrok http 3000
# → Forwarding https://<random>.ngrok-free.app -> http://localhost:3000Dùng URL https://<random>.ngrok-free.app/webhooks/soq khi đăng ký.
Tip: URL tunnel đổi mỗi lần chạy lại. Sau khi khởi động lại tunnel, cập nhật webhook bằng
PATCH /v1/me/webhooks/:idvớiurlmới thay vì tạo webhook mới (tránh chạm giới hạn 5). Rồi gọi lạiPOST /v1/me/webhooks/:id/testđể xác nhận.
Yêu cầu response
- Trả về HTTP
2xxđể báo đã nhận thành công. - Trả về trong vòng 10 giây — quá thời gian này request bị coi là timeout và sẽ retry.
- Redirect không được follow — endpoint phải trả về trực tiếp, không
3xx. - Xử lý nặng nên đẩy sang hàng đợi nền; chỉ cần xác thực + lưu event rồi trả
2xxngay.
Retry và auto-disable
Nếu một delivery thất bại (timeout, lỗi kết nối, hoặc status không phải 2xx), nó được thử lại
theo lịch backoff cố định:
| Lần thử | Thời gian chờ trước lần thử này |
|---|---|
| 1 | (gửi ngay) |
| 2 | 1 phút |
| 3 | 5 phút |
| 4 | 30 phút |
| 5 | 2 giờ |
| 6 | 6 giờ |
| 7 | 12 giờ |
Tổng cộng 7 lần thử (1 lần đầu + 6 lần retry). Sau lần thứ 7 vẫn thất bại, delivery được đánh
failed và không thử lại nữa.
Auto-disable: nếu một endpoint có 10 delivery thất bại liên tiếp, nó tự động bị vô hiệu
hóa (is_active: false, disabled_reason: "too_many_failures"). Một delivery thành công sẽ
reset bộ đếm này về 0.
Khi endpoint bị vô hiệu hóa, mọi delivery đang chờ retry sẽ bị hủy (không gửi nữa,
last_error: "webhook_disabled") — chúng không tính vào chuỗi thất bại.
Để bật lại, gọi PATCH /v1/me/webhooks/:id với { "is_active": true }. Việc
này reset consecutive_failures về 0 và xóa disabled_reason.
Note: Log delivery được lưu 30 ngày rồi tự xóa.
Idempotency
Cùng một sự kiện có thể được gửi nhiều lần (do retry, hoặc bạn đăng ký nhiều endpoint).
Mọi lần gửi của cùng một sự kiện đều mang cùng X-Webhook-Id (bằng id trong envelope).
Important: Dedupe theo
X-Webhook-Id. Lưu lại các event id đã xử lý và bỏ qua id trùng để tránh xử lý hai lần (như trong ba receiver ở trên).
API quản lý webhook
Tất cả endpoint dưới đây yêu cầu header Authorization: Bearer <client_token>.
| Method | Endpoint | Reference | Mô tả |
|---|---|---|---|
GET | /v1/me/webhooks/topics | list-webhook-topics | Danh sách topic hợp lệ + mô tả. |
GET | /v1/me/webhooks | list-webhooks | Liệt kê webhook của client (không kèm secret). |
POST | /v1/me/webhooks | create-webhook | Đăng ký webhook mới. Trả 201 kèm secret (một lần). |
PATCH | /v1/me/webhooks/:id | update-webhook | Cập nhật url, topics, description, is_active. |
DELETE | /v1/me/webhooks/:id | delete-webhook | Xóa webhook. Trả 204. |
POST | /v1/me/webhooks/:id/rotate-secret | rotate-webhook-secret | Cấp signing secret mới. Trả secret (một lần). |
POST | /v1/me/webhooks/:id/test | test-webhook | Gửi sự kiện webhook.test. Trả 202. |
GET | /v1/me/webhooks/:id/deliveries | list-webhook-deliveries | Lịch sử delivery của webhook. Phân trang: page (mặc định 1), limit (mặc định 20, tối đa 100). |
POST | /v1/me/webhooks/deliveries/:delivery_id/redeliver | redeliver-webhook-delivery | Gửi lại một delivery từ đầu. Trả 202. |
Cập nhật webhook
PATCH /v1/me/webhooks/b2f8c0a1-3d4e-4f5a-9b6c-7d8e9f0a1b2c
{
"topics": ["order.created", "order.cancelled"],
"is_active": true
}Truyền is_active: true trên một endpoint đang bị vô hiệu hóa sẽ bật lại nó và reset chuỗi
thất bại.
Xem lịch sử delivery
GET /v1/me/webhooks/:id/deliveries?page=1&limit=20Mỗi bản ghi delivery gồm status (pending/success/failed), attempts,
last_status_code, last_error, next_retry_at, delivered_at và cả payload đã gửi — hữu
ích để debug.
Troubleshooting
| HTTP / Trạng thái | Mã / dấu hiệu | Nguyên nhân & cách xử lý |
|---|---|---|
| 400 | INVALID_WEBHOOK_URL | URL sai scheme, quá 2048 ký tự, hoặc trỏ tới host private/localhost/loopback/CGNAT. Dùng https public; khi dev thì expose bằng cloudflared/ngrok. |
| 400 | WEBHOOK_LIMIT_REACHED | Đã đạt tối đa 5 webhook mỗi client. Xóa (DELETE) hoặc cập nhật (PATCH) webhook cũ thay vì tạo mới. |
| 400 | WEBHOOK_INACTIVE | Gọi /test (hoặc endpoint bị auto-disable) trên webhook đang is_active: false. Bật lại bằng PATCH { "is_active": true } rồi test lại. |
| — | disabled_reason: "too_many_failures" | Endpoint bị auto-disable sau 10 delivery thất bại liên tiếp. Sửa receiver, rồi PATCH { "is_active": true } để reset consecutive_failures và xóa disabled_reason. |
| — | delivery last_error: "webhook_disabled" | Delivery bị hủy vì endpoint đã bị vô hiệu hóa lúc tới lượt gửi. Không tính vào chuỗi thất bại; bật lại endpoint rồi redeliver nếu cần. |
| 401 (phía bạn) | signature mismatch | (1) Verify trên body đã parse/re-serialize thay vì raw body — luôn ký/verify trên byte thô. (2) Dùng sai secret sau khi rotate — cập nhật secret mới, chấp nhận cả cũ + mới trong giai đoạn chuyển. (3) Lệch đồng hồ khiến timestamp quá 5 phút — đồng bộ NTP hoặc nới tolerance. (4) Quên ghép "<timestamp>." trước raw body. |
| timeout / retry | delivery status: failed, attempts tăng | Receiver trả chậm hơn 10 giây hoặc không 2xx. Trả 2xx ngay rồi xử lý nền; kiểm tra GET /:id/deliveries xem last_status_code/last_error. |
| 404 | (not found) | Webhook/delivery id không tồn tại hoặc không thuộc client của bạn. |
Trang reference
- Endpoints: list-webhook-topics · list-webhooks · create-webhook · update-webhook · delete-webhook · rotate-webhook-secret · test-webhook · list-webhook-deliveries · redeliver-webhook-delivery
- Events (payload từng topic): order.created · order.status_updated · order.cancelled · tracking.created · tracking.updated · webhook.test
Checklist tích hợp
- Đăng ký endpoint và lưu
secret(chỉ hiện một lần). - Xác thực
X-Webhook-Signaturetrên raw body trước khi parse. - Từ chối request có
X-Webhook-Timestampcũ hơn 5 phút. - Dedupe theo
X-Webhook-Id. - Trả
2xxtrong vòng 10 giây; đẩy xử lý nặng sang hàng đợi nền. - Gửi sự kiện test để xác nhận endpoint hoạt động.
- Theo dõi lịch sử delivery và bật lại endpoint nếu bị auto-disable.

