Building a Morrisons grocery bot for our household Telegram group
Here's how to build a Telegram bot that lets household members add groceries or recipe URLs to a shared Morrisons basket by sending a message. The Pepesto /oneshot endpoint handles both plain-text item lists and recipe URLs — the bot's core Pepesto logic is about 30 lines of JavaScript.
Run this yourself
$ PEPESTO_API_KEY=your_key node morrisons-telegram-grocery-bot.jsIncludes a simulation mode — no live Telegram bot needed. Full script: morrisons-telegram-grocery-bot.js. You'll need an API key to run it — get one here.
Getting started
The Pepesto API key comes from a single POST call — instant, no waiting.
const res = await fetch('https://s.pepesto.com/api/link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'jamie@example.com' }),
});
const { api_key } = await res.json();
// Set: PEPESTO_API_KEY=pepesto_live_...Deploy the bot to a VPS or serverless function. The bot server listens for Telegram webhook events. The Pepesto call is the only external API involved — no separate product database, no scraping.
The first call — the core Pepesto handler
The key function is buildMorrisonsCart. If the message has a URL, that goes into content_urls. If it's plain text, that goes into content_text. Both fields can be sent together — so if someone sends "make this recipe + also add oat milk" with a recipe URL, both parts get processed in one call.
const API_KEY = process.env.PEPESTO_API_KEY;
const URL_REGEX = /https?:\/\/[^\s]+/i;
async function buildMorrisonsCart({ contentUrls = [], contentText = '' }) {
const body = { supermarket_domain: 'groceries.morrisons.com' };
if (contentUrls.length > 0) body.content_urls = contentUrls;
if (contentText) body.content_text = contentText;
const res = await fetch('https://s.pepesto.com/api/oneshot', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
body: JSON.stringify(body),
});
const data = await res.json();
return data.redirect_url;
}
async function handleTelegramMessage(message, reply) {
const { text, from } = message;
if (!text?.trim()) return;
const urlMatch = text.match(URL_REGEX);
try {
let cartUrl;
if (urlMatch) {
await reply(`Parsing that recipe for you, ${from}...`);
cartUrl = await buildMorrisonsCart({ contentUrls: [urlMatch[0]] });
await reply(`Here's your Morrisons cart:\n${cartUrl}`);
} else {
await reply(`Adding that to Morrisons, ${from}...`);
cartUrl = await buildMorrisonsCart({ contentText: text.trim() });
await reply(`Here's your Morrisons cart:\n${cartUrl}`);
}
} catch (err) {
await reply(`Something went wrong, ${from}. Try again in a moment.`);
}
}The /oneshot response has one field that matters:
{
"redirect_url": "https://app.pepesto.com/composed?force=1&req=GigKJmFsc28gYWRkIHNwYXJrbGluZyB3YXRlciBhbmQgb2xpdmUgb2ls..."
}When a household member opens that URL, the Pepesto service resolves it into a pre-filled Morrisons basket. The actual SKU matching and cart composition happens server-side on Pepesto's end — the bot itself never sees individual product data.
What the data showed
The most useful discovery was that content_text handles messy input much better than I expected. "oat milk, pasta, eggs, washing up liquid" works fine. So does "can you add some bananas and maybe some of that nice bread we had last week" — it figures out the grocery items from natural language, ignores the filler words, and builds a reasonable cart.
Recipe URL parsing worked on every link we threw at it — BBC Good Food, Olive Magazine, Nigel Slater's site, a random WordPress food blog. I also found out that copying the ingredients text and sending it as plain text works just as well.
Next steps
The redirect URL is the end of the bot's job. A household member clicks it, Morrisons opens with the pre-filled basket, and they can adjust quantities or remove items before ordering. Because we're on a Morrisons delivery subscription, the link goes to the regular site flow — the bot doesn't need to handle payment or account details at all.
The result
The bot handles both recipe URLs and plain-text grocery lists in a single handler. The Morrisons cart link needs to be opened within a reasonable time window — if a cart is generated at 11pm but not ordered until 48 hours later, some items might have availability changes. That's a Morrisons inventory reality, not a Pepesto issue.
What else you could do?
Detect when the bot generates a cart link and automatically pin it in the Telegram group so it's easy to find later. Accumulate items across the week — instead of each message producing its own cart link, maintain a weekly "running basket" that household members add to throughout the week, then consolidate on Sunday. Surface which items weren't matched so the person placing the order knows to add them manually. The /products endpoint provides that data; /oneshot doesn't expose it, so switch to the three-step flow for that feature.