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
Anonymous reads return a sample of 10 ingredients. The full catalog needs a signed-in User, Editor, or Admin session, or an API account token. API accounts cannot sign in to the website.
- Create an API account under Admin → Users → Add user, then send
Authorization: Bearer <token>orX-API-Key: <token>. - The optional deployment key
API_WRITE_KEYstill unlocks reads and writes when no API account is used. - JSON writes never use a browser cookie. Use an API token (or
API_WRITE_KEY). - A missing or wrong key on a write (or on a non-sample read) 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
Failed validation is 400 with {"error":"bad_request","message":"…"}. Missing rows are 404. Duplicate names are rejected. Website admin mutations require a verified admin session; JSON mutations still go through require_api_write.