Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

Webhook

最終更新 Markdown で表示Agent セットアップ

外部サービスの webhook イベントを受け取り、専用のエージェントインスタンスへ振り分けます。Webhook の送信元(リポジトリ、顧客、デバイス)ごとに、分離された状態、永続ストレージ、リアルタイムのクライアント接続を持つエージェントを置けます。

クイックスタート

import { Agent, getAgentByName, routeAgentRequest } from "agents";

export class WebhookAgent extends Agent {
	async onRequest(request) {
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		const rawBody = await request.text();
		const signature = request.headers.get("X-Hub-Signature-256");
		if (
			!(await verifyGitHubWebhook(rawBody, signature, this.env.WEBHOOK_SECRET))
		) {
			return new Response("Invalid signature", { status: 401 });
		}

		let payload;
		try {
			payload = JSON.parse(rawBody);
		} catch {
			return new Response("Invalid payload", { status: 400 });
		}

		await this.processEvent(payload);
		return new Response("OK");
	}

	async processEvent(payload) {
		// Store event, update state, trigger actions...
	}
}

async function verifyGitHubWebhook(rawBody, signature, secret) {
	if (!signature || !/^sha256=[0-9a-f]{64}$/i.test(signature)) return false;

	const encoder = new TextEncoder();
	const key = await crypto.subtle.importKey(
		"raw",
		encoder.encode(secret),
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["verify"],
	);
	const signatureBytes = Uint8Array.from(
		signature.slice("sha256=".length).match(/.{2}/g) ?? [],
		(byte) => Number.parseInt(byte, 16),
	);

	return crypto.subtle.verify(
		"HMAC",
		key,
		signatureBytes,
		encoder.encode(rawBody),
	);
}

export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		if (url.pathname === "/webhooks/github" && request.method === "POST") {
			const rawBody = await request.clone().text();
			const signature = request.headers.get("X-Hub-Signature-256");
			if (
				!(await verifyGitHubWebhook(rawBody, signature, env.WEBHOOK_SECRET))
			) {
				return new Response("Invalid signature", { status: 401 });
			}

			let payload;
			try {
				payload = JSON.parse(rawBody);
			} catch {
				return new Response("Invalid payload", { status: 400 });
			}

			const repository = payload.repository?.full_name;
			if (!repository) {
				return new Response("Missing repository", { status: 400 });
			}

			const agentName = repository.toLowerCase().replace(/\//g, "-");
			const agent = await getAgentByName(env.WebhookAgent, agentName);
			return agent.fetch(request);
		}

		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
};
import { Agent, getAgentByName, routeAgentRequest } from "agents";

export class WebhookAgent extends Agent<Env> {
	async onRequest(request: Request): Promise<Response> {
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		const rawBody = await request.text();
		const signature = request.headers.get("X-Hub-Signature-256");
		if (
			!(await verifyGitHubWebhook(
				rawBody,
				signature,
				this.env.WEBHOOK_SECRET,
			))
		) {
			return new Response("Invalid signature", { status: 401 });
		}

		let payload: unknown;
		try {
			payload = JSON.parse(rawBody);
		} catch {
			return new Response("Invalid payload", { status: 400 });
		}

		await this.processEvent(payload);
		return new Response("OK");
	}

	private async processEvent(payload: unknown) {
		// Store event, update state, trigger actions...
	}
}

async function verifyGitHubWebhook(
	rawBody: string,
	signature: string | null,
	secret: string,
): Promise<boolean> {
	if (!signature || !/^sha256=[0-9a-f]{64}$/i.test(signature)) return false;

	const encoder = new TextEncoder();
	const key = await crypto.subtle.importKey(
		"raw",
		encoder.encode(secret),
		{ name: "HMAC", hash: "SHA-256" },
		false,
		["verify"],
	);
	const signatureBytes = Uint8Array.from(
		signature.slice("sha256=".length).match(/.{2}/g) ?? [],
		(byte) => Number.parseInt(byte, 16),
	);

	return crypto.subtle.verify(
		"HMAC",
		key,
		signatureBytes,
		encoder.encode(rawBody),
	);
}

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		if (url.pathname === "/webhooks/github" && request.method === "POST") {
			const rawBody = await request.clone().text();
			const signature = request.headers.get("X-Hub-Signature-256");
			if (
				!(await verifyGitHubWebhook(
					rawBody,
					signature,
					env.WEBHOOK_SECRET,
				))
			) {
				return new Response("Invalid signature", { status: 401 });
			}

			let payload: { repository?: { full_name?: string } };
			try {
				payload = JSON.parse(rawBody);
			} catch {
				return new Response("Invalid payload", { status: 400 });
			}

			const repository = payload.repository?.full_name;
			if (!repository) {
				return new Response("Missing repository", { status: 400 });
			}

			const agentName = repository.toLowerCase().replace(/\//g, "-");
			const agent = await getAgentByName(env.WebhookAgent, agentName);
			return agent.fetch(request);
		}

		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

ユースケース

Webhook とエージェントを組み合わせると、外部のエンティティごとに、分離された状態付きエージェントインスタンスを割り当てられます。

開発者向けツール

ユースケース 説明
GitHub リポジトリ監視 リポジトリごとに 1 つのエージェントを置き、コミット、PR、Issue、スターを追跡します
CI/CD パイプライン ビルド / デプロイイベントに反応し、失敗を通知し、デプロイ履歴を追跡します
Linear / Jira 追跡 Issue を自動で振り分け、内容に応じて担当を決め、解決までの時間を追跡します

Eコマースと決済

ユースケース 説明
Stripe 顧客エージェント 顧客ごとに 1 つのエージェントを置き、支払い、サブスクリプション、異議を追跡します
Shopify 注文エージェント 作成からフルフィルメントまでの注文ライフサイクルと、在庫同期を扱います
支払い照合 webhook イベントを内部レコードと照合し、不一致にフラグを付けます

コミュニケーションと通知

ユースケース 説明
Twilio SMS / Voice 着信メッセージや通話をきっかけに動く会話エージェント
Slack Bot スラッシュコマンド、ボタンクリック、インタラクティブメッセージに応答します
メール追跡 SendGrid / Mailgun の配信イベント、バウンス処理、エンゲージメント分析

IoT とインフラ

ユースケース 説明
デバイステレメトリ デバイスごとに 1 つのエージェントを置き、センサーデータストリームを処理します
アラート集約 PagerDuty、Datadog、独自モニタリングからのアラートを集めます
ホームオートメーション 永続状態を保ちながら、IFTTT / Zapier のトリガーに反応します

SaaS 連携

ユースケース 説明
CRM 同期 Salesforce / HubSpot の連絡先と案件の更新
カレンダーエージェント Google Calendar のイベント通知とスケジュール調整
フォーム送信 Typeform、Tally、独自フォームの webhook と、その後のフォローアップ

エージェントへの webhook 振り分け

要点は、解析する前に生リクエストを検証し、認証済みペイロードから Agent の識別子を導出することです。ボディの署名は、無関係な URL セグメントや任意のヘッダーを認証しません。

ペイロードからエンティティを取り出す

多くの webhook には、ペイロード内に識別子が含まれます。

export default {
	async fetch(request, env) {
		const url = new URL(request.url);
		if (request.method === "POST" && url.pathname === "/webhooks/github") {
			const rawBody = await request.clone().text();
			const signature = request.headers.get("X-Hub-Signature-256");
			if (
				!(await verifyGitHubWebhook(rawBody, signature, env.WEBHOOK_SECRET))
			) {
				return new Response("Invalid signature", { status: 401 });
			}

			let payload;
			try {
				payload = JSON.parse(rawBody);
			} catch {
				return new Response("Invalid payload", { status: 400 });
			}

			const repository = payload.repository?.full_name;
			if (!repository) {
				return new Response("Missing repository", { status: 400 });
			}

			const agentName = repository.toLowerCase().replace(/\//g, "-");
			const agent = await getAgentByName(env.RepoAgent, agentName);
			return agent.fetch(request);
		}
	},
};
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		if (request.method === "POST" && url.pathname === "/webhooks/github") {
			const rawBody = await request.clone().text();
			const signature = request.headers.get("X-Hub-Signature-256");
			if (
				!(await verifyGitHubWebhook(
					rawBody,
					signature,
					env.WEBHOOK_SECRET,
				))
			) {
				return new Response("Invalid signature", { status: 401 });
			}

			let payload: { repository?: { full_name?: string } };
			try {
				payload = JSON.parse(rawBody);
			} catch {
				return new Response("Invalid payload", { status: 400 });
			}

			const repository = payload.repository?.full_name;
			if (!repository) {
				return new Response("Missing repository", { status: 400 });
			}

			const agentName = repository.toLowerCase().replace(/\//g, "-");
			const agent = await getAgentByName(env.RepoAgent, agentName);
			return agent.fetch(request);
		}
	},
} satisfies ExportedHandler<Env>;

URL 内のエンティティ ID を検証する

プロバイダーのボディ署名は、webhook の URL を認証しません。URL にエンティティ ID が含まれる場合は、検証済みプロバイダーペイロードの対応する識別子と比較し、不一致なら getAgentByName() を呼ぶ前に拒否します。

検証済みボディから Slack の識別子を導出する

Slack は、認証済みの X-Slack-Team-Id 振り分けヘッダーを送りません。タイムスタンプ付き署名とリプレイウィンドウを生ボディに対して検証し、検証済みのイベントまたはフォームボディから team_id を読み取ります。

署名の検証

ペイロードを信頼したり処理したりする前に、必ず webhook の署名を検証します。

GitHub の HMAC-SHA256 パターン

クイックスタートの verifyGitHubWebhook() ヘルパーは、生ボディに対する GitHub の sha256=<hex> 署名を crypto.subtle.verify() で検証します。この形式は GitHub 固有です。他のプロバイダーは、署名のエンコード、署名対象、タイムスタンプチェック、リプレイ対策が異なります。主な webhook プロバイダー にリンクしたドキュメントに従ってください。

プロバイダー固有のヘッダー

プロバイダー 署名ヘッダー アルゴリズム
GitHub X-Hub-Signature-256 HMAC-SHA256
Stripe Stripe-Signature HMAC-SHA256(タイムスタンプ付き)
Twilio X-Twilio-Signature HMAC-SHA1
Slack X-Slack-Signature HMAC-SHA256(タイムスタンプ付き)
Shopify X-Shopify-Hmac-Sha256 HMAC-SHA256(base64)

webhook の処理

onRequest ハンドラー

エージェントで受信 webhook を扱うときは onRequest() を使います。Worker 側でまだ検証していない場合は、ボディを解析する前に検証します。次の例は、クイックスタートの verifyGitHubWebhook() ヘルパーを再利用します。

export class WebhookAgent extends Agent {
	async onRequest(request) {
		// 1. Validate method
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		// 2. Get the GitHub event type
		const eventType = request.headers.get("X-GitHub-Event") ?? "unknown";

		// 3. Verify the GitHub signature
		const signature = request.headers.get("X-Hub-Signature-256");
		const body = await request.text();

		if (
			!(await verifyGitHubWebhook(body, signature, this.env.WEBHOOK_SECRET))
		) {
			return new Response("Invalid signature", { status: 401 });
		}

		// 4. Parse and process
		const payload = JSON.parse(body);
		await this.handleEvent(eventType, payload);

		// 5. Respond quickly
		return new Response("OK", { status: 200 });
	}

	async handleEvent(type, payload) {
		// Update state (broadcasts to connected clients)
		this.setState({
			...this.state,
			lastEventType: type,
			lastEventTime: new Date().toISOString(),
		});

		// Store in SQL for history
		this
			.sql`INSERT INTO events (type, payload, timestamp) VALUES (${type}, ${JSON.stringify(payload)}, ${Date.now()})`;
	}
}
export class WebhookAgent extends Agent {
	async onRequest(request: Request): Promise<Response> {
		// 1. Validate method
		if (request.method !== "POST") {
			return new Response("Method not allowed", { status: 405 });
		}

		// 2. Get the GitHub event type
		const eventType = request.headers.get("X-GitHub-Event") ?? "unknown";

		// 3. Verify the GitHub signature
		const signature = request.headers.get("X-Hub-Signature-256");
		const body = await request.text();

		if (
			!(await verifyGitHubWebhook(body, signature, this.env.WEBHOOK_SECRET))
		) {
			return new Response("Invalid signature", { status: 401 });
		}

		// 4. Parse and process
		const payload = JSON.parse(body);
		await this.handleEvent(eventType, payload);

		// 5. Respond quickly
		return new Response("OK", { status: 200 });
	}

	private async handleEvent(type: string, payload: unknown) {
		// Update state (broadcasts to connected clients)
		this.setState({
			...this.state,
			lastEventType: type,
			lastEventTime: new Date().toISOString(),
		});

		// Store in SQL for history
		this
			.sql`INSERT INTO events (type, payload, timestamp) VALUES (${type}, ${JSON.stringify(payload)}, ${Date.now()})`;
	}
}

webhook イベントの保存

履歴と再実行のために、SQLite へ webhook イベントを永続化します。

イベントテーブルのスキーマ

class WebhookAgent extends Agent {
	async onStart() {
		this.sql`
      CREATE TABLE IF NOT EXISTS events (
        id TEXT PRIMARY KEY,
        type TEXT NOT NULL,
        action TEXT,
        title TEXT NOT NULL,
        description TEXT,
        url TEXT,
        actor TEXT,
        payload TEXT,
        timestamp TEXT NOT NULL
      )
    `;

		this.sql`
      CREATE INDEX IF NOT EXISTS idx_events_timestamp
      ON events(timestamp DESC)
    `;
	}
}
class WebhookAgent extends Agent {
	async onStart(): Promise<void> {
		this.sql`
      CREATE TABLE IF NOT EXISTS events (
        id TEXT PRIMARY KEY,
        type TEXT NOT NULL,
        action TEXT,
        title TEXT NOT NULL,
        description TEXT,
        url TEXT,
        actor TEXT,
        payload TEXT,
        timestamp TEXT NOT NULL
      )
    `;

		this.sql`
      CREATE INDEX IF NOT EXISTS idx_events_timestamp
      ON events(timestamp DESC)
    `;
	}
}

古いイベントの削除

直近のイベントだけを残し、無制限な増加を防ぎます。

// Keep last 100 events
this.sql`
  DELETE FROM events WHERE id NOT IN (
    SELECT id FROM events ORDER BY timestamp DESC LIMIT 100
  )
`;

// Or delete events older than 30 days
this.sql`
  DELETE FROM events
  WHERE timestamp < datetime('now', '-30 days')
`;
// Keep last 100 events
this.sql`
  DELETE FROM events WHERE id NOT IN (
    SELECT id FROM events ORDER BY timestamp DESC LIMIT 100
  )
`;

// Or delete events older than 30 days
this.sql`
  DELETE FROM events
  WHERE timestamp < datetime('now', '-30 days')
`;

イベントのクエリ

import { Agent, callable } from "agents";

class WebhookAgent extends Agent {
	@callable()
	getEvents(limit = 20) {
		return [
			...this.sql`
      SELECT * FROM events
      ORDER BY timestamp DESC
      LIMIT ${limit}
    `,
		];
	}

	@callable()
	getEventsByType(type, limit = 20) {
		return [
			...this.sql`
      SELECT * FROM events
      WHERE type = ${type}
      ORDER BY timestamp DESC
      LIMIT ${limit}
    `,
		];
	}
}
import { Agent, callable } from "agents";

class WebhookAgent extends Agent {
	@callable()
	getEvents(limit = 20) {
		return [
			...this.sql`
      SELECT * FROM events
      ORDER BY timestamp DESC
      LIMIT ${limit}
    `,
		];
	}

	@callable()
	getEventsByType(type: string, limit = 20) {
		return [
			...this.sql`
      SELECT * FROM events
      WHERE type = ${type}
      ORDER BY timestamp DESC
      LIMIT ${limit}
    `,
		];
	}
}

リアルタイム配信

webhook が届いたらエージェントの状態を更新します。接続中の WebSocket クライアントへ自動で配信されます。

class WebhookAgent extends Agent {
	async processWebhook(eventType, payload) {
		// Update state - this automatically broadcasts to all connected clients
		this.setState({
			...this.state,
			stats: payload.stats,
			lastEvent: {
				type: eventType,
				timestamp: new Date().toISOString(),
			},
		});
	}
}
class WebhookAgent extends Agent {
	private async processWebhook(eventType: string, payload: WebhookPayload) {
		// Update state - this automatically broadcasts to all connected clients
		this.setState({
			...this.state,
			stats: payload.stats,
			lastEvent: {
				type: eventType,
				timestamp: new Date().toISOString(),
			},
		});
	}
}

クライアント側は次のとおりです。

import { useAgent } from "agents/react";

function Dashboard() {
	const [state, setState] = useState(null);

	const agent = useAgent({
		agent: "webhook-agent",
		name: "my-entity-id",
		onStateUpdate: (newState) => {
			setState(newState); // Automatically updates when webhooks arrive
		},
	});

	return <div>Last event: {state?.lastEvent?.type}</div>;
}

パターン

イベントの重複排除

イベント ID を使い、同じイベントを二重処理しないようにします。

class WebhookAgent extends Agent {
	async handleEvent(eventId, payload) {
		// Check if already processed
		const existing = [
			...this.sql`
      SELECT id FROM events WHERE id = ${eventId}
    `,
		];

		if (existing.length > 0) {
			console.log(`Event ${eventId} already processed, skipping`);
			return;
		}

		// Process and store
		await this.processPayload(payload);
		this.sql`INSERT INTO events (id, ...) VALUES (${eventId}, ...)`;
	}
}
class WebhookAgent extends Agent {
	async handleEvent(eventId: string, payload: unknown) {
		// Check if already processed
		const existing = [
			...this.sql`
      SELECT id FROM events WHERE id = ${eventId}
    `,
		];

		if (existing.length > 0) {
			console.log(`Event ${eventId} already processed, skipping`);
			return;
		}

		// Process and store
		await this.processPayload(payload);
		this.sql`INSERT INTO events (id, ...) VALUES (${eventId}, ...)`;
	}
}

素早く応答し、非同期で処理する

Webhook プロバイダーは、速い応答を期待します。重い処理にはキューを使います。

class WebhookAgent extends Agent {
	async onRequest(request) {
		const payload = await request.json();

		// Quick validation
		if (!this.isValid(payload)) {
			return new Response("Invalid", { status: 400 });
		}

		// Queue heavy processing
		await this.queue("processWebhook", payload);

		// Respond immediately
		return new Response("Accepted", { status: 202 });
	}

	async processWebhook(payload) {
		// Heavy processing happens here, after response sent
		await this.enrichData(payload);
		await this.notifyDownstream(payload);
		await this.updateAnalytics(payload);
	}
}
class WebhookAgent extends Agent {
	async onRequest(request: Request): Promise<Response> {
		const payload = await request.json();

		// Quick validation
		if (!this.isValid(payload)) {
			return new Response("Invalid", { status: 400 });
		}

		// Queue heavy processing
		await this.queue("processWebhook", payload);

		// Respond immediately
		return new Response("Accepted", { status: 202 });
	}

	async processWebhook(payload: WebhookPayload) {
		// Heavy processing happens here, after response sent
		await this.enrichData(payload);
		await this.notifyDownstream(payload);
		await this.updateAnalytics(payload);
	}
}

非同期作業が Think のチャットターン 1 回なら、submitMessages() を使います。耐久的な受付 ID をすぐ返し、再試行ではメッセージターンを重複させず、べき等キーを使えます。

const submission = await this.submitMessages(messages, {
	idempotencyKey: payload.id,
});

return Response.json(
	{ submissionId: submission.submissionId },
	{ status: 202 },
);
const submission = await this.submitMessages(messages, {
	idempotencyKey: payload.id,
});

return Response.json(
	{ submissionId: submission.submissionId },
	{ status: 202 },
);

webhook がターン周辺のアプリケーション副作用(プロバイダースレッドの復元や、ユーザーに見える返信の投稿など)を持つ場合は、そのジョブを startFiber() で囲みます。Managed fiber は状態を保持し、プロバイダーの再試行を重複排除し、onFiberRecovered() または resolveFiber() でアプリ側の復旧結果を記録できます。

複数プロバイダーの振り分け

プロバイダー固有の検証と解析は、型付きヘルパー 1 つにまとめます。このヘルパーは、主な webhook プロバイダー にリンクしたドキュメントに従い生リクエストを検証し、検証済みボディからのみ agentName を導出する必要があります。

export default {
	async fetch(request, env) {
		const url = new URL(request.url);
		if (request.method === "POST" && url.pathname.startsWith("/webhooks/")) {
			const verified = await verifyAndParseWebhook(request.clone(), env);
			if (!verified) {
				return new Response("Invalid signature", { status: 401 });
			}

			switch (verified.provider) {
				case "github":
					return (
						await getAgentByName(env.GitHubAgent, verified.agentName)
					).fetch(request);
				case "stripe":
					return (
						await getAgentByName(env.StripeAgent, verified.agentName)
					).fetch(request);
				case "slack":
					return (
						await getAgentByName(env.SlackAgent, verified.agentName)
					).fetch(request);
			}
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
};
type VerifiedWebhook =
	| { provider: "github"; agentName: string }
	| { provider: "stripe"; agentName: string }
	| { provider: "slack"; agentName: string };

declare function verifyAndParseWebhook(
	request: Request,
	env: Env,
): Promise<VerifiedWebhook | null>;

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		if (request.method === "POST" && url.pathname.startsWith("/webhooks/")) {
			const verified = await verifyAndParseWebhook(request.clone(), env);
			if (!verified) {
				return new Response("Invalid signature", { status: 401 });
			}

			switch (verified.provider) {
				case "github":
					return (
						await getAgentByName(env.GitHubAgent, verified.agentName)
					).fetch(request);
				case "stripe":
					return (
						await getAgentByName(env.StripeAgent, verified.agentName)
					).fetch(request);
				case "slack":
					return (
						await getAgentByName(env.SlackAgent, verified.agentName)
					).fetch(request);
			}
		}

		return (
			(await routeAgentRequest(request, env)) ??
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

送信 webhook

エージェントから、外部サービスへ webhook を送ることもできます。

export class NotificationAgent extends Agent {
	async notifySlack(message) {
		const response = await fetch(this.env.SLACK_WEBHOOK_URL, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ text: message }),
		});

		if (!response.ok) {
			throw new Error(`Slack notification failed: ${response.status}`);
		}
	}

	async sendSignedWebhook(url, payload) {
		const body = JSON.stringify(payload);
		const signature = await this.sign(body, this.env.WEBHOOK_SECRET);

		await fetch(url, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"X-Signature": signature,
			},
			body,
		});
	}
}
export class NotificationAgent extends Agent {
	async notifySlack(message: string) {
		const response = await fetch(this.env.SLACK_WEBHOOK_URL, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify({ text: message }),
		});

		if (!response.ok) {
			throw new Error(`Slack notification failed: ${response.status}`);
		}
	}

	async sendSignedWebhook(url: string, payload: unknown) {
		const body = JSON.stringify(payload);
		const signature = await this.sign(body, this.env.WEBHOOK_SECRET);

		await fetch(url, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				"X-Signature": signature,
			},
			body,
		});
	}
}

セキュリティのベストプラクティス

  1. 署名は必ず検証します — 未検証の webhook を信頼してはいけません。
  2. 環境シークレットを使います — シークレットはコードに書かず、wrangler secret put で保存します。
  3. 素早く応答します — 再試行を避けるため、数秒以内に 200 / 202 を返します。
  4. ペイロードを検証します — 処理前に必須フィールドを確認します。
  5. 拒否を記録します — 無効な署名を追跡し、セキュリティ監視に使います。
  6. HTTPS を使います — webhook URL には常に TLS を使います。
// Store secrets securely
// wrangler secret put GITHUB_WEBHOOK_SECRET

// Access in agent
const secret = this.env.GITHUB_WEBHOOK_SECRET;
// Store secrets securely
// wrangler secret put GITHUB_WEBHOOK_SECRET

// Access in agent
const secret = this.env.GITHUB_WEBHOOK_SECRET;

主な webhook プロバイダー

次のステップ

Agents API

Agents SDK の API リファレンスです。

役に立ちましたか?