---
description: Receive and route webhook events from external services to dedicated Cloudflare Agent instances.
title: Webhooks
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt  
> Use this file to discover all available pages before exploring further.

# Webhooks

Last updated Aug 24, 2026|Copy as Markdown|[View as Markdown](https://e8aee267.previews.developers.cloudflare.com/agents/communication-channels/webhooks/index.md)|[Agent setup](https://e8aee267.previews.developers.cloudflare.com/agent-setup/)

Receive webhook events from external services and route them to dedicated agent instances. Each webhook source (repository, customer, device) can have its own agent with isolated state, persistent storage, and real-time client connections.

## Quick start

```js
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 })
		);
	},
};
```

```ts
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>;
```

## Use cases

Webhooks combined with agents enable patterns where each external entity gets its own isolated, stateful agent instance.

### Developer tools

| Use case                 | Description                                                                |
| ------------------------ | -------------------------------------------------------------------------- |
| **GitHub Repo Monitor**  | One agent per repository tracking commits, PRs, issues, and stars          |
| **CI/CD Pipeline Agent** | React to build/deploy events, notify on failures, track deployment history |
| **Linear/Jira Tracker**  | Auto-triage issues, assign based on content, track resolution times        |

### E-commerce and payments

| Use case                   | Description                                                           |
| -------------------------- | --------------------------------------------------------------------- |
| **Stripe Customer Agent**  | One agent per customer tracking payments, subscriptions, and disputes |
| **Shopify Order Agent**    | Order lifecycle from creation to fulfillment with inventory sync      |
| **Payment Reconciliation** | Match webhook events to internal records, flag discrepancies          |

### Communication and notifications

| Use case             | Description                                                             |
| -------------------- | ----------------------------------------------------------------------- |
| **Twilio SMS/Voice** | Conversational agents triggered by inbound messages or calls            |
| **Slack Bot**        | Respond to slash commands, button clicks, and interactive messages      |
| **Email Tracking**   | SendGrid/Mailgun delivery events, bounce handling, engagement analytics |

### IoT and infrastructure

| Use case              | Description                                                  |
| --------------------- | ------------------------------------------------------------ |
| **Device Telemetry**  | One agent per device processing sensor data streams          |
| **Alert Aggregation** | Collect alerts from PagerDuty, Datadog, or custom monitoring |
| **Home Automation**   | React to IFTTT/Zapier triggers with persistent state         |

### SaaS integrations

| Use case             | Description                                                     |
| -------------------- | --------------------------------------------------------------- |
| **CRM Sync**         | Salesforce/HubSpot contact and deal updates                     |
| **Calendar Agent**   | Google Calendar event notifications and scheduling              |
| **Form Submissions** | Typeform, Tally, or custom form webhooks with follow-up actions |

## Routing webhooks to agents

The key pattern is verifying the raw request before parsing it, then deriving the Agent identity from authenticated payload data. A body signature does not authenticate an unrelated URL segment or arbitrary header.

### Extract entity from payload

Most webhooks include an identifier in the payload:

```js
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);
		}
	},
};
```

```ts
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>;
```

### Validate entity IDs in URLs

A provider's body signature does not authenticate the webhook URL. If the URL includes an entity ID, compare it with the corresponding identity from the verified provider payload and reject a mismatch before calling `getAgentByName()`.

### Derive Slack identity from the verified body

Slack does not send an authenticated `X-Slack-Team-Id` routing header. Validate Slack's timestamped signature and replay window against the raw body, then read `team_id` from the verified event or form body.

## Signature verification

Always verify webhook signatures before trusting or processing the payload.

### GitHub HMAC-SHA256 pattern

The quick start's `verifyGitHubWebhook()` helper verifies GitHub's `sha256=<hex>` signature over the raw body with `crypto.subtle.verify()`. This format is GitHub-specific. Other providers use different signature encodings, signed inputs, timestamp checks, and replay protections. Follow the provider documentation linked under [Common webhook providers](#common-webhook-providers).

### Provider-specific headers

| Provider | Signature Header      | Algorithm                    |
| -------- | --------------------- | ---------------------------- |
| GitHub   | X-Hub-Signature-256   | HMAC-SHA256                  |
| Stripe   | Stripe-Signature      | HMAC-SHA256 (with timestamp) |
| Twilio   | X-Twilio-Signature    | HMAC-SHA1                    |
| Slack    | X-Slack-Signature     | HMAC-SHA256 (with timestamp) |
| Shopify  | X-Shopify-Hmac-Sha256 | HMAC-SHA256 (base64)         |

## Processing webhooks

### The onRequest handler

Use `onRequest()` to handle incoming webhooks in your agent. If the Worker has not already verified the request, verify it before parsing the body. This example reuses the quick start's `verifyGitHubWebhook()` helper:

```js
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()})`;
	}
}
```

```ts
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()})`;
	}
}
```

## Storing webhook events

Use SQLite to persist webhook events for history and replay.

### Event table schema

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

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

### Cleanup old events

Prevent unbounded growth by keeping only recent events:

```js
// 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')
`;
```

```ts
// 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')
`;
```

### Query events

```js
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}
    `,
		];
	}
}
```

```ts
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}
    `,
		];
	}
}
```

## Real-time broadcasting

When a webhook arrives, update agent state to automatically broadcast to connected WebSocket clients.

```js
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(),
			},
		});
	}
}
```

```ts
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(),
			},
		});
	}
}
```

On the client side:

```tsx
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>;
}
```

## Patterns

### Event deduplication

Prevent processing duplicate events using event IDs:

```js
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}, ...)`;
	}
}
```

```ts
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}, ...)`;
	}
}
```

### Respond quickly, process asynchronously

Webhook providers expect fast responses. Use the queue for heavy processing:

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

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

If the asynchronous work is a single Think chat turn, use `submitMessages()` instead. It returns a durable submission ID immediately and lets retries use an idempotency key instead of duplicating the message turn:

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

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

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

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

If the webhook owns application side effects around a turn, such as restoring a provider thread and posting a visible reply, use [startFiber()](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/execution/durable-execution/#startfiber) around that job. Managed fibers retain status, dedupe provider retries, and let `onFiberRecovered()` or `resolveFiber()` record the app-level recovery outcome.

### Multi-provider routing

Use one typed helper for provider-specific verification and parsing. It must validate the raw request according to the provider documentation linked under [Common webhook providers](#common-webhook-providers), then derive `agentName` only from the verified body.

```js
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 })
		);
	},
};
```

```ts
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>;
```

## Sending outgoing webhooks

Agents can also send webhooks to external services:

```js
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,
		});
	}
}
```

```ts
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,
		});
	}
}
```

## Security best practices

1. **Always verify signatures** \- Never trust unverified webhooks.
2. **Use environment secrets** \- Store secrets with `wrangler secret put`, not in code.
3. **Respond quickly** \- Return 200/202 within seconds to avoid retries.
4. **Validate payloads** \- Check required fields before processing.
5. **Log rejections** \- Track invalid signatures for security monitoring.
6. **Use HTTPS** \- Webhook URLs should always use TLS.

```js
// Store secrets securely
// wrangler secret put GITHUB_WEBHOOK_SECRET

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

```ts
// Store secrets securely
// wrangler secret put GITHUB_WEBHOOK_SECRET

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

## Common webhook providers

| Provider | Documentation                                                                                                  |
| -------- | -------------------------------------------------------------------------------------------------------------- |
| GitHub   | [Webhook events and payloads ↗](https://docs.github.com/en/webhooks)                                           |
| Stripe   | [Webhook signatures ↗](https://stripe.com/docs/webhooks/signatures)                                            |
| Twilio   | [Validate webhook requests ↗](https://www.twilio.com/docs/usage/webhooks/webhooks-security)                    |
| Slack    | [Verifying requests ↗](https://api.slack.com/authentication/verifying-requests-from-slack)                     |
| Shopify  | [Webhook verification ↗](https://shopify.dev/docs/apps/webhooks/configuration/https#step-5-verify-the-webhook) |
| SendGrid | [Event webhook ↗](https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook)      |
| Linear   | [Webhooks ↗](https://developers.linear.app/docs/graphql/webhooks)                                              |

## Next steps

### [Queue tasks](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/execution/queue-tasks/)

Background task processing.

### [Email routing](https://e8aee267.previews.developers.cloudflare.com/agents/communication-channels/email/)

Handle inbound emails in your agent.

### [Agents API](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/agents-api/)

Complete API reference for the Agents SDK.

Was this helpful?

YesNo

## On this page

[![](https://e8aee267.previews.developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://e8aee267.previews.developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/communication-channels/webhooks/#page","headline":"Webhooks · Cloudflare Agents docs","description":"Receive and route webhook events from external services to dedicated Cloudflare Agent instances.","url":"https://developers.cloudflare.com/agents/communication-channels/webhooks/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-24","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
```
