AnyCrawl

Webhooks

スクレイプ、クロール、サイトマップ、検索、スケジュールタスクなど、AnyCrawl のイベントをリアルタイムで受け取ります。

はじめに

Webhook では、AnyCrawl アカウントでイベントが発生したときに、HTTP でリアルタイム通知を受け取れます。ポーリングは不要で、イベントごとに指定したエンドポイントへ AnyCrawl が POST します。

主な特徴: 複数イベント種別の購読、HMAC-SHA256 署名検証、指数バックオフ付き自動再試行、配信履歴、プライベート IP 保護。

主な機能

  • イベント購読: スクレイプ、クロール、サイトマップ、検索、スケジュールタスク、システムイベント
  • 安全な配信: HMAC-SHA256 で真正性を検証
  • 自動再試行: 失敗時は指数バックオフで再試行
  • 配信の追跡: すべての Webhook 配信履歴
  • スコープ: すべてのイベント、または特定タスクのみ
  • カスタムヘッダー: Webhook リクエストに任意の HTTP ヘッダーを追加
  • プライベート IP 保護: SSRF 対策を内蔵

API エンドポイント

POST   /v1/webhooks                              # 購読を作成
GET    /v1/webhooks                              # 一覧
GET    /v1/webhooks/:webhookId                   # 詳細
PUT    /v1/webhooks/:webhookId                   # 更新
DELETE /v1/webhooks/:webhookId                   # 削除
GET    /v1/webhooks/:webhookId/deliveries        # 配信履歴
POST   /v1/webhooks/:webhookId/test              # テスト送信
PUT    /v1/webhooks/:webhookId/activate          # 有効化
PUT    /v1/webhooks/:webhookId/deactivate        # 無効化
POST   /v1/webhooks/:webhookId/deliveries/:deliveryId/replay  # 失敗分の再送
GET    /v1/webhook-events                        # 対応イベント一覧

対応イベント

ジョブ

イベント説明発火タイミング
scrape.createdスクレイプジョブ作成キューに入ったとき
scrape.startedスクレイプ開始実行が始まったとき
scrape.completedスクレイプ完了成功終了
scrape.failedスクレイプ失敗エラー発生
scrape.cancelledスクレイプ取消手動キャンセル
crawl.createdクロールジョブ作成キューに入ったとき
crawl.startedクロール開始実行が始まったとき
crawl.completedクロール完了成功終了
crawl.failedクロール失敗エラー発生
crawl.cancelledクロール取消手動キャンセル

スケジュールタスク

イベント説明発火タイミング
task.executedタスク実行スケジュール実行時
task.failedタスク失敗失敗時
task.paused一時停止停止されたとき
task.resumed再開再開されたとき

検索

イベント説明発火タイミング
search.created検索ジョブ作成キューに入ったとき
search.started検索開始実行が始まったとき
search.completed検索完了成功終了
search.failed検索失敗エラー発生

サイトマップ(map)

イベント説明発火タイミング
map.createdmap ジョブ作成キューに入ったとき
map.startedmap 開始実行が始まったとき
map.completedmap 完了成功終了
map.failedmap 失敗エラー発生

テスト

イベント説明発火タイミング
webhook.testテスト手動テスト送信時

クイックスタート

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
  }'

レスポンス

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

重要: secret は作成直後に必ず保存してください。表示は一度きりで、署名検証に必要です。

リクエストパラメータ

Webhook 設定

パラメータ必須既定値説明
namestringはい-名前(1〜255 文字)
descriptionstringいいえ-説明
webhook_urlstringはい-受信 URL(本番は HTTPS 推奨)
event_typesstring[]はい-購読するイベント種別
scopestringいいえ"all""all" または "specific"
specific_task_idsstring[]いいえ-scope"specific" のとき必須のタスク ID

配信設定

パラメータ必須既定値説明
timeout_secondsnumberいいえ10タイムアウト(1〜60 秒)
max_retriesnumberいいえ3最大再試行回数(0〜10)
retry_backoff_multipliernumberいいえ2バックオフ倍率(1〜10)
custom_headersobjectいいえ-任意の HTTP ヘッダー

連続 10 回失敗すると Webhook は自動で無効化され、過剰な再試行を防ぎます。原因を直したら手動で再有効化できます。

メタデータ

パラメータ必須既定値説明
tagsstring[]いいえ-整理用タグ
metadataobjectいいえ-任意のメタデータ

Webhook ペイロード形式

HTTP ヘッダー

各リクエストに含まれるヘッダー例:

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

ペイロード例

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"
}

署名の検証

なぜ検証するか

署名が正しければ、リクエストが AnyCrawl 由来で改ざんされていないことを確認できます。

アルゴリズム

ペイロードは HMAC-SHA256 で署名されます。

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

実装例

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;

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

  // イベント情報
  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);

  // すぐに応答(5 秒以内推奨)
  res.status(200).json({ received: true });

  // 本体処理は非同期
  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()

    # 署名を検証
    if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    # イベント情報
    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}')

    # すぐに応答
    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)
}

Webhook の管理

一覧

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

レスポンス

{
  "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 は一覧・詳細では常にマスクされます。

更新

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"]
  }'

シークレットは更新できません。変更する場合は Webhook を削除して作り直してください。

テスト

設定を確認するためテストイベントを送ります。

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

テストペイロード:

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

無効化 / 有効化

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

削除

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

Webhook を削除すると、配信履歴もすべて削除されます。

失敗した配信の再送

失敗した配信を手動で再試行します。

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

レスポンス:

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

再送は同じペイロードで新しい配信試行を作ります。エンドポイント修正後に失敗分をやり直すのに便利です。

配信履歴

一覧取得

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

クエリパラメータ

パラメータ既定値説明
limitnumber100取得件数
offsetnumber0スキップ件数
statusstring-deliveredfailedretrying でフィルター
fromstring-開始日(ISO 8601)
tostring-終了日(ISO 8601)

レスポンス

{
  "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
    }
  }
}

再試行

いつ再試行されるか

次の場合に再試行されます。

  • HTTP ステータスが 2xx 以外
  • 接続タイムアウト
  • ネットワークエラー

スケジュール

既定(max_retries: 3retry_backoff_multiplier: 2)の例:

試行遅延初回からの経過
1 回目の再試行1 分1 分
2 回目の再試行2 分3 分
3 回目の再試行4 分7 分

遅延は backoff_multiplier ^ (attempt - 1) × 1 分 で計算されます。

自動無効化

連続 10 回失敗すると Webhook は自動で無効化され、過剰な再試行を防ぎます。

再有効化:

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

スコープ

すべてのイベント(scope: "all"

購読した種別のイベントをすべて受け取ります。

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

特定タスク(scope: "specific"

指定したスケジュールタスクの通知だけを受け取ります。

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

プライベート IP 保護

既定の動作

AnyCrawl はプライベート IP への Webhook 配信をブロックします。

  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
  • 169.254.0.0/16(リンクローカル)
  • 127.0.0.1 / localhost
  • IPv6 のプライベートアドレス

ローカル Webhook を許可(テストのみ)

ローカル開発時:

ALLOW_LOCAL_WEBHOOKS=true

本番では有効にしないでください。重大なセキュリティリスクがあります。

ベストプラクティス

1. すぐに応答する

5 秒以内に 2xx を返してください。

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

  // すぐに受領応答
  res.status(200).json({ received: true });

  // 本体は非同期
  queue.add('process-webhook', req.body);
});

2. 冪等性

X-Webhook-Delivery-Id で重複処理を防ぎます。

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);

  // イベント処理…

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

3. 適切なステータスコード

ステータス意味AnyCrawl の挙動
200-299成功再試行しない
400-499クライアントエラー再試行しない(失敗として記録)
500-599サーバーエラーバックオフで再試行
タイムアウトネットワークバックオフで再試行

4. ログを残す

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. セキュリティチェックリスト

  • ✅ 署名を必ず検証する
  • ✅ 本番は HTTPS を使う
  • ✅ シークレットを URL に含めない
  • ✅ レート制限を実装する
  • ✅ 異常を監視する
  • ✅ ペイロード構造を検証する

よくある使い方

Slack 通知

スクレイプ結果を 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 });
});

メール通知

失敗時にメールを送る例:

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 });
});

DB に記録

Webhook を DB に保存する例:

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 });
});

トラブルシューティング

イベントが届かない

確認:

  • is_active: true
  • event_types は正しいか
  • URL はインターネットから到達できるか
  • プライベート IP 保護でブロックされていないか
  • scope(all / specific)は意図どおりか

署名検証が失敗する

よくある原因:

  • シークレットが違う(作成時のレスポンスを確認)
  • ハッシュ前にペイロードを文字列化していない
  • JSON の空白・改行が一致していない
  • HMAC が SHA-256 以外

失敗が多い

対処:

  • 5 秒以内に応答できるか
  • 適切な HTTP ステータスを返しているか
  • 配信履歴のエラーメッセージを確認
  • ngrok などでローカル検証

自動無効化された

原因: 連続 10 回失敗

対処:

  1. エンドポイント・署名・ネットワークなど根本原因を修正
  2. テスト API で動作確認
  3. Webhook を再有効化:
curl -X PUT "https://api.anycrawl.dev/v1/webhooks/:webhookId/activate" \
  -H "Authorization: Bearer <your-api-key>"

デバッグ用ツール

外部サービス

ローカル開発

ngrok でローカルサーバーを公開:

ngrok http 3000

表示された ngrok URL を Webhook URL に設定:

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

制限

項目制限
名前の長さ1〜255 文字
Webhook URL本番は HTTPS 推奨
タイムアウト1〜60 秒
最大再試行0〜10
ペイロードサイズ最大 1MB
カスタムヘッダー最大 20 個
イベント種別数上限なし

関連ドキュメント