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

  1. Bạn cung cấp webhook_url trong yêu cầu tạo ảnh/video
  2. AIGate xử lý job (có thể mất vài giây tới vài phút)
  3. Khi xong (succeeded hoặc failed), AIGate gửi POST tới URL của bạn
  4. 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)

EventKhi nào
job.queuedJob được nhận, đang chờ slot
job.startedJob bắt đầu xử lý
job.progressCập nhật tiến độ (gửi mỗi 25%)
job.succeededJob xong, có URL kết quả
job.failedJob lỗi, credits đã hoàn lại
credits.lowSố dư < 100 credits
credits.depletedHế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 1Ngay lập tức
Lần 2Sau 30 giây
Lần 3Sau 5 phút
Lần 4Sau 30 phút
Lần 5Sau 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)

  1. Vào webhook.site
  2. Copy URL của bạn
  3. Đặt vào webhook_url trong API request
  4. 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

Troubleshooting

Triệu chứngNguyên nhân thường gặp
401 invalid signatureSecret sai, hoặc dùng parsed JSON thay vì raw body
Webhook không tớiURL không reachable từ Internet, firewall chặn, hoặc URL chưa SSL
Nhận trùng eventServer trả về > 2xx → AIGate retry. Kiểm tra log error
Timestamp 401Đồng hồ server chênh quá 5 phút — sync NTP

Tiếp theo

👉 Trở về tài liệu

👉 Cấu hình webhook trên Dashboard