REST API
Ingredient API
Base URL: http://127.0.0.1:5001/api. JSON in and JSON out. Designed for recipe pickers on the Endless Flavour website. Machine index: GET /api.
Authentication
Reads are public. Writes (POST, PATCH, PUT, DELETE) will require user auth later. Until then they check an optional API key so you can lock the catalog down without changing URLs.
- If
API_WRITE_KEYis unset, writes succeed (local development). - If it is set, send
Authorization: Bearer <key>orX-API-Key: <key>. - A missing or wrong key returns
401with{"error":"unauthorized"}.
Search
Use q on the list endpoint, or GET /api/search which requires q. Matching covers name, scientific name, description, aliases, and a trigram similarity fallback. Results are ranked by similarity unless you pass sort.
GET /api/ingredients?q=tilapia GET /api/search?q=crème+fraîche GET /api/search/suggest?q=gou
Suggest needs at least two characters and returns up to ten {id, slug, name, category, energy_kcal} hits for typeahead.
How to filter
All filters are query parameters on GET /api/ingredients (and /api/search). Combine freely. Tags are AND; cuisines are OR. Nutrition values are per 100 g. form=fresh also matches stored raw.
| Parameter | Example | Meaning |
|---|---|---|
| q | tilapia | Search text |
| category | fish | Category slug, including children |
| cuisine | dutch | Single cuisine |
| cuisines | french&cuisines=italian | Any of these cuisines |
| tags / tag | vegan&tags=high-protein | Must have every tag |
| exclude_allergen | milk | Drop rows that contain the allergen |
| form / forms | fresh | Culinary form |
| min_protein, max_kcal, … | min_protein=20 | Macro bounds per 100 g |
| ids / slugs | 12,15 | Exact id or slug list |
| estimated | true | Estimated vs USDA-measured macros |
| sort, order | sort=protein&order=desc | name, protein, kcal, fat, fiber, updated, id |
| page, per_page | page=2&per_page=50 | Pagination (max 100 per page) |
GET /api/ingredients?category=fish&form=fresh&cuisine=ugandan GET /api/ingredients?cuisines=dutch&cuisines=belgian&sort=name GET /api/ingredients?tags=vegan&exclude_allergen=gluten&max_kcal=150 GET /api/ingredients?q=pepper&min_protein=15&order=desc&sort=protein
The list response is {page, per_page, pages, total, data[]}. Each card includes category, cuisines, tags, allergens, aliases, image URLs, and macros.
Ingredient CRUD
Look up by numeric id or slug. Names are unique ignoring case and surrounding space.
| GET /api/ingredients | List + filter |
| POST /api/ingredients | Create (201) |
| GET /api/ingredients/<id-or-slug> | Detail (micronutrients + servings) |
| PATCH /api/ingredients/<id-or-slug> | Partial update |
| PUT /api/ingredients/<id-or-slug> | Replace provided fields |
| DELETE /api/ingredients/<id-or-slug> | Delete |
POST /api/ingredients
Content-Type: application/json
{
"name": "Witlof",
"category": "leafy-greens",
"typical_form": "fresh",
"description": "Belgian endive for stews and salads.",
"cuisines": [{"slug": "belgian", "primary": true}, {"slug": "dutch"}],
"tags": ["vegan", "fresh"],
"allergens": [],
"aliases": ["witloof", "Belgian endive"],
"energy_kcal": 17,
"protein_g": 0.9,
"fat_g": 0.1,
"carbs_g": 4.0,
"sugars_g": 0.7,
"fiber_g": 3.1,
"sodium_mg": 2,
"nutrition_estimated": true
}
category is a slug. cuisines, tags, allergens, and aliases replace the whole list when sent. PATCH only changes keys you include.
Reference data
Use these to populate filter chips. POST creates a row (same write-key rules). GET on a slug returns that row plus a page of ingredients.
GET /api/categories
GET /api/categories/fish
POST /api/categories {"name":"Wild herbs","parent":"produce"}
GET /api/cuisines
GET /api/cuisines/dutch
POST /api/cuisines {"name":"Dutch","region":"Europe"}
GET /api/tags
POST /api/tags {"name":"High iron","type":"nutrition"}
GET /api/allergens
POST /api/allergens {"name":"Lupin"}
GET /api/forms
GET /api/stats
GET /api/health
Errors and later auth
Failed validation is 400 with {"error":"bad_request","message":"…"}. Missing rows are 404. Duplicate names are rejected. When login lands, keep these paths: attach a session or JWT in Authorization and keep require_api_write as the single gate on mutating routes.