Webhooks·续

this.sqlCREATE 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')
`;
// 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

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

Real-time broadcasting

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

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

On the client side:

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 &lt;div>Last event: {state?.lastEvent?.type}&lt;/div>;
}

Patterns

Event deduplication

Prevent processing duplicate events using event IDs:

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

Respond quickly, process asynchronously

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

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&lt;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:

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

If the webhook owns application side effects around a turn, such as restoring a provider thread and posting a visible reply, use 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, then derive agentName only from the verified body.

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&lt;VerifiedWebhook | null>;

export default {
    async fetch(request: Request, env: Env): Promise&lt;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&lt;Env>;

Sending outgoing webhooks

Agents can also send webhooks to external services:

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

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.
// 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;

Common webhook providers

Provider Documentation
GitHub Webhook events and payloads ↗︎
Stripe Webhook signatures ↗︎
Twilio Validate webhook requests ↗︎
Slack Verifying requests ↗︎
Shopify Webhook verification ↗︎
SendGrid Event webhook ↗︎
Linear Webhooks ↗︎

Next steps

Queue tasks

Background task processing.

Email routing

Handle inbound emails in your agent.

Agents API

Complete API reference for the Agents SDK.

Was this helpful?

YesNo

On this page

Webhooks·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/communication-channels/webhooks/#page","headline":"Webhooks","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/agents/communication-channels/webhooks/og.png?v=3407b6ff09d39a33","dateModified":"2026-08-17","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Send browser push notifications from a Cloudflare Agent, even when the user has closed the tab. title: Push notifications image: https://developers.cloudflare.com/agents/communication-channels/webhooks/push-notifications/og.png?v=550b1495887435fc


Skip to 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.