ingestlayer/blog

all posts
Post#playbook

Get a Discord message when someone signs up.

Pasting a Discord webhook URL into your signup handler takes five minutes. Getting a Discord message that says who actually signed up is the real job.

ben4 min read


You ship something and you want to feel it work. The simplest version of that is a Discord message when someone signs up — a line in a channel you already have open, telling you a real person just showed up.

Discord makes it almost too easy. Any channel hands you an incoming webhook URL; you paste it into your signup handler, POST some JSON, and you're done in five minutes. It fires on the first signup and it's lovely. It just doesn't stay that way.

where the native webhook stops

It almost always starts like this:

signup.ts
// the version everybody pastes in first
app.post("/signup", async (req) => {
  await createUser(req.body);
  await fetch(process.env.DISCORD_WEBHOOK_URL, {
    method: "POST",
    body: JSON.stringify({ content: "new signup: " + req.body.email }),
  });
});
// fires on every signup — bots, typos, the same click twice — into one channel.
// and that URL is a bearer token sitting in your app, pinned to #signups forever.

Three things are quietly wrong, and none of them show up on day one. It fires on everything — the bot that found your form, the typo retry, the disposable address on its ninth free signup — so the channel pings on noise and you mute it by Friday. The message is whatever you stringified: new signup: j.doe@acme.io tells you an email and nothing else. And that URL is an unauthenticated bearer token sitting in your code, welded to one channel — want the same event shaped differently somewhere else and it's a code change and a redeploy.

The native webhook is a delivery mechanism with no room to think. Everything interesting about a signup happens in the gap it skips: the gap between the event arriving and the message going out.

the steps worth putting in between

Do the work in that gap, somewhere that isn't your signup handler, and a few cheap steps cover most of it. filter drops the signups you'd never act on, so the bots never make a sound. dedupe collapses the double-click into one ping. And enrich.person turns the bare email into a real name and the company behind the domain, so the message finally says who showed up instead of quoting their address.

classify is the one a webhook URL can never do. Hand the enriched signup to a model with a typed set of labels — hot_lead, normal, spam — and it writes the verdict back as a field you can route on. No keyword rules, no reading every signup by hand: a real decision made mid-pipe, before anyone gets pinged. summarize, throttle, and per-destination redaction sit in the same toolbox for when you need them.

one signup, two rooms

Put it together and here's the whole thing running — a signup goes in one end and a shaped Discord message lands at the other:

demo · signup → discord

Your #signups channel gets every real signup, enriched, with the model's read attached. #hot-leads only lights up when classify says it's worth it — the same event, told twice, each shaped for its room. Postgres keeps all of them. And the webhook URL is gone from your code: which channel hears what now lives in the file, not a fetch call you have to go find.

how ingestlayer does this

Everything above is real. Discord is a first-class destination — authorize it once, then route to as many channels as you like, each with its own template and its own when. filter, dedupe, enrich, and classify are real actions you stack between a source and a destination, run once before the event fans out. You point the SDK or a webhook at the pipeline and the rest is just the file.

It's the same shape as the same pings in Slack without the noise — anything that arrives with more volume than attention. Want the public-room rule (no email, ever) enforced rather than just left out of a template? That's per-destination redaction. A signup that lands in Discord already knowing who it is makes a good place to start.


Read next

valve: your pipeline in the menu bar.

valve makes your menu bar a pipeline destination, so the pipeline decides which events are worth a desktop notification — not a setting in the app.

← back to all posts