Skip to content

Use a recipe API from Python in ten minutes

Search recipes with requests, read the JSON, and have some fun with pandas.

You've got a notebook open and you want recipe data in it. Maybe for a class project, maybe for a side project, maybe just because you're curious how long Italian food takes to cook. Let's get there in about ten minutes with requests and pandas.

First, grab a free key from the sign-up page. No credit card. You get 50 requests a day and 300 a week, which is plenty for this. Put the key in an environment variable so it doesn't end up in your notebook or on GitHub:

export TINYPLATES_API_KEY=rd_your_api_key
pip install requests pandas

Now a tiny helper. Every request goes to https://api.tinyplates.dev with the key as a bearer token. When something goes wrong, the API sends back a JSON error with a message written for humans, so we just pass that along:

import os

import requests

API = "https://api.tinyplates.dev"

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TINYPLATES_API_KEY']}"


def get(path, **params):
    response = session.get(f"{API}{path}", params=params, timeout=30)

    if not response.ok:
        try:
            message = response.json()["error"]["message"]
        except (ValueError, KeyError):
            message = response.text
        raise RuntimeError(f"Tiny Plates said {response.status_code}: {message}")

    return response.json()

Let's search for something:

data = get("/search", query="creamy pasta", limit=5)

for result in data["results"]:
    print(result["id"], result["title"])

Search works on meaning as well as words, so "creamy pasta" finds carbonara too. You get up to 50 results, best first. If you only need a few fields, ask for them with fields. The responses get much smaller:

data = get("/search", query="creamy pasta", fields="title,times,cuisines", limit=5)

What's in a recipe

Pick an id from the search and fetch the whole thing:

recipe = get(f"/recipes/{data['results'][0]['id']}")["recipe"]

print(recipe["title"])
print(recipe["times"])            # {'prep': {'seconds': 600}, 'total': {'seconds': 1800}, ...}

for line in recipe["ingredients"]:
    print(line["display"])        # "2 cloves garlic, roughly chopped"

for step in recipe["instructions"]:
    print(step["step"], step["text"])

A few things worth knowing. Times are in seconds, under prep, cook, inactive and total, and any of them can be missing. Each ingredient has a ready-to-print display line, plus the pieces split out: name, quantity (with value and unit), preparation and optional. The line the source site wrote is kept in original. If you just want text, recipe["text"]["ingredients"] and recipe["text"]["instructions"] are flat lists of strings. And url points back to the page the recipe came from. Please link to it when you show a recipe.

You can also scale and convert on the way in. This asks for two servings in metric:

small = get(f"/recipes/{recipe['id']}", servings=2, units="metric")

The full list of fields is in the recipe object docs.

Now filters. Allergens are done on the server. Add without and recipes with those allergens never show up:

data = get("/search", query="weeknight curry", without="milk,peanuts")

The names come from /vocabularies. We wrote more about how that works in Allergen data for every recipe.

Diets are a bit different. Search doesn't take a diet filter, but every recipe carries dietary flags like vegetarian and glutenFree. So filter in Python. One catch: a missing flag means "we don't know", and False means a known no. Check for True, not just "not False":

veggie = [r for r in data["results"] if r.get("dietary", {}).get("vegetarian") is True]

You might spot that the /recipes list endpoint has filters like diet and cuisine. It's meant for a business plan that doesn't exist yet, so on a free key it answers with a 403. Stick to /search, /popular and /recipes/{id}.

Next, more than one page. Search gives you the best matches and stops, so it has no paging. /popular does. It takes limit (up to 100) and offset, and each response has a pagination object with the total. Here's the thing to watch: every page is one request, and you have 50 a day. So always put a cap on the loop:

def popular(pages=3, language="en"):
    recipes = []

    for page in range(pages):
        data = get("/popular", limit=100, offset=page * 100, language=language)
        recipes += data["recipes"]

        if len(recipes) >= data["pagination"]["total"]:
            break

    return recipes

Three pages is 300 recipes for 3 requests. language can be en, es or sv. If you go over the daily allowance you get a 429 that says so, and it resets at midnight UTC. More on that in the pagination docs.

Notebooks re-run cells a lot, and each re-run spends requests. Save what you fetched to a file and read it from there next time. The full script below does that.

Now the fun part. Turn the recipes into a DataFrame and ask it something:

import pandas as pd

rows = []
for r in recipes:
    total = r["times"].get("total", {}).get("seconds")
    rows.append({
        "title": r["title"],
        "cuisine": r["cuisines"],
        "minutes": total / 60 if total else None,
        "ingredients": len(r["ingredients"]),
        "vegetarian": r.get("dietary", {}).get("vegetarian"),
    })

df = pd.DataFrame(rows)

by_cuisine = (
    df.explode("cuisine")
    .dropna(subset=["cuisine", "minutes"])
    .groupby("cuisine")["minutes"]
    .agg(["count", "mean"])
    .query("count >= 5")
    .sort_values("mean")
)
print(by_cuisine.round(1))

A recipe can have more than one cuisine, so explode gives each one its own row. The count >= 5 keeps a single slow-roasted outlier from crowning a cuisine. Try the same with ingredients instead of minutes, or check what share of each cuisine is vegetarian with df["vegetarian"].eq(True). Keep in mind these are popular recipes, not every recipe, so it's a fun look and not a study.

The full script

import json
import os
from pathlib import Path

import pandas as pd
import requests

API = "https://api.tinyplates.dev"
CACHE = Path("popular.json")

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['TINYPLATES_API_KEY']}"


def get(path, **params):
    response = session.get(f"{API}{path}", params=params, timeout=30)

    if not response.ok:
        try:
            message = response.json()["error"]["message"]
        except (ValueError, KeyError):
            message = response.text
        raise RuntimeError(f"Tiny Plates said {response.status_code}: {message}")

    return response.json()


def popular(pages=3, language="en"):
    if CACHE.exists():
        return json.loads(CACHE.read_text())

    recipes = []
    for page in range(pages):
        data = get("/popular", limit=100, offset=page * 100, language=language)
        recipes += data["recipes"]

        if len(recipes) >= data["pagination"]["total"]:
            break

    CACHE.write_text(json.dumps(recipes))
    return recipes


rows = []
for r in popular():
    total = r["times"].get("total", {}).get("seconds")
    rows.append({
        "title": r["title"],
        "cuisine": r["cuisines"],
        "minutes": total / 60 if total else None,
        "ingredients": len(r["ingredients"]),
        "vegetarian": r.get("dietary", {}).get("vegetarian"),
    })

df = pd.DataFrame(rows)

by_cuisine = (
    df.explode("cuisine")
    .dropna(subset=["cuisine", "minutes"])
    .groupby("cuisine")["minutes"]
    .agg(["count", "mean"])
    .query("count >= 5")
    .sort_values("mean")
)

print(by_cuisine.round(1))
print(f"{df['vegetarian'].eq(True).mean():.0%} of these are vegetarian")

The first run costs three requests. Every run after that is free, until you delete popular.json.

If you landed here looking for Spoonacular in Python, that's a fine choice too. It covers much more ground, like grocery products and restaurant menu items. We wrote an honest comparison and a roundup of Spoonacular alternatives if you're weighing them up.

From here you could build a "what can I cook" script with /pantry, or merge a week of recipes into one list with /shopping-list. If you work with an AI assistant, the MCP server lets it search recipes for you. Grab a free key and have a play.