Forward webhooks to Slack
Turn events from any tool that can send a webhook (monitoring, payments, forms, CI) into readable Slack messages, protected by a shared secret.
import { timingSafeEqual } from "node:crypto";
type IncomingEvent = {
title: string;
message?: string;
severity?: "info" | "warning" | "critical";
url?: string;
source?: string;
};
const ICON = { info: ":information_source:", warning: ":warning:", critical: ":rotating_light:" };
// Slack mrkdwn treats &, < and > as control characters
const escape = (text: string) =>
text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
function hasValidToken(req: Request): boolean {
const expected = Buffer.from(process.env.NOTIFY_TOKEN ?? "");
const received = Buffer.from(req.headers.get("x-notify-token") ?? "");
return expected.length > 0 && received.length === expected.length && timingSafeEqual(received, expected);
}
export async function POST(req: Request) {
if (!hasValidToken(req)) return new Response("Unauthorized", { status: 401 });
const event = (await req.json().catch(() => null)) as IncomingEvent | null;
if (!event?.title) return new Response("Missing title", { status: 400 });
const severity = event.severity ?? "info";
const heading = `${ICON[severity]} *${escape(event.title)}*`;
const link = event.url ? `\n<${event.url}|Open details>` : "";
const res = await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
signal: AbortSignal.timeout(5_000),
body: JSON.stringify({
text: `${severity.toUpperCase()}: ${event.title}`, // shown in push notifications
blocks: [
{
type: "section",
text: { type: "mrkdwn", text: `${heading}\n${escape(event.message ?? "")}${link}` },
},
{
type: "context",
elements: [{ type: "mrkdwn", text: `Source: ${escape(event.source ?? "unknown")}` }],
},
],
}),
});
if (!res.ok) return new Response(`Slack error: ${await res.text()}`, { status: 502 });
return Response.json({ ok: true });
}Usage
curl -X POST https://your-app.com/api/notify \
-H "Content-Type: application/json" \
-H "x-notify-token: $NOTIFY_TOKEN" \
-d '{"title":"Payment failed","message":"Card declined for order 1042","severity":"warning","source":"billing"}'How it works & gotchas
- 01
SLACK_WEBHOOK_URLcomes from a Slack app with Incoming Webhooks enabled. Each URL posts to one channel, so use one per channel. - 02The top-level
textis the fallback shown in notifications and screen readers. Slack still recommends it when you sendblocks. - 03User-supplied text is escaped. Without it, a payload containing
<!channel>would ping the whole channel. - 04If the source tool signs its webhooks, verify that signature instead of a shared token. A static token in a header can leak through logs.
- 05For high-volume sources, queue events and batch them. Slack rate-limits incoming webhooks to roughly one message per second per channel.
Need this wired into a real system?
We build and harden production software end to end.
More code helpers
Sort incoming documents into folders with an LLM
A small worker that watches an inbox folder, classifies each document with an LLM and moves it to the right place. Low-confidence results go to a review folder instead of being guessed.
Stream LLM responses from a Next.js route handler
Send tokens to the browser as the model produces them instead of making users wait for the full completion. Uses a plain ReadableStream, so it works with any client that can read a fetch body.
Retry LLM calls with exponential backoff and jitter
LLM APIs return 429s and 5xx errors under load. This wrapper retries only the failures worth retrying, honours Retry-After and spreads retries out so many workers don't hit the API in lockstep.