Receive provider webhooks and run the job that polls and renews channels.
The webhook route
One route serves every channel: the last path segment is the channel id. Point each provider's webhook at its URL, such as https://acme.com/api/webhooks/support/email.
import { support } from "@/lib/support";
export async function POST(request: Request, { params }: { params: Promise<{ channel: string }> }) {
const { channel } = await params;
return support().handleWebhook(channel, request);
}
// Meta (WhatsApp Cloud API) verifies the URL with a GET first.
export const GET = POST;handleWebhook verifies the signature, ingests every event and answers the provider. Ingestion is idempotent: a redelivered webhook never creates a second message.
Responses
| Name | Type | Description |
|---|---|---|
200 | | Accepted, or the provider's expected body (an empty TwiML document, a verification challenge). |
401 / 400 | | The signature or payload didn't verify. Nothing was stored. |
404 | | No channel with that id, or it doesn't receive webhooks. |
500 | | Storing failed. The provider retries, which is safe. |
The scheduled job
Call runScheduled() every few minutes. It polls channels that read mailboxes (Gmail, Microsoft 365, IMAP), renews push subscriptions and watches, and marks replies stuck sending for more than ten minutes as failed so an agent can retry. Push-only channels make it a no-op, and it's safe to run concurrently.
import { support } from "@/lib/support";
export async function GET(request: Request) {
if (request.headers.get("authorization") !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response("Unauthorized", { status: 401 });
}
return Response.json(await support().runScheduled());
}It returns a summary such as { channels: { email: "3 events" }, staleFailed: 0 }.
Other runtimes
On Cloudflare Workers, run it from the scheduled handler next to your fetch handler:
export default {
fetch: app.fetch,
scheduled(_event, _env, ctx) {
ctx.waitUntil(support().runScheduled());
},
};Keep the webhook URL stable