---
title: "Use a recipe API from Python in ten minutes — Tiny Plates"
description: "Use a recipe API from Python with requests and pandas: search recipes, read ingredients and steps, filter allergens, paginate and analyse cook times."
url: "https://www.tinyplates.dev/blog/recipe-api-python"
---

# Use a recipe API from Python in ten minutes

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

19 October 2026

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](https://www.tinyplates.dev/sign-up). 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:

```bash
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:

```python
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:

```python
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:

```python
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:

```python
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:

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

```

The full list of fields is in the [recipe object docs](https://www.tinyplates.dev/docs/recipe-object).

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

```python
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](https://www.tinyplates.dev/blog/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":

```python
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:

```python
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](https://www.tinyplates.dev/docs/pagination).

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:

```python
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

```python
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](https://www.tinyplates.dev/compare/spoonacular) and a roundup of [Spoonacular alternatives](https://www.tinyplates.dev/blog/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](https://www.tinyplates.dev/blog/tiny-plates-mcp-server) lets it search recipes for you. [Grab a free key](https://www.tinyplates.dev/sign-up) and have a play.
