DEV Community

RAXXO Studios
RAXXO Studios

Posted on • Originally published at raxxo.shop

Build a Telegram Alert Bot for Shopify Orders in 30 Lines of TypeScript

  • A Bun serverless function on Vercel turns Shopify orders/create webhooks into Telegram pings in under 5s

  • HMAC verification with the webhook secret stops random POSTs from spoofing your phone

  • BotFather setup plus a one-time chat_id lookup is the only manual step before deploy

  • The full alert handler is 28 lines of TypeScript, retries on Telegram 429s, and costs zero EUR on Vercel Hobby

I missed two orders during a flash sale because the Shopify mobile app push lag was somewhere between 90 seconds and never. So I wrote my own. The whole thing fits on one screen, runs on Vercel, and pings my phone before the customer's confirmation email even renders.

What the alert actually looks like

When a new order lands, my phone buzzes with this:


NEW ORDER #1042
Total: 47 EUR
City: Berlin
Items:
- Statusline Builder License x1
- Claude Blueprint Bundle x1

Enter fullscreen mode Exit fullscreen mode

Five seconds, end to end. No app, no polling, no Zapier middleman charging 20 EUR a month for what is essentially one HTTPS request forwarding another HTTPS request. Telegram is the messenger because it has a real bot API, supports markdown, and works on every device I own. Slack would also work. Discord too. The pattern is identical, only the final POST URL changes.

The point of this build is that it is small enough to actually understand. You can read the whole function, hold it in your head, and modify it without spelunking through a vendor dashboard. That matters when something breaks at 2am during a launch.

If you want the broader pattern of using webhooks to drive automation across your store, Building a Webhook System for Shopify Order Automation covers the full architecture. This article is the minimum viable version of that, scoped to one job.

BotFather and chat_id, the only manual part

Open Telegram. Search for @BotFather. Send /newbot. Pick a name. Pick a username ending in bot. BotFather hands you a token that looks like 7234567890:AAH.... Save it. This is the credential your serverless function will use to send messages. Treat it like a password.

The bot can now send messages, but it does not know where to send them yet. Telegram routes messages by chat_id, a number that identifies a user, group, or channel. To find yours, send any message to your new bot inside Telegram, then visit:


https://api.telegram.org/bot/getUpdates

Enter fullscreen mode Exit fullscreen mode

You will see a JSON blob. Inside result[0].message.chat.id is your number. Mine is a 10-digit positive integer. Save that too.

If you want alerts in a group instead of a private chat, add the bot to the group, send a message, and the same endpoint will return a negative integer chat_id. Channels work the same way but require posting permission for the bot.

That is the entire Telegram side. Two values, both stored as Vercel environment variables, never committed to git.

The 28-line handler

This is the whole thing. Bun is not strictly required, plain Node works, but Bun starts faster on Vercel and the types are cleaner. Save as api/shopify-order.ts:


import crypto from "node:crypto";

const TG = process.env.TELEGRAM_TOKEN!;
const CHAT = process.env.TELEGRAM_CHAT_ID!;
const SECRET = process.env.SHOPIFY_WH_SIGNATURE!;

export const config = { runtime: "edge" };

export default async function handler(req: Request) {
  const raw = await req.text();
  const sig = req.headers.get("x-shopify-hmac-sha256") ?? "";
  const expected = crypto.createHmac("sha256", SECRET).update(raw).digest("base64");
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return new Response("bad sig", { status: 401 });
  }
  const o = JSON.parse(raw);
  const items = o.line_items.map((l: any) => `- ${l.title} x${l.quantity}`).join("\n");
  const city = o.shipping_address?.city ?? "unknown";
  const text = `NEW ORDER #${o.order_number}\nTotal: ${o.total_price} ${o.currency}\nCity: ${city}\nItems:\n${items}`;
  for (let i = 0; i < 3; i++) {
    const r = await fetch(`https://api.telegram.org/bot${TG}/sendMessage`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ chat_id: CHAT, text }),
    });
    if (r.ok) return new Response("ok");
    if (r.status === 429) await new Promise(s => setTimeout(s, (i + 1) * 1000));
  }
  return new Response("telegram failed", { status: 502 });
}

Enter fullscreen mode Exit fullscreen mode

A few things worth pointing out before you ship it.

The HMAC check uses timingSafeEqual instead of === because string comparison leaks timing information. Anyone who can hit your endpoint can technically brute-force a valid signature one byte at a time if you compare with a regular equals sign. It is paranoid for a small store, but it is one extra line and removes the question entirely.

The body is read once with req.text() because the HMAC is computed over the raw payload. If you parse JSON first and re-stringify, the signature will not match. Order matters here: verify, then parse.

The retry loop only catches Telegram 429s. Shopify will retry on its side if your function returns a 5xx, so I do not need to do anything special there. Telegram rate limits are usually 30 messages per second, way above what a small store will ever hit, but a flash sale can spike, and a single failed retry beats losing the alert entirely.

For Vercel Edge Config patterns that pair well with this kind of webhook handler, 5 Vercel Edge Config Patterns I Use For Shopify A/B Tests walks through Edge Config setup. Same deployment target, same instant cold starts.

Wiring the Shopify webhook

Two ways to register the webhook. The dashboard works fine for one alert: Settings, Notifications, Webhooks, scroll to the bottom, Create webhook. Pick Order creation, format JSON, paste your Vercel URL, save. Shopify shows the signing secret right after you save it. Copy it immediately. You cannot view it again later, only rotate it.

The other way is the Admin API, which I prefer because I can put it in version control:


curl -X POST "https://your-shop.myshopify.com/admin/api/2026-01/webhooks.json" \
  -H "X-Shopify-Access-Token: $ADMIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"webhook":{"topic":"orders/create","address":"https://your-app.vercel.app/api/shopify-order","format":"json"}}'

Enter fullscreen mode Exit fullscreen mode

Either way, the signing key lives at Settings, Notifications, Webhooks. Drop it into Vercel as SHOPIFY_WH_SIGNATURE. Add TELEGRAM_TOKEN and TELEGRAM_CHAT_ID while you are there. Three environment variables, all sensitive, all in Vercel never in code.


vercel env add SHOPIFY_WH_SIGNATURE production
vercel env add TELEGRAM_TOKEN production
vercel env add TELEGRAM_CHAT_ID production
vercel deploy --prod

Enter fullscreen mode Exit fullscreen mode

First deploy takes about 20 seconds. Subsequent deploys are faster because the build cache is warm. Vercel Hobby is fine for this. I have run alert bots like this on free tier for over a year without ever hitting the function invocation limit, because order webhooks are by definition rare. If you do 1000 orders a month, that is 1000 invocations. Hobby allows 100k.

Testing without placing real orders

Shopify has a nice quality of life feature in the webhook list: a Send test notification button. It fires the webhook with a sample payload, which is enough to verify your function works end to end. You should see your phone buzz within a couple seconds.

If nothing happens, check Vercel Logs first. The function either failed signature verification (wrong secret in env), threw a JSON parse error (malformed payload), or got a 4xx back from Telegram (wrong token or chat_id). The error message will tell you which one.

I also keep a tiny curl one-liner to test the Telegram side in isolation, decoupled from Shopify entirely:


curl "https://api.telegram.org/bot$TELEGRAM_TOKEN/sendMessage" \
  -d "chat_id=$CHAT_ID" -d "text=ping"

Enter fullscreen mode Exit fullscreen mode

If that pings your phone, Telegram is fine and the issue is in the webhook chain. If it does not ping, the token or chat_id is wrong. Two layers to check, easy to isolate.

One thing that bit me the first time: Telegram silently drops messages over 4096 characters. Not an issue for order alerts, but if you start adding line item descriptions or customer notes, watch the length. The fetch will return 200, the message will not arrive, you will assume the bot is broken.

Bottom Line

This is the cheapest, most boring useful thing I built last quarter. Five seconds from order placed to phone buzzing, three environment variables, twenty-eight lines of TypeScript. It cost zero EUR to run and it has not gone down once in three months.

The trick is keeping it small. Every time I have been tempted to add features, like an end of day order summary, a low-stock ping, or a Slack mirror, I have built a separate function instead. One job per handler. When something breaks, I know exactly where to look.

If you want more patterns like this for running a small Shopify store with serverless plumbing instead of subscription apps, the Lab has a growing collection of tutorials. Start at Lab Overview for the index. The whole point of the studio is showing how a one-person team can replace what used to need a team, with code small enough to fit in your head.

Try it on a staging store first. The Send test notification button is your friend. And keep the BotFather token out of git.

Top comments (0)