Webhooks
Thay vì poll endpoint mỗi vài giây, hãy để AIGate gọi tới server của bạn ngay khi job hoàn thành. Tiết kiệm tài nguyên, phản hồi tức thì.
Cách hoạt động
- Bạn cung cấp
webhook_urltrong yêu cầu tạo ảnh/video - AIGate xử lý job (có thể mất vài giây tới vài phút)
- Khi xong (succeeded hoặc failed), AIGate gửi POST tới URL của bạn
- Server bạn xác thực chữ ký HMAC, xử lý kết quả, trả về 2xx
Cấu hình webhook
Cách 1: Per-request (theo từng yêu cầu)
{
"prompt": "...",
"model": "kling-pro",
"webhook_url": "https://your-app.com/webhook/aigate"
}
Cách 2: Global (cho mọi job)
Vào cp.aigate.id.vn → Settings → Webhooks để cấu hình URL mặc định và signing secret.
Cấu trúc payload
AIGate gửi POST với Content-Type: application/json:
{
"event": "job.succeeded",
"id": "evt_a8b3c9",
"job_id": "vid_x9k2lp",
"kind": "video",
"created_at": "2026-04-24T16:31:30Z",
"data": {
"id": "vid_x9k2lp",
"status": "succeeded",
"url": "https://cdn.aigate.id.vn/vid/x9k2lp.mp4",
"thumbnail": "https://cdn.aigate.id.vn/vid/x9k2lp_thumb.jpg",
"duration": 5.0,
"resolution": "1080p",
"model_used": "kling-pro",
"credits_used": 200,
"metadata": {
"user_ref": "order_12345"
}
}
}
Các sự kiện (events)
| Event | Khi nào |
|---|---|
job.queued | Job được nhận, đang chờ slot |
job.started | Job bắt đầu xử lý |
job.progress | Cập nhật tiến độ (gửi mỗi 25%) |
job.succeeded | Job xong, có URL kết quả |
job.failed | Job lỗi, credits đã hoàn lại |
credits.low | Số dư < 100 credits |
credits.depleted | Hết credits hoàn toàn |
Xác thực chữ ký HMAC
Mỗi webhook đều có header X-AIGate-Signature:
X-AIGate-Signature: sha256=a3b1c2d4e5f6...
X-AIGate-Timestamp: 1714234234
Chữ ký được tính:
signature = HMAC-SHA256(secret, timestamp + "." + raw_body)
Luôn verify chữ ký trước khi xử lý — nếu không, kẻ tấn công có thể giả mạo callback.
Verify bằng PHP
<?php
// /webhook/aigate.php
$secret = getenv('AIGATE_WEBHOOK_SECRET'); // lấy từ cp.aigate.id.vn
$timestamp = $_SERVER['HTTP_X_AIGATE_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_AIGATE_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');
// Verify thời gian (chống replay): chỉ chấp nhận trong 5 phút
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(401);
exit('Timestamp ngoài khoảng cho phép');
}
// Tính chữ ký mong đợi
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
// So sánh constant-time
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Chữ ký không hợp lệ');
}
// OK — xử lý event
$event = json_decode($rawBody, true);
switch ($event['event']) {
case 'job.succeeded':
$videoUrl = $event['data']['url'];
// Lưu vào DB, gửi email cho user...
break;
case 'job.failed':
// Báo lỗi cho user
break;
}
http_response_code(200);
echo 'ok';
Verify bằng Node.js (Express)
import express from 'express';
import crypto from 'crypto';
const app = express();
const SECRET = process.env.AIGATE_WEBHOOK_SECRET;
// QUAN TRỌNG: dùng raw body, không phải parsed JSON
app.post('/webhook/aigate',
express.raw({ type: 'application/json' }),
(req, res) => {
const ts = req.header('X-AIGate-Timestamp') || '';
const sig = req.header('X-AIGate-Signature') || '';
const raw = req.body.toString('utf8');
// Chống replay
if (Math.abs(Date.now()/1000 - parseInt(ts)) > 300) {
return res.status(401).send('timestamp');
}
// Verify HMAC
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(ts + '.' + raw)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(raw);
if (event.event === 'job.succeeded') {
console.log('Video xong:', event.data.url);
// ... lưu DB, notify user ...
}
res.send('ok');
}
);
app.listen(3000);
Verify bằng Python (Flask)
import hmac, hashlib, time, os
from flask import Flask, request
app = Flask(__name__)
SECRET = os.environ['AIGATE_WEBHOOK_SECRET'].encode()
@app.post('/webhook/aigate')
def webhook():
ts = request.headers.get('X-AIGate-Timestamp', '')
sig = request.headers.get('X-AIGate-Signature', '')
raw = request.get_data()
if abs(time.time() - int(ts)) > 300:
return 'timestamp', 401
payload = f"{ts}.".encode() + raw
expected = 'sha256=' + hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return 'invalid', 401
event = request.get_json()
# ... xử lý event ...
return 'ok', 200
Retry & idempotency
Retry policy
Nếu server bạn trả về không phải 2xx, AIGate sẽ retry với backoff:
| Lần thử | Sau |
|---|---|
| Lần 1 | Ngay lập tức |
| Lần 2 | Sau 30 giây |
| Lần 3 | Sau 5 phút |
| Lần 4 | Sau 30 phút |
| Lần 5 | Sau 2 giờ |
| Lần 6 (cuối) | Sau 12 giờ |
Sau 6 lần thất bại, webhook bị đánh dấu dead — bạn có thể replay thủ công từ cp.aigate.id.vn → Webhook Logs.
Idempotency (xử lý trùng lặp)
Do retry, server bạn có thể nhận cùng event nhiều lần. Luôn dùng id của event để khử trùng:
<?php
$eventId = $event['id'];
$pdo->prepare("INSERT IGNORE INTO webhook_seen (event_id, seen_at) VALUES (?, NOW())")
->execute([$eventId]);
if ($pdo->query("SELECT ROW_COUNT()")->fetchColumn() === 0) {
// Đã xử lý event này rồi
http_response_code(200);
exit('ok (duplicate)');
}
// Xử lý lần đầu...
Test webhook trong môi trường dev
Cách 1: Dùng ngrok
# Mở tunnel tới localhost:3000
ngrok http 3000
# Copy URL forward (https://xxx.ngrok-free.app)
# Set làm webhook_url khi gọi API
Cách 2: Dùng webhook.site (test nhanh)
- Vào webhook.site
- Copy URL của bạn
- Đặt vào
webhook_urltrong API request - Xem payload nhận được realtime trên trình duyệt
Cách 3: Replay từ Dashboard
Trong cp.aigate.id.vn → Webhook Logs, nhấn Replay bên cạnh bất kỳ event nào để gửi lại tới webhook URL hiện tại.
Best practices
- ✓ Verify chữ ký luôn luôn, dùng
hash_equals/timing_safe_equal - ✓ Trả 2xx ngay khi nhận, xử lý logic nặng trong job nền (queue)
- ✓ Khử trùng theo
event.id— không xử lý cùng event 2 lần - ✓ Log mọi webhook — giúp debug khi gặp vấn đề
- ✓ Whitelist IP AIGate webhook server (xem cp.aigate.id.vn → Settings)
- ✗ Đừng để timeout > 10s — AIGate sẽ coi là failed và retry
- ✗ Đừng dùng GET để verify webhook URL — webhook luôn là POST
Troubleshooting
| Triệu chứng | Nguyên nhân thường gặp |
|---|---|
| 401 invalid signature | Secret sai, hoặc dùng parsed JSON thay vì raw body |
| Webhook không tới | URL không reachable từ Internet, firewall chặn, hoặc URL chưa SSL |
| Nhận trùng event | Server trả về > 2xx → AIGate retry. Kiểm tra log error |
| Timestamp 401 | Đồng hồ server chênh quá 5 phút — sync NTP |