AnyCrawl

Webhooks

Nhận thông báo thời gian thực cho mọi sự kiện AnyCrawl: scrape, crawl, map, search và tác vụ theo lịch.

Giới thiệu

Webhooks cho phép bạn nhận thông báo HTTP thời gian thực khi có sự kiện trong tài khoản AnyCrawl. Thay vì polling, AnyCrawl tự gửi request POST tới endpoint bạn chỉ định khi có sự kiện.

Điểm chính: Đăng ký nhiều loại sự kiện, xác minh chữ ký HMAC-SHA256, thử lại tự động với backoff lũy thừa, lịch sử gửi và bảo vệ IP riêng.

Tính năng cốt lõi

  • Đăng ký sự kiện: scrape, crawl, map, search, tác vụ theo lịch và sự kiện hệ thống
  • Gửi an toàn: Xác minh chữ ký HMAC-SHA256 để đảm bảo tính xác thực
  • Thử lại tự động: Cơ chế backoff lũy thừa khi gửi thất bại
  • Theo dõi gửi: Toàn bộ lịch sử gửi webhook
  • Lọc phạm vi: Tất cả sự kiện hoặc chỉ các tác vụ cụ thể
  • Header tùy chỉnh: Thêm HTTP header tùy chỉnh cho request webhook
  • Bảo vệ IP riêng: Chống SSRF tích hợp

Endpoint API

POST   /v1/webhooks                              # Tạo đăng ký webhook
GET    /v1/webhooks                              # Liệt kê webhook
GET    /v1/webhooks/:webhookId                   # Chi tiết webhook
PUT    /v1/webhooks/:webhookId                   # Cập nhật webhook
DELETE /v1/webhooks/:webhookId                   # Xóa webhook
GET    /v1/webhooks/:webhookId/deliveries        # Lịch sử gửi
POST   /v1/webhooks/:webhookId/test              # Gửi webhook thử
PUT    /v1/webhooks/:webhookId/activate          # Kích hoạt webhook
PUT    /v1/webhooks/:webhookId/deactivate        # Vô hiệu hóa webhook
POST   /v1/webhooks/:webhookId/deliveries/:deliveryId/replay  # Gửi lại lần thất bại
GET    /v1/webhook-events                        # Danh sách sự kiện hỗ trợ

Sự kiện hỗ trợ

Sự kiện job

Sự kiệnMô tảKích hoạt khi
scrape.createdJob scrape đã tạoJob scrape mới xếp hàng
scrape.startedJob scrape đã bắt đầuJob bắt đầu thực thi
scrape.completedJob scrape hoàn thànhJob thành công
scrape.failedJob scrape thất bạiJob gặp lỗi
scrape.cancelledJob scrape đã hủyJob bị hủy thủ công
crawl.createdJob crawl đã tạoJob crawl mới xếp hàng
crawl.startedJob crawl đã bắt đầuJob bắt đầu thực thi
crawl.completedJob crawl hoàn thànhJob thành công
crawl.failedJob crawl thất bạiJob gặp lỗi
crawl.cancelledJob crawl đã hủyJob bị hủy thủ công

Sự kiện tác vụ theo lịch

Sự kiệnMô tảKích hoạt khi
task.executedTác vụ đã chạyTác vụ theo lịch thực thi
task.failedTác vụ thất bạiTác vụ theo lịch lỗi
task.pausedTác vụ tạm dừngTác vụ bị tạm dừng
task.resumedTác vụ tiếp tụcTác vụ được tiếp tục
Sự kiệnMô tảKích hoạt khi
search.createdJob search đã tạoJob search mới xếp hàng
search.startedJob search đã bắt đầuJob bắt đầu thực thi
search.completedJob search hoàn thànhJob thành công
search.failedJob search thất bạiJob gặp lỗi

Sự kiện map

Sự kiệnMô tảKích hoạt khi
map.createdJob map đã tạoJob map mới xếp hàng
map.startedJob map đã bắt đầuJob bắt đầu thực thi
map.completedJob map hoàn thànhJob thành công
map.failedJob map thất bạiJob gặp lỗi

Sự kiện thử

Sự kiệnMô tảKích hoạt khi
webhook.testSự kiện thửGửi webhook thử thủ công

Bắt đầu nhanh

Tạo webhook

curl -X POST "https://api.anycrawl.dev/v1/webhooks" \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production Notifications",
    "webhook_url": "https://your-domain.com/webhooks/anycrawl",
    "event_types": ["scrape.completed", "scrape.failed", "crawl.completed"],
    "scope": "all",
    "timeout_seconds": 10,
    "max_retries": 3
  }'

Phản hồi

{
  "success": true,
  "data": {
    "webhook_id": "webhook-uuid-here",
    "secret": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6",
    "message": "Webhook created successfully. Save the secret - it won't be shown again."
  }
}

Quan trọng: Lưu secret ngay! Chỉ hiển thị một lần khi tạo và cần để xác minh chữ ký.

Tham số request

Cấu hình webhook

Tham sốKiểuBắt buộcMặc địnhMô tả
namestring-Tên webhook (1-255 ký tự)
descriptionstringKhông-Mô tả webhook
webhook_urlstring-URL endpoint (khuyến nghị HTTPS)
event_typesstring[]-Mảng loại sự kiện đăng ký
scopestringKhông"all"Phạm vi: "all" hoặc "specific"
specific_task_idsstring[]Không-ID tác vụ (bắt buộc nếu scope"specific")

Cấu hình gửi

Tham sốKiểuBắt buộcMặc địnhMô tả
timeout_secondsnumberKhông10Timeout request (1-60 giây)
max_retriesnumberKhông3Số lần thử tối đa (0-10)
retry_backoff_multipliernumberKhông2Hệ số backoff (1-10)
custom_headersobjectKhông-HTTP header tùy chỉnh

Webhook tự vô hiệu sau 10 lỗi liên tiếp để tránh thử lại quá mức. Bạn có thể kích hoạt lại thủ công sau khi sửa lỗi.

Metadata

Tham sốKiểuBắt buộcMặc địnhMô tả
tagsstring[]Không-Thẻ tổ chức
metadataobjectKhông-Metadata tùy chỉnh

Định dạng payload webhook

HTTP Headers

Mọi request webhook kèm các header sau:

Content-Type: application/json
X-AnyCrawl-Signature: sha256=abc123...
X-Webhook-Event: scrape.completed
X-Webhook-Delivery-Id: delivery-uuid-1
X-Webhook-Timestamp: 2026-01-27T10:00:00.000Z

Ví dụ payload

scrape.completed

{
  "job_id": "job-uuid-1",
  "status": "completed",
  "url": "https://example.com",
  "total": 10,
  "completed": 10,
  "failed": 0,
  "credits_used": 5,
  "created_at": "2026-01-27T09:00:00.000Z",
  "completed_at": "2026-01-27T10:00:00.000Z"
}

scrape.failed

{
  "job_id": "job-uuid-1",
  "status": "failed",
  "url": "https://example.com",
  "error_message": "Connection timeout",
  "credits_used": 3,
  "created_at": "2026-01-27T09:00:00.000Z",
  "completed_at": "2026-01-27T10:00:00.000Z"
}

task.executed

{
  "task_id": "task-uuid-1",
  "task_name": "Daily News Scrape",
  "execution_id": "exec-uuid-1",
  "execution_number": 45,
  "status": "completed",
  "job_id": "job-uuid-1",
  "credits_used": 5,
  "scheduled_for": "2026-01-27T09:00:00.000Z",
  "completed_at": "2026-01-27T09:02:15.000Z"
}

Xác minh chữ ký

Vì sao cần xác minh?

Xác minh chữ ký đảm bảo request webhook thật sự từ AnyCrawl và không bị sửa đổi, chống request độc hại.

Thuật toán

AnyCrawl dùng HMAC-SHA256 để ký payload:

signature = HMAC-SHA256(payload, webhook_secret)
header_value = "sha256=" + hex(signature)

Ví dụ triển khai

Node.js / Express

const crypto = require('crypto');
const express = require('express');

function verifyWebhookSignature(payload, signature, secret) {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  const expectedSignature = `sha256=${hmac.digest('hex')}`;

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

const app = express();
app.use(express.json());

app.post('/webhooks/anycrawl', (req, res) => {
  const signature = req.headers['x-anycrawl-signature'];
  const secret = process.env.WEBHOOK_SECRET;

  // Verify signature
  if (!verifyWebhookSignature(req.body, signature, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Extract event info
  const eventType = req.headers['x-webhook-event'];
  const deliveryId = req.headers['x-webhook-delivery-id'];

  console.log(`Received event: ${eventType}`);
  console.log(`Delivery ID: ${deliveryId}`);
  console.log('Payload:', req.body);

  // Respond quickly (< 5 seconds recommended)
  res.status(200).json({ received: true });

  // Process asynchronously
  processWebhookAsync(eventType, req.body).catch(console.error);
});

app.listen(3000);

Python / Flask

import hmac
import hashlib
import json
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = 'your-webhook-secret-here'

def verify_webhook_signature(payload, signature, secret):
    expected_signature = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        json.dumps(payload).encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

@app.route('/webhooks/anycrawl', methods=['POST'])
def webhook_handler():
    signature = request.headers.get('X-AnyCrawl-Signature')
    payload = request.get_json()

    # Verify signature
    if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    # Extract event info
    event_type = request.headers.get('X-Webhook-Event')
    delivery_id = request.headers.get('X-Webhook-Delivery-Id')

    print(f'Received event: {event_type}')
    print(f'Delivery ID: {delivery_id}')
    print(f'Payload: {payload}')

    # Respond quickly
    return jsonify({'received': True}), 200

if __name__ == '__main__':
    app.run(port=3000)

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

func verifyWebhookSignature(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expectedSignature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-AnyCrawl-Signature")
    eventType := r.Header.Get("X-Webhook-Event")
    secret := os.Getenv("WEBHOOK_SECRET")

    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading body", http.StatusBadRequest)
        return
    }

    if !verifyWebhookSignature(body, signature, secret) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    var payload map[string]interface{}
    if err := json.Unmarshal(body, &payload); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    fmt.Printf("Received event: %s\n", eventType)
    fmt.Printf("Payload: %+v\n", payload)

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

func main() {
    http.HandleFunc("/webhooks/anycrawl", webhookHandler)
    http.ListenAndServe(":3000", nil)
}

Quản lý webhook

Liệt kê tất cả webhook

curl -X GET "https://api.anycrawl.dev/v1/webhooks" \
  -H "Authorization: Bearer <your-api-key>"

Phản hồi

{
  "success": true,
  "data": [
    {
      "uuid": "webhook-uuid-1",
      "name": "Production Notifications",
      "webhook_url": "https://your-domain.com/webhooks/anycrawl",
      "webhook_secret": "***hidden***",
      "event_types": ["scrape.completed", "scrape.failed"],
      "scope": "all",
      "is_active": true,
      "consecutive_failures": 0,
      "total_deliveries": 145,
      "successful_deliveries": 142,
      "failed_deliveries": 3,
      "last_success_at": "2026-01-27T10:00:00.000Z",
      "last_failure_at": "2026-01-26T15:30:00.000Z",
      "created_at": "2026-01-01T00:00:00.000Z"
    }
  ]
}

webhook_secret luôn bị ẩn trong danh sách và chi tiết vì lý do bảo mật.

Cập nhật webhook

curl -X PUT "https://api.anycrawl.dev/v1/webhooks/:webhookId" \
  -H "Authorization: Bearer <your-api-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "event_types": ["scrape.completed", "scrape.failed", "crawl.completed"]
  }'

Không thể cập nhật secret webhook. Để đổi, hãy xóa và tạo lại webhook.

Thử webhook

Gửi sự kiện thử để kiểm tra cấu hình:

curl -X POST "https://api.anycrawl.dev/v1/webhooks/:webhookId/test" \
  -H "Authorization: Bearer <your-api-key>"

Payload thử:

{
  "message": "This is a test webhook from AnyCrawl",
  "timestamp": "2026-01-27T10:00:00.000Z",
  "webhook_id": "webhook-uuid-1"
}

Vô hiệu / kích hoạt webhook

curl -X PUT "https://api.anycrawl.dev/v1/webhooks/:webhookId/deactivate" \
  -H "Authorization: Bearer <your-api-key>"

Xóa webhook

curl -X DELETE "https://api.anycrawl.dev/v1/webhooks/:webhookId" \
  -H "Authorization: Bearer <your-api-key>"

Xóa webhook cũng xóa toàn bộ lịch sử gửi.

Gửi lại lần thất bại

Thử lại thủ công một lần gửi thất bại:

curl -X POST "https://api.anycrawl.dev/v1/webhooks/:webhookId/deliveries/:deliveryId/replay" \
  -H "Authorization: Bearer <your-api-key>"

Phản hồi:

{
  "success": true,
  "message": "Delivery replayed successfully",
  "data": {
    "delivery_id": "delivery-uuid-1",
    "status": "pending"
  }
}

Gửi lại tạo một lần gửi mới với cùng payload. Hữu ích sau khi sửa endpoint.

Lịch sử gửi

Xem các lần gửi

curl -X GET "https://api.anycrawl.dev/v1/webhooks/:webhookId/deliveries?limit=20" \
  -H "Authorization: Bearer <your-api-key>"

Tham số truy vấn

Tham sốKiểuMặc địnhMô tả
limitnumber100Số lần gửi trả về
offsetnumber0Số bản ghi bỏ qua
statusstring-Lọc theo trạng thái: delivered, failed, retrying
fromstring-Ngày bắt đầu (ISO 8601)
tostring-Ngày kết thúc (ISO 8601)

Phản hồi

{
  "success": true,
  "data": [
    {
      "uuid": "delivery-uuid-1",
      "webhookSubscriptionUuid": "webhook-uuid-1",
      "eventType": "scrape.completed",
      "status": "delivered",
      "attempt_number": 1,
      "request_url": "https://your-domain.com/webhooks/anycrawl",
      "request_method": "POST",
      "response_status": 200,
      "response_duration_ms": 125,
      "created_at": "2026-01-27T10:00:00.000Z",
      "delivered_at": "2026-01-27T10:00:00.125Z"
    },
    {
      "uuid": "delivery-uuid-2",
      "status": "failed",
      "attempt_number": 3,
      "error_message": "Connection timeout",
      "error_code": "ETIMEDOUT",
      "created_at": "2026-01-27T09:00:00.000Z"
    }
  ],
  "meta": {
    "limit": 20,
    "offset": 0,
    "filters": {
      "status": null,
      "from": null,
      "to": null
    }
  }
}

Cơ chế thử lại

Khi nào thử lại

Webhook được thử lại khi:

  • Mã HTTP không phải 2xx
  • Hết thời gian kết nối
  • Lỗi mạng

Lịch thử lại

Với mặc định (max_retries: 3, retry_backoff_multiplier: 2):

Lần thửĐộ trễThời gian sau lần đầu
Lần 11 phút1 phút
Lần 22 phút3 phút
Lần 34 phút7 phút

Công thức độ trễ: backoff_multiplier ^ (attempt - 1) × 1 phút

Tự vô hiệu hóa

Webhook tự vô hiệu sau 10 lỗi liên tiếp để tránh thử lại quá mức.

Để bật lại:

curl -X PUT "https://api.anycrawl.dev/v1/webhooks/:webhookId/activate" \
  -H "Authorization: Bearer <your-api-key>"

Lọc phạm vi

Mọi sự kiện (scope: "all")

Nhận thông báo cho mọi sự kiện thuộc các loại đã đăng ký:

{
  "scope": "all",
  "event_types": ["scrape.completed", "crawl.completed"]
}

Tác vụ cụ thể (scope: "specific")

Chỉ nhận thông báo cho các tác vụ theo lịch được chỉ định:

{
  "scope": "specific",
  "specific_task_ids": ["task-uuid-1", "task-uuid-2"],
  "event_types": ["task.executed", "task.failed"]
}

Bảo vệ IP riêng

Hành vi mặc định

AnyCrawl chặn gửi webhook tới địa chỉ IP riêng:

  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
  • 169.254.0.0/16 (link-local)
  • 127.0.0.1 / localhost
  • Địa chỉ IPv6 riêng

Cho phép webhook cục bộ (chỉ thử nghiệm)

Khi phát triển cục bộ, đặt:

ALLOW_LOCAL_WEBHOOKS=true

Không bật tùy chọn này trên môi trường production — rủi ro bảo mật nghiêm trọng.

Thực hành tốt

1. Phản hồi nhanh

Trả mã trạng thái 2xx trong 5 giây:

app.post('/webhook', async (req, res) => {
  // Verify signature
  if (!verifySignature(req.body, req.headers['x-anycrawl-signature'])) {
    return res.status(401).send('Invalid signature');
  }

  // Quick acknowledgment
  res.status(200).json({ received: true });

  // Process asynchronously
  queue.add('process-webhook', req.body);
});

2. Idempotent

Dùng X-Webhook-Delivery-Id để tránh xử lý trùng:

const processedDeliveries = new Set();

app.post('/webhook', (req, res) => {
  const deliveryId = req.headers['x-webhook-delivery-id'];

  if (processedDeliveries.has(deliveryId)) {
    return res.status(200).json({ received: true, duplicate: true });
  }

  processedDeliveries.add(deliveryId);

  // Process event...

  res.status(200).json({ received: true });
});

3. Mã trạng thái phù hợp

Mô tảHành vi AnyCrawl
200-299Thành côngKhông thử lại
400-499Lỗi phía clientKhông thử lại (ghi nhận thất bại)
500-599Lỗi máy chủThử lại với backoff
TimeoutHết thời gian mạngThử lại với backoff

4. Ghi log mọi hoạt động webhook

app.post('/webhook', (req, res) => {
  const deliveryId = req.headers['x-webhook-delivery-id'];
  const eventType = req.headers['x-webhook-event'];

  logger.info('Webhook received', {
    deliveryId,
    eventType,
    timestamp: req.headers['x-webhook-timestamp']
  });

  try {
    processWebhook(req.body, eventType);
    logger.info('Webhook processed', { deliveryId });
    res.status(200).json({ received: true });
  } catch (error) {
    logger.error('Webhook failed', {
      deliveryId,
      error: error.message
    });
    res.status(500).json({ error: 'Processing failed' });
  }
});

5. Checklist bảo mật

  • ✅ Luôn xác minh chữ ký
  • ✅ Dùng HTTPS trên production
  • ✅ Không để lộ secret trong URL
  • ✅ Giới hạn tốc độ (rate limiting)
  • ✅ Giám sát bất thường
  • ✅ Kiểm tra cấu trúc payload

Tình huống dùng phổ biến

Thông báo Slack

Gửi kết quả scrape lên Slack:

app.post('/webhooks/anycrawl', async (req, res) => {
  const { job_id, status, url } = req.body;

  await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `Job ${status}: ${url}\nJob ID: ${job_id}`
    })
  });

  res.status(200).json({ received: true });
});

Cảnh báo email

Gửi email khi thất bại:

app.post('/webhooks/anycrawl', async (req, res) => {
  const eventType = req.headers['x-webhook-event'];

  if (eventType.endsWith('.failed')) {
    await sendEmail({
      to: 'admin@example.com',
      subject: 'AnyCrawl Job Failed',
      body: JSON.stringify(req.body, null, 2)
    });
  }

  res.status(200).json({ received: true });
});

Ghi cơ sở dữ liệu

Lưu sự kiện webhook vào database:

app.post('/webhooks/anycrawl', async (req, res) => {
  const eventType = req.headers['x-webhook-event'];
  const deliveryId = req.headers['x-webhook-delivery-id'];

  await db.webhookEvents.create({
    deliveryId,
    eventType,
    payload: req.body,
    receivedAt: new Date()
  });

  res.status(200).json({ received: true });
});

Xử lý sự cố

Webhook không nhận sự kiện

Kiểm tra:

  • Webhook có đang bật? (is_active: true)
  • Loại sự kiện đã cấu hình đúng?
  • URL webhook có truy cập được từ internet?
  • Có bị chặn bởi bảo vệ IP riêng?
  • Phạm vi (all vs specific)

Xác minh chữ ký thất bại

Thường gặp:

  • Secret sai (đối chiếu phản hồi lúc tạo webhook)
  • Không stringify payload trước khi hash
  • JSON thừa khoảng trắng hoặc định dạng khác
  • Thuật toán HMAC sai (phải SHA-256)

Tỷ lệ thất bại cao

Cách xử lý:

  • Endpoint phản hồi trong 5 giây
  • Trả đúng mã HTTP
  • Xem thông báo lỗi trong lịch sử gửi
  • Thử cục bộ với ngrok hoặc công cụ tương tự

Webhook tự tắt

Nguyên nhân: 10 lỗi liên tiếp

Cách xử lý:

  1. Sửa nguyên nhân (endpoint, xác minh chữ ký, v.v.)
  2. Thử bằng endpoint test
  3. Kích hoạt lại webhook:
curl -X PUT "https://api.anycrawl.dev/v1/webhooks/:webhookId/activate" \
  -H "Authorization: Bearer <your-api-key>"

Công cụ gỡ lỗi

Công cụ thử

Phát triển cục bộ

Dùng ngrok để expose máy chủ cục bộ:

ngrok http 3000

Sau đó dùng URL ngrok làm webhook URL:

https://abc123.ngrok.io/webhooks/anycrawl

Giới hạn

MụcGiới hạn
Độ dài tên webhook1-255 ký tự
URL webhookKhuyến nghị HTTPS (production)
Timeout1-60 giây
Số lần thử tối đa0-10
Kích thước payloadTối đa 1MB
Header tùy chỉnhTối đa 20
Loại sự kiện mỗi webhookKhông giới hạn

Tài liệu liên quan