I Had Cursor Build a Telegram-to-Slack Message Relay by Pointing It at the Webhook Docs — UnifyPort
If you want Telegram messages in Slack, the standard path is: register a bot with @BotFather, set up a webhook or long-poll with getUpdates, parse Telegram’s nested Update object, format for Slack’s incoming webhook, and handle rate limits. It works, but the code is Telegram-only. The Update object has its own shape, the bot can only see its own conversations, and if you later want WhatsApp messages in the same Slack channel, you start from scratch with a completely different API.
I took a different route. I opened Cursor, fed it the UnifyPort webhook reference, and asked it to build a Telegram-to-Slack relay. Thirty minutes later I had an Express server that verifies every delivery with HMAC-SHA256, formats inbound Telegram messages with Slack Block Kit, and posts them to a channel — about 45 lines of code. The same relay handles WhatsApp, LINE, TikTok, Zalo, and X with zero changes, because the inbound event shape is identical for all six platforms.
The demo you’ll end up with
A Node.js Express server that:
- Accepts
message.receivedevents from UnifyPort’s unified webhook - Verifies every delivery with HMAC-SHA256 against your
signing_secret - Formats each message into a Slack Block Kit payload with provider tag, sender, and timestamp
- Posts it to a Slack channel via incoming webhook
Time: about 30 minutes. You’ll need a UnifyPort workspace with a Telegram account connected (QR code scan — no bot registration) and a Slack incoming webhook URL.
Why not the Bot API directly?
A quick sketch of what the Telegram Bot API path involves:
- Create a bot via @BotFather — you get a bot token, but the bot is a separate entity from your personal account
- Choose a delivery method:
getUpdatespolling or webhook — both require Telegram-specific setup - Parse the
Updateobject: message text lives atupdate.message.text, the sender atupdate.message.from.id, the chat atupdate.message.chat.id— a shape unique to Telegram - Handle rate limits: 30 messages/second globally, 20 messages/minute per group
- Format and forward to Slack
The code runs fine — until you need WhatsApp messages in the same channel. Then you’re integrating the WhatsApp Cloud API separately, parsing a different webhook payload, and maintaining two codepaths that both produce a Slack message. Three platforms means three codepaths.
The UnifyPort webhook collapses this. Every platform delivers the same message.received event — provider, from, text, message_id. One handler serves all six.
Setup: point Cursor at the docs
Before prompting, get the API reference into Cursor’s context. Use @Docs to add the UnifyPort webhook reference — specifically:
- The
message.receivedevent payload shape - The
x-unifyport-signatureheader and HMAC-SHA256 verification section - The
POST /v1/webhook-endpointscreation call
Or paste those sections directly into the chat. The critical part: real field names go in, correct code comes out. Without the reference, Cursor will invent a telegram.onMessage() callback or a bot.on('text') handler that doesn’t exist in this API.
Claude Code, Windsurf, and Copilot work the same way — file context, docs panel, or paste into chat. The tool is secondary; the docs are primary.
The build
First prompt — the relay skeleton:
Using the UnifyPort webhook docs, write an Express server with a POST /webhook route. When the event is “message.received”, post a formatted message to a Slack incoming webhook URL from SLACK_WEBHOOK_URL. Include the provider name, sender, and message text. Return 200 for all events.
Cursor reads the reference and produces:
import express from "express";
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const app = express();
app.use(express.json());
app.post("/webhook", async (req, res) => {
const evt = req.body;
if (evt.event === "message.received") {
await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `*[${evt.provider}]* ${evt.from}: ${evt.text}`,
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
Fifteen lines of handler. Because the event shape is the same across all six platforms, there’s no Telegram-specific parsing — evt.provider says "telegram", but the structure is identical to a WhatsApp or LINE message.
Second prompt — signature verification:
Each delivery includes an x-unifyport-signature header. Verify it with HMAC-SHA256 using a signing_secret from the environment. The HMAC must run over the raw request body — not re-serialized JSON. Return 401 if it fails. Use a timing-safe comparison.
Cursor adds the verification layer:
import crypto from "crypto";
const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
function isValid(req) {
const sig = req.get("x-unifyport-signature") || "";
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
The verify callback captures the raw buffer before Express parses it. If Cursor had used req.body and re-serialized to JSON, the HMAC would fail on whitespace differences. The prompt said “raw request body,” so it handled this correctly without a correction round.
Third prompt — richer Slack formatting:
Update the Slack message to use Block Kit. Show the provider as a tag and the sender ID in bold. Add a context block with the timestamp and message_id.
The complete server.js:
import express from "express";
import crypto from "crypto";
const SIGNING_SECRET = process.env.UNIFYPORT_SIGNING_SECRET;
const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL;
const app = express();
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
function isValid(req) {
const sig = req.get("x-unifyport-signature") || "";
const expected = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(req.rawBody)
.digest("hex");
return sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
app.post("/webhook", async (req, res) => {
if (!isValid(req)) return res.sendStatus(401);
const evt = req.body;
if (evt.event === "message.received" && SLACK_WEBHOOK) {
await fetch(SLACK_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*[${evt.provider}]* from *${evt.from}*\n${evt.text}`,
},
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `${evt.message_id} · ${new Date(evt.timestamp * 1000).toISOString()}`,
},
],
},
],
}),
});
}
res.sendStatus(200);
});
app.listen(3000, () => console.log("listening on :3000"));
Under 50 lines. Signature-verified, formatted with Slack Block Kit, and forwarding every inbound message. The handler never branches on provider because it never needs to.
Run it and watch a Telegram message arrive
Connect a Telegram account in your UnifyPort workspace — Telegram supports QR code auth, so you scan from the Telegram app and you’re connected in seconds. No @BotFather, no bot token, no API credentials to manage. This connects your actual Telegram account, not a bot.
Register a webhook endpoint pointed at your server with subscribed_events: ["message.received"] and a signing_secret. Start the server, then send a test message to your Telegram account from another user. The event arrives:
{
"event": "message.received",
"account_id": "acct_5Qm8nR",
"provider": "telegram",
"from": "user_7c3d9e",
"text": "Are we still meeting tomorrow at 3?",
"timestamp": 1750521600,
"message_id": "tg_msg_2a6f4b"
}
Your server verifies the signature, formats the Block Kit payload, and posts to Slack. The message appears in your channel with the provider tag, sender, timestamp, and full text. If the signature check fails — 401 — verify that UNIFYPORT_SIGNING_SECRET matches the value set on the webhook endpoint.
The architecture payoff: add WhatsApp without touching the code
This is where the relay diverges from a Bot API build. Connect a WhatsApp account in the same workspace, subscribe the same webhook endpoint. A WhatsApp message arrives in the exact same shape:
{
"event": "message.received",
"account_id": "acct_3Xk1wL",
"provider": "whatsapp",
"from": "user_9b4e7a",
"text": "Invoice attached, please confirm receipt",
"timestamp": 1750521660,
"message_id": "wa_msg_8d2c5f"
}
Same handler. Same Slack channel. The [whatsapp] tag distinguishes it from [telegram] in Slack, but the code path is identical. A Telegram Bot API relay would need a second, separate integration — different webhook payload, different parsing, different error handling. This relay handles LINE, TikTok, Zalo, and X the same way, because the event shape doesn’t change.
That’s the difference between building against one platform’s API and building against a normalized layer. The Bot API path ties your relay to Telegram. The UnifyPort webhook reference gives Cursor — or any AI coding tool — a surface simple enough that it finishes cleanly on the first try, and the result works for every channel you connect. Point your tool at the UnifyPort API reference, paste in the message.received schema, and the relay will be running before lunch.