🇬🇧 Sainsbury's Example solution JavaScript /parse + /products + /session

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.js

Edit 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.

js
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.

js
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.

json — /parse response (abbreviated)
{
  "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.

js
// 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
json — /session response
{
  "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.

Links

Ready to build?

Start building shared Sainsbury's carts

Parse 5 recipe URLs in parallel and merge them into a single Sainsbury's checkout link — /parse + /products + /session.

27supermarkets 13countries 1schema Instant access