Planning 5 Sainsbury's dinners with one shared cart every Sunday
This walks through building a weekly meal planner that turns multiple recipe URLs into a single Sainsbury's shopping basket. The script uses Pepesto's /parse endpoint to extract ingredients from each recipe, /products to match them to Sainsbury's SKUs, and /session to create a shared checkout link.
Run this yourself
$ PEPESTO_API_KEY=your_key node sainsburys-weekly-meal-planner.jsEdit the recipe URLs array at the top of the file first. Full script: sainsburys-weekly-meal-planner.js. You'll need an API key to run it — get one here.
Getting started
The API key comes from a single /api/link POST. Set it as PEPESTO_API_KEY in your environment.
const res = await fetch('https://s.pepesto.com/api/link', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'priya@example.com' }),
});
const { api_key } = await res.json();
// Save this to your environment: export PEPESTO_API_KEY=pepesto_live_...The first call — parsing recipes in parallel
The flow has three steps: /parse each recipe to get a kg_token, then /products with all the tokens at once to get Sainsbury's SKUs, then /session to create a checkout link. Run all five /parse calls concurrently with Promise.all.
const API_KEY = process.env.PEPESTO_API_KEY;
const API_BASE = 'https://s.pepesto.com/api';
const recipeUrls = [
'https://www.bbcgoodfood.com/recipes/pizza-margherita-4-easy-steps',
'https://www.bbcgoodfood.com/recipes/spaghetti-bolognese-recipe',
'https://www.bbcgoodfood.com/recipes/chicken-tikka-masala',
'https://www.bbcgoodfood.com/recipes/easy-vegetable-curry',
'https://www.bbcgoodfood.com/recipes/one-pot-salmon-rice',
];
// Step 1: parse all recipes in parallel
async function parseRecipe(url) {
const res = await fetch(`${API_BASE}/parse`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
body: JSON.stringify({ recipe_url: url, locale: 'en-GB' }),
});
const data = await res.json();
return { title: data.recipe.title, kg_token: data.recipe.kg_token };
}
const parsed = await Promise.all(recipeUrls.map(parseRecipe));
// parsed = [{ title: 'Pizza Margherita in 4 easy steps', kg_token: '...' }, ...]Each /parse response includes the recipe title, ingredient list, steps, nutrition data, and the kg_token. The token is a compact binary encoding of the ingredient graph — all the ingredient quantities and categories that /products needs to do the matching. You don't need to inspect it; just pass it along.
{
"recipe": {
"title": "Pizza Margherita in 4 easy steps",
"ingredients": [
"300g flour", "1tsp yeast", "salt", "olive oil",
"200ml water", "tomato sauce", "basil",
"1 garlic clove", "130g mozzarella cheese",
"parmesan cheese", "100g cherry tomatoes"
],
"nutrition": {
"calories": 1773,
"carbohydrates_grams": 244,
"protein_grams": 70,
"fat_grams": 52
},
"kg_token": "EiIKIFBpenphIE1hcmdoZXJpdGEgaW4gNCBlYXN5IHN0ZXBz..."
}
}Then the /products call. I pass all five kg_token values at once. The API returns a flat list of ingredient lines, each with ranked product matches including name, price, and a session_token needed for checkout.
// Step 2: find Sainsbury's products for all recipes
const kgTokens = parsed.map(r => r.kg_token);
const productsRes = await fetch(`${API_BASE}/products`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
body: JSON.stringify({
recipe_kg_tokens: kgTokens,
supermarket_domain: 'sainsburys.co.uk',
}),
});
const { items } = await productsRes.json();
// Step 3: pick the top product for each ingredient and build the session
const skus = items
.filter(item => item.products?.length > 0)
.map(item => ({
session_token: item.products[0].session_token,
num_units_to_buy: item.products[0].num_units_to_buy || 1,
}));
const sessionRes = await fetch(`${API_BASE}/session`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
},
body: JSON.stringify({
supermarket_domain: 'sainsburys.co.uk',
skus,
}),
});
const session = await sessionRes.json();
console.log('Session ID:', session.session_id);
// Pass session_id to /checkout to get the pre-filled cart URL{
"session_id": "ses_8fK2mRqTpLnV4xYw"
}What the data showed
The most useful thing about seeing the items list printed out is the consolidation. Five recipes might share garlic, olive oil, salt, and onions — the API de-duplicates and consolidates quantities automatically. Garlic appearing across three recipes came back as one line item with num_units_to_buy: 2 rather than three separate entries. The basket you send to /session is already a clean, merged shopping list.
Matching quality on Italian recipes held up well. Galbani Italian Mozzarella Cheese 125g came back as the top match for the mozzarella line on the margherita — a sensible result. Tesco Mozzarella 200g came back as the second option. The ranking was consistent throughout.
Some things don't match at all — very specific items like "00 flour" or "pomegranate molasses" come back with no products. The script prints a warning for unmatched lines; those can be added manually in Sainsbury's before checkout.
Next steps
From the /session call you get a session_id. Pass it to /checkout to get a URL that opens the Sainsbury's cart directly. I send this to my partner via iMessage. They click it, review the cart on Sainsbury's, make any changes, and order. The session link stays live long enough for same-day ordering, which is all we need.
To filter by promoted items before building the session, inspect item.products[0].product.price.promotion.promo and prefer those — offer-priced versions of the same ingredient are surfaced automatically.
What else you could do?
Build a simple web UI where household members can submit recipe URLs during the week without running the script themselves. Choose lower-cost alternatives automatically: if the top match is Taste the Difference tier and there's a Sainsbury's own-brand match a rank below, prefer the own-brand. Calculate the estimated total price before opening the checkout and compare week-on-week to track meal planning costs over time.