Recipe schema reference
The complete, field-by-field anatomy of the Recipe object — every top-level key, every nested index, with type, nullability, format, a rigorous explanation, and the live value served by GET /api/v1/dinner. The TypeScript interfaces in src/types/api.ts are the contract; this page is the commentary.
The worked example throughout is Texas Chili con Carne (a066f472-ed0c-46ea-8e2c-a0053c3183a8). Fetch it yourself, no key required:
curl https://recipe-api.com/api/v1/dinner
Looking for endpoint behaviour, auth, and rate limits? That's on the docs page. This page is purely about the shape of the data.
The Recipe object
Fifteen top-level keys. Seven scalar identifiers, then seven nested structures and arrays. Every recipe returned by the API conforms to this shape exactly.
Identity & classification
The seven scalar fields that identify and classify a recipe. Every recipe — from /dinner, /recipes/:id, or /generate — carries these. They are the keys you filter, sort, and cache on.
interface ApiRecipe {
id: string;
name: string;
description: string;
category: string;
cuisine: string;
difficulty: string;
tags: string[];
meta: ApiRecipeMeta;
dietary: ApiDietary;
storage: ApiStorage;
equipment: ApiEquipment[];
ingredients: ApiIngredientGroup[];
instructions: ApiInstruction[];
troubleshooting: ApiTroubleshooting[];
chef_notes: string[];
cultural_context: string | null;
nutrition: ApiNutrition;
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
idUUIDv4 | string (uuid) | "a066f472-ed0c-46ea-8e2c-a0053c3183a8" | Immutable, globally-unique identifier. Use this as your cache key and as the path parameter for GET /api/v1/recipes/:id. Re-fetching a recipe by the same id does not consume a new unique-recipe credit. |
name | string | "Texas Chili con Carne" | Human-readable display title. Not guaranteed unique across the catalog — key by id, not name. |
description | string | "A thick, beef-based stew featuring tender cubes of meat in a rich sauce made from reconstituted whole chilies and aromatic spices without beans or tomatoes." | One-paragraph summary of the dish. Useful for list previews and search result snippets. Free-form prose, not structured. |
categoryControlled vocabulary | string | "Dinner" | Coarse meal/course bucket, e.g. Dinner, Dessert, Breakfast. The full set (with counts) is available from GET /api/v1/categories and is filterable via ?category= on /recipes. |
cuisineControlled vocabulary | string | "American" | Cuisine taxonomy value, e.g. Italian, American, Mexican. Listed by GET /api/v1/cuisines; substring-filterable via ?cuisine= on /recipes. |
difficultyEnum: Easy | Intermediate | Advanced | string | "Intermediate" | Skill rating for the home cook. Exact-match filterable via ?difficulty= on /recipes. The generate endpoint also accepts a Professional value. |
tags | string[] | ["Beef", "Slow-Cooked", "High-Protein", "Southwestern"] | Free-form descriptive tags. These are flavourful descriptors, not the controlled category/cuisine taxonomy — do not assume a fixed set. Useful for badges and secondary faceting. |
meta — timing & yield
ApiRecipeMetaTiming and portioning. All durations are ISO 8601 — parse them (don't string-match), because PT2H and PT120M are equivalent. The numeric yield_count and serving_size_g exist so you can do portion math without parsing the human strings.
interface ApiRecipeMeta {
active_time: string; // ISO 8601 duration
passive_time: string; // ISO 8601 duration
total_time: string; // ISO 8601 duration
overnight_required: boolean;
yields: string; // human-readable
yield_count: number; // numeric, for calculations
serving_size_g: number | null; // grams per serving
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
meta.active_timeISO 8601 duration | string | "PT20M" | Hands-on time the cook must be actively working (20 minutes here). ISO 8601 duration — PT = time period, then H/M/S. |
meta.passive_timeISO 8601 duration | string | "PT1H40M" | Unattended time: simmering, proofing, resting, soaking. Distinct from active_time so apps can show '20 min hands-on / 2 hr total'. |
meta.total_timeISO 8601 duration | string | "PT2H" | Wall-clock start-to-finish (active + passive, plus any gaps). Use this for meal-planning scheduling. Here, 2 hours. |
meta.overnight_required | boolean | false | True when the recipe needs an overnight soak, marinade, or rest. Lets planning apps flag 'start this the day before'. |
meta.yields | string | "4 servings" | Human-readable output quantity. May be servings, loaves, cookies, etc. Display only — don't parse it; use yield_count for math. |
meta.yield_count | number | 4 | Numeric yield (here, 4 servings). Use this to scale ingredient quantities: desired_servings / yield_count is your scaling factor. |
meta.serving_size_gnullablegrams | number | 300 | Weight of one serving in grams. Non-null here (300g). Combined with nutrition.per_serving this lets you compute per-gram macros. null when the yield isn't divisible into servings (e.g. '1 loaf'). |
ISO 8601 durations: PT#H#M#S for clock times (PT20M), P#D / P#M / P#Y for calendar spans used in storage. Example: PT2H = 2 hours; P4D = 4 days.
dietary — flags & allergens
ApiDietaryTwo complementary lists: positive attributes a recipe satisfies (flags) and conditions it is not suitable for (allergens/warnings). Both are filterable on /recipes.
interface ApiDietary {
flags: string[]; // e.g. ["Vegetarian", "Gluten-Free"]
not_suitable_for: string[]; // e.g. ["Nut allergy"]
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
dietary.flagsControlled vocabulary | string[] | ["Gluten-Free", "Dairy-Free", "Egg-Free", "Nut-Free", "Peanut-Free", "Soy-Free"] | Dietary attributes the recipe meets (Vegetarian, Vegan, Gluten-Free, Dairy-Free, ...). The full set with counts is at GET /api/v1/dietary-flags. Filter with ?dietary=Gluten-Free,Dairy-Free — a recipe must match ALL supplied flags. |
dietary.not_suitable_forOpen list | string[] | [] | Allergen or condition warnings the recipe does NOT satisfy, even if no flag captures it (e.g. 'Nut allergy' for a recipe that isn't formally flagged). Empty here — the chili is clean for the listed allergens. Always surface these in your UI for safety. |
storage — keeping & reheating
ApiStorageHow long the cooked result keeps and how to bring it back. refrigerator and freezer are nullable objects — a null means that storage method is not recommended for this recipe.
interface ApiStorage {
refrigerator: {
duration: string; // ISO 8601 duration, e.g. "P3D"
notes: string;
} | null;
freezer: {
duration: string | null; // ISO 8601, null if not recommended
notes: string;
} | null;
reheating: string | null;
does_not_keep: boolean;
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
storage.refrigerator | object | null | { "duration": "P4D", "notes": "Flavor improves after 24 hours." } | Fridge storage. duration is an ISO 8601 calendar duration (P4D = 4 days); notes carry qualitative guidance. null when refrigeration doesn't apply. |
storage.refrigerator.durationISO 8601 duration | string | "P4D" | Maximum recommended fridge life. Use P/nD-style calendar durations (days), not clock times. |
storage.refrigerator.notes | string | "Flavor improves after 24 hours." | Container/quality tips for fridge storage. |
storage.freezer | object | null | { "duration": "P3M", "notes": "Thaw overnight in refrigerator before reheating." } | Freezer storage. The outer object is null if freezing isn't recommended; the inner duration may also be null. |
storage.reheatingnullable | string | "Heat in a saucepan over medium-low heat, adding a splash of water if too thick." | Method to reheat cooked leftovers. null when reheating isn't viable (e.g. delicate salads). |
storage.does_not_keep | boolean | false | True when the dish is best made fresh and degrades badly on storage (soufflés, fried foods). When true, show a 'make fresh' warning even if refrigerator is populated. |
equipment — tools & alternatives
ApiEquipmentAn array of tools the recipe calls for. Each entry names the tool, whether it's mandatory, and — crucially for substitution UX — a workable alternative.
interface ApiEquipment {
name: string;
required: boolean;
alternative: string | null;
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
equipment[].name | string | "Blender" | Tool name as a cook would recognise it. Index 0 of the dinner recipe is 'Blender'. |
equipment[].required | boolean | true | Whether the tool is essential. false items are nice-to-have; you can still cook the recipe without them. |
equipment[].alternativenullable | string | "Food processor or mortar and pestle" | A substitutable tool, or null when there's no real swap (e.g. the 'Mixing bowl' entry has alternative: null). Great for accessibility and 'what can I use instead?' features. |
Dinner recipe equipment: [ Blender → Food processor or mortar and pestle, Heavy skillet → Dutch oven or heavy-bottomed pot, Mixing bowl → null ].
ingredients — grouped, sourced, traceable
ApiIngredientGroup → ApiIngredientThe heart of the schema. Ingredients are grouped by component (dough, filling, sauce...). Every line item is linked to a stable ingredient UUID and a named nutrition source, so you can trace exactly where each macro came from. This traceability is what separates this API from plain recipe text.
interface ApiIngredientGroup {
group_name: string;
items: ApiIngredient[];
}
interface ApiIngredient {
name: string;
quantity: number | null;
unit: string | null;
preparation: string | null; // e.g. "diced", "chopped"
notes: string | null;
substitutions: string[];
ingredient_id: string | null; // UUID from ingredients table
nutrition_source: string | null; // e.g. "USDA FoodData Central"
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
ingredients[].group_name | string | "Chili Base" | Component label for a set of items. The dinner recipe has two groups: 'Chili Base' and 'Flavor Paste'. Render groups as sub-headers in your ingredient list. |
ingredients[].items[].name | string | "stewing beef" | Ingredient display name, as written for the recipe. The authoritative record is keyed by ingredient_id below. |
ingredients[].items[].quantitynullable | number | 910 | Numeric amount. null for 'to taste' ingredients (see salt/black pepper in the Flavor Paste group). Always a number when present — never a string — so you can scale it by desired_servings / yield_count. |
ingredients[].items[].unitnullableOpen list (g, ml, tsp, ...) | string | "g" | Unit of measure. null for countable items (6 chilies, 2 bay leaves have unit: null). Units are free-form strings; normalize in your app if you need a canonical set. |
ingredients[].items[].preparationnullable | string | "cut into 1.3cm (1/2 inch) cubes" | How the ingredient should be prepped before measuring/cooking (diced, peeled, dried, ...). Surface this next to the ingredient line so cooks prep correctly. |
ingredients[].items[].notesnullable | string | "about 30g" | Extra context that doesn't fit quantity/unit (e.g. 'about 30g' for 6 dried chilies, or 'to taste'). null when nothing extra applies. |
ingredients[].items[].substitutions | string[] | [] | Acceptable swaps for this ingredient. Empty here, but populated where it matters (e.g. buttermilk → milk + lemon juice). Use for dietary-flexibility features. |
ingredients[].items[].ingredient_idnullableUUIDv4 | string (uuid) | "09f2eef4-739a-4fca-8b63-97d697257990" | Foreign key into the ingredients table. Pass this UUID to GET /api/v1/ingredients/:id for that ingredient's full per-100g USDA nutrition, or to /recipes?ingredients= to find every recipe using it. null only for rare custom ingredients. |
ingredients[].items[].nutrition_sourcenullable | string | "USDA FoodData Central" | Named provenance of this ingredient's nutrient data. Predominantly 'USDA FoodData Central'; some aggregated ingredients cite 'Aggregated Public Sources'. Lets you show users exactly where the numbers came from. |
Worked example — stewing beef line: { name: "stewing beef", quantity: 910, unit: "g", preparation: "cut into 1.3cm (1/2 inch) cubes", notes: null, substitutions: [], ingredient_id: "09f2eef4-739a-4fca-8b63-97d697257990", nutrition_source: "USDA FoodData Central" }.
instructions — phased, structured, cueable
ApiInstruction → ApiStructuredStepOrdered steps. Each carries human text plus an optional structured object that encodes the verb, temperature (in both units), duration, and doneness cues. The structured layer is what lets apps render smart timers, temperature targets, and 'is it done yet?' checks instead of a wall of prose.
interface ApiInstruction {
step_number: number;
phase: string; // prep | cook | assemble | finish
text: string;
structured: ApiStructuredStep | null;
tips: string[];
}
interface ApiStructuredStep {
action: string; // e.g. "ROAST", "SIMMER"
temperature: {
celsius: number;
fahrenheit: number;
} | null;
duration: string | null; // ISO 8601 duration
doneness_cues: {
visual: string | null;
tactile: string | null;
} | null;
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
instructions[].step_number | number | 1 | 1-indexed ordering. Always render steps by this number; never rely on array position alone. |
instructions[].phaseEnum: prep | cook | assemble | finish | string | "prep" | Cooking stage. Lets you group steps into phase headers or filter to 'just the cooking steps'. The dinner recipe uses prep, cook, and finish. |
instructions[].text | string | "Tear the dried chilies into strips and place them in a bowl. Cover with 240ml (1 cup) of boiling water and soak for 30 minutes." | Full prose instruction for display. This is the canonical human-readable step; structured data supplements, not replaces, it. |
instructions[].structurednullable | ApiStructuredStep | null | { action: "SOAK", temperature: null, duration: "PT30M", doneness_cues: null } | Machine-readable step. Present on most steps; null when a step can't be cleanly structured. Step 1 of dinner: action SOAK, duration PT30M. |
instructions[].structured.actionSCREAMING_SNAKE_CASE verb | string | "SEAR" | Canonical cooking verb (SOAK, DRAIN, SEAR, BOIL, SIMMER, PUREE, SERVE, ROAST, MIX, SLICE, ...). Use to pick icons or verify an automation. Open vocabulary of imperative cooking actions. |
instructions[].structured.temperature | object | null | { celsius: 90, fahrenheit: 194 } | Target temperature in BOTH units, always consistent. Step 5 (the 1-hour simmer) specifies 90°C / 194°F. null when temperature isn't a factor. |
instructions[].structured.durationnullableISO 8601 duration | string | "PT1H" | How long this action takes (PT30M, PT1H). Drive your step timers from this. null for instantaneous actions (e.g. DRAIN, SERVE). |
instructions[].structured.doneness_cues | object | null | { visual: "Beef is deeply browned on all sides", tactile: null } | How to tell the step is done: a visual cue and/or a tactile cue. Step 3 (SEAR) gives a visual cue; step 7 (SIMMER) gives a tactile one ('Beef cubes are fork-tender'). null when the action ends on time/temperature alone. |
instructions[].tips | string[] | [] | Optional technique tips for this step. Empty on the dinner recipe steps but populated where a nudge helps (e.g. 'don't crowd the pan'). Render as a collapsible 'tip' under the step. |
troubleshooting — failure modes & fixes
ApiTroubleshootingA knowledge base of what goes wrong, why, and how to recover — structured for 'my X happened, now what?' lookup. Each entry is a complete symptom → cause → prevention → fix loop.
interface ApiTroubleshooting {
symptom: string;
likely_cause: string;
prevention: string;
fix: string;
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
troubleshooting[].symptom | string | "Beef is tough or chewy" | What the cook observes. Index this for searchable help. The dinner recipe has two: tough beef, and watery chili. |
troubleshooting[].likely_cause | string | "The meat has not simmered long enough to break down connective tissue." | Why the symptom most likely occurred. |
troubleshooting[].prevention | string | "Ensure the liquid is at a very low simmer and the lid is tightly sealed." | How to avoid it next time. |
troubleshooting[].fix | string | "Continue simmering in 15-minute increments until tender." | Recovery action for the current batch. |
chef_notes — technique guidance
string[]An array of free-form technique notes from the recipe's chef — the 'why' behind the method. Not tied to a specific step; these are the principles that make the dish work.
chef_notes: string[];
| Key | Type | Value in /dinner | Description |
|---|---|---|---|
chef_notes[] | string | "For the deepest chili flavor, use a variety of dried chilies like ancho, guajillo, and pasilla. Toasting them briefly before soaking can enhance their aroma." | A technique or philosophy note. The dinner recipe has four notes covering chili selection, searing in batches, low-and-slow simmering, and consistency adjustment. Render as a highlighted callout section, not inline with steps. |
cultural_context — the story
string | nullBackground on the dish's origin and tradition. Nullable — present where there's a meaningful story, null when the dish is generic.
cultural_context: string | null;
| Key | Type | Value in /dinner | Description |
|---|---|---|---|
cultural_contextnullable | string | "Texas Chili, or Chili con Carne, is a hearty stew deeply rooted in Texan culinary tradition. Its origins trace back to the mid-19th century ... The absence of beans and tomatoes is a defining characteristic ..." | Provenance and cultural significance. Here it explains why this chili has no beans or tomatoes. Use for 'about this dish' panels. null when no context is recorded. |
nutrition — 32 USDA nutrients, per serving
ApiNutrition → NutrientValuesPer-serving nutrition computed from USDA FoodData Central ingredient data. per_serving carries all 32 tracked nutrients; every value is a nullable number — handle null per-nutrient, not per-recipe. sources names where the numbers came from.
interface ApiNutrition {
per_serving: NutrientValues;
sources: string[]; // e.g. ["USDA FoodData Central"]
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
nutrition.per_serving | NutrientValues | { calories: 569.02, protein_g: 44.14, ...32 nutrients } | The full 32-nutrient panel, per serving. See the table below for every key. Values are decimals with full precision — round for display, keep precision for calculation. |
nutrition.sources | string[] | ["USDA FoodData Central"] | Provenance of the nutrition calculation. Predominantly ['USDA FoodData Central']. Show this so users trust the numbers. |
All 32 per_serving values are number | null. alcohol_g and caffeine_mg are null on the dinner recipe (a non-alcoholic, decaf dish). Never assume a value is present — null-check each nutrient.
All 32 per-serving nutrients
nutrition.per_serving : NutrientValuesThe same 32-key panel appears as per_serving on recipes and as per_100g on ingredient detail — identical keys, different basis. Every value is number | null. Values shown are the live dinner-recipe numbers per serving.
Energy & macros
| Key | Unit | /dinner value | Notes |
|---|---|---|---|
calories | kcal | 569.02 | Energy per serving. The headline number for most apps. |
protein_g | g | 44.14 | Total protein. High here (beef-forward dish). |
carbohydrates_g | g | 5.59 | Total carbohydrates by difference. |
fat_g | g | 41.99 | Total lipid (fat). Broken down into the four fields below. |
fiber_g | g | 1.97 | Total dietary fiber. |
sugar_g | g | 1.84 | Total sugars (includes intrinsic and added). |
Fat breakdown
| Key | Unit | /dinner value | Notes |
|---|---|---|---|
saturated_fat_g | g | 16.48 | Saturated fatty acids. |
trans_fat_g | g | 2.39 | Trans fatty acids (naturally occurring in ruminant meat here). |
monounsaturated_fat_g | g | 20.66 | Monounsaturated fatty acids. |
polyunsaturated_fat_g | g | 2.29 | Polyunsaturated fatty acids. |
Minerals
| Key | Unit | /dinner value | Notes |
|---|---|---|---|
sodium_mg | mg | 350.75 | Sodium. Note: reflects recipe as written; salt 'to taste' is an estimate. |
potassium_mg | mg | 903.3 | Potassium. |
calcium_mg | mg | 74.24 | Calcium. |
iron_mg | mg | 7.16 | Iron (rich here, from beef). |
magnesium_mg | mg | 60.57 | Magnesium. |
phosphorus_mg | mg | 434.12 | Phosphorus. |
zinc_mg | mg | 16.82 | Zinc (very high, from beef). |
Vitamins
| Key | Unit | /dinner value | Notes |
|---|---|---|---|
vitamin_a_mcg | mcg | 101.01 | Vitamin A (RAE). |
vitamin_c_mg | mg | 11.51 | Vitamin C (ascorbic acid). |
vitamin_d_mcg | mcg | 0.23 | Vitamin D. |
vitamin_e_mg | mg | 2.15 | Vitamin E (alpha-tocopherol). |
vitamin_k_mcg | mcg | 14.73 | Vitamin K. |
vitamin_b6_mg | mg | 0.97 | Vitamin B6. |
vitamin_b12_mcg | mcg | 6.14 | Vitamin B12 (high, from beef). |
thiamin_mg | mg | 0.19 | Thiamin (B1). |
riboflavin_mg | mg | 0.38 | Riboflavin (B2). |
niacin_mg | mg | 10.47 | Niacin (B3). |
folate_mcg | mcg | 12.53 | Folate (total). |
Other
| Key | Unit | /dinner value | Notes |
|---|---|---|---|
cholesterol_mg | mg | 154.7 | Cholesterol (elevated here from the beef). |
water_g | g | 154.87 | Water content of a serving. |
alcohol_g | g | null | Ethanol. null here — the dish is non-alcoholic. |
caffeine_mg | mg | null | Caffeine. null here — no coffee/chocolate contribution. |
Response types & wrappers
How a Recipe is wrapped depending on the endpoint, plus the lighter list-item shape and the ingredient-detail shape that reuses the same nutrient panel.
Response envelope
ApiResponse<T>Metered detail responses (single recipe, single ingredient, generate) are wrapped with data and an optional usage object. List/discovery responses add a meta object instead.
interface ApiResponse<T> {
data: T;
usage?: ApiUsage;
meta?: {
total?: number;
page?: number;
per_page?: number;
total_capped?: boolean; // true if more exist beyond the discovery limit
};
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
data | T | { /* the Recipe object */ } | The payload. Typed by the endpoint — ApiRecipe for /recipes/:id, ApiIngredientDetail for /ingredients/:id, etc. |
usagenullable | ApiUsage | { monthly_remaining: 1950, monthly_limit: 2000, daily_remaining: 95, daily_limit: 100 } | Present only on metered calls (1-credit detail & generate). Use it to track remaining quota in your dashboard. Absent on free discovery/list calls. |
meta.total_cappednullable | boolean | false | On list responses, true when the real result count exceeds the 500-recipe discovery cap. When true, narrow with filters rather than paginating further. |
Recipe list item (lighter weight)
ApiRecipeListItemReturned by GET /api/v1/recipes. Carries identity, meta, dietary, and a 4-macro nutrition_summary — not the full 32 nutrients, ingredients, or instructions. Fetch the detail by id when you need those.
interface ApiRecipeListItem {
id: string;
name: string;
description: string;
category: string;
cuisine: string;
difficulty: string;
tags: string[];
meta: ApiRecipeMeta;
dietary: ApiDietary;
nutrition_summary: {
calories: number | null;
protein_g: number | null;
carbohydrates_g: number | null;
fat_g: number | null;
};
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
nutrition_summary | object | { calories: 569, protein_g: 44.1, carbohydrates_g: 5.6, fat_g: 42 } | Just the four headline macros. The full NutrientValues panel, ingredients, and instructions come from GET /api/v1/recipes/:id (1 credit). |
Ingredient detail (per 100g)
ApiIngredientDetailReturned by GET /api/v1/ingredients/:id. Same 32 nutrient keys as recipe nutrition, but on a per-100g basis. Lets you compute macros for any quantity of any ingredient.
interface ApiIngredientDetail extends ApiIngredientItem {
nutrition: ApiIngredientNutrition | null;
}
interface ApiIngredientNutrition {
per_100g: NutrientValues; // same 32 keys as per_serving
sources: string[];
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
per_100g | NutrientValues | { calories: 146.5, protein_g: 7.14, ... } | The identical 32-nutrient panel as recipe per_serving, but normalized to 100g. Multiply by (your_grams / 100) for arbitrary quantities. |
nutritionnullable | object | { per_100g: { ... }, sources: [...] } | null only for the rare custom ingredient with no USDA match yet. For all USDA/Aggregated ingredients this is populated. |
Error shape
ApiErrorEvery error uses one JSON envelope. Switch on the stable error.code, never the human message.
interface ApiError {
error: {
code: string;
message: string;
};
}| Key | Type | Value in /dinner | Description |
|---|---|---|---|
error.code | string | "UNIQUE_RECIPE_LIMIT_EXCEEDED" | Stable machine code (BAD_REQUEST, UNAUTHORIZED, RATE_LIMITED, NOT_FOUND, ...). See /docs for the full table. 429s include a Retry-After header. |
error.message | string | "Monthly unique recipe limit reached ..." | Human-readable detail. Display-safe, but may change — key off code for logic. |