Recipe API
Canonical reference

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;
}
KeyTypeValue in /dinnerDescription
id
UUIDv4
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.
category
Controlled 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.
cuisine
Controlled vocabulary
string"American"Cuisine taxonomy value, e.g. Italian, American, Mexican. Listed by GET /api/v1/cuisines; substring-filterable via ?cuisine= on /recipes.
difficulty
Enum: 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

ApiRecipeMeta

Timing 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
}
KeyTypeValue in /dinnerDescription
meta.active_time
ISO 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_time
ISO 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_time
ISO 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
booleanfalseTrue 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
number4Numeric yield (here, 4 servings). Use this to scale ingredient quantities: desired_servings / yield_count is your scaling factor.
meta.serving_size_g
nullablegrams
number300Weight 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

ApiDietary

Two 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"]
}
KeyTypeValue in /dinnerDescription
dietary.flags
Controlled 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_for
Open 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

ApiStorage

How 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;
}
KeyTypeValue in /dinnerDescription
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.duration
ISO 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.reheating
nullable
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
booleanfalseTrue 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

ApiEquipment

An 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;
}
KeyTypeValue in /dinnerDescription
equipment[].name
string"Blender"Tool name as a cook would recognise it. Index 0 of the dinner recipe is 'Blender'.
equipment[].required
booleantrueWhether the tool is essential. false items are nice-to-have; you can still cook the recipe without them.
equipment[].alternative
nullable
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 → ApiIngredient

The 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"
}
KeyTypeValue in /dinnerDescription
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[].quantity
nullable
number910Numeric 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[].unit
nullableOpen 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[].preparation
nullable
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[].notes
nullable
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_id
nullableUUIDv4
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_source
nullable
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 → ApiStructuredStep

Ordered 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;
}
KeyTypeValue in /dinnerDescription
instructions[].step_number
number11-indexed ordering. Always render steps by this number; never rely on array position alone.
instructions[].phase
Enum: 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[].structured
nullable
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.action
SCREAMING_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.duration
nullableISO 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

ApiTroubleshooting

A 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;
}
KeyTypeValue in /dinnerDescription
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[];
KeyTypeValue in /dinnerDescription
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 | null

Background 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;
KeyTypeValue in /dinnerDescription
cultural_context
nullable
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 → NutrientValues

Per-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"]
}
KeyTypeValue in /dinnerDescription
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 : NutrientValues

The 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

KeyUnit/dinner valueNotes
calorieskcal569.02Energy per serving. The headline number for most apps.
protein_gg44.14Total protein. High here (beef-forward dish).
carbohydrates_gg5.59Total carbohydrates by difference.
fat_gg41.99Total lipid (fat). Broken down into the four fields below.
fiber_gg1.97Total dietary fiber.
sugar_gg1.84Total sugars (includes intrinsic and added).

Fat breakdown

KeyUnit/dinner valueNotes
saturated_fat_gg16.48Saturated fatty acids.
trans_fat_gg2.39Trans fatty acids (naturally occurring in ruminant meat here).
monounsaturated_fat_gg20.66Monounsaturated fatty acids.
polyunsaturated_fat_gg2.29Polyunsaturated fatty acids.

Minerals

KeyUnit/dinner valueNotes
sodium_mgmg350.75Sodium. Note: reflects recipe as written; salt 'to taste' is an estimate.
potassium_mgmg903.3Potassium.
calcium_mgmg74.24Calcium.
iron_mgmg7.16Iron (rich here, from beef).
magnesium_mgmg60.57Magnesium.
phosphorus_mgmg434.12Phosphorus.
zinc_mgmg16.82Zinc (very high, from beef).

Vitamins

KeyUnit/dinner valueNotes
vitamin_a_mcgmcg101.01Vitamin A (RAE).
vitamin_c_mgmg11.51Vitamin C (ascorbic acid).
vitamin_d_mcgmcg0.23Vitamin D.
vitamin_e_mgmg2.15Vitamin E (alpha-tocopherol).
vitamin_k_mcgmcg14.73Vitamin K.
vitamin_b6_mgmg0.97Vitamin B6.
vitamin_b12_mcgmcg6.14Vitamin B12 (high, from beef).
thiamin_mgmg0.19Thiamin (B1).
riboflavin_mgmg0.38Riboflavin (B2).
niacin_mgmg10.47Niacin (B3).
folate_mcgmcg12.53Folate (total).

Other

KeyUnit/dinner valueNotes
cholesterol_mgmg154.7Cholesterol (elevated here from the beef).
water_gg154.87Water content of a serving.
alcohol_ggnullEthanol. null here — the dish is non-alcoholic.
caffeine_mgmgnullCaffeine. 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
  };
}
KeyTypeValue in /dinnerDescription
data
T{ /* the Recipe object */ }The payload. Typed by the endpoint — ApiRecipe for /recipes/:id, ApiIngredientDetail for /ingredients/:id, etc.
usage
nullable
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_capped
nullable
booleanfalseOn 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)

ApiRecipeListItem

Returned 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;
  };
}
KeyTypeValue in /dinnerDescription
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)

ApiIngredientDetail

Returned 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[];
}
KeyTypeValue in /dinnerDescription
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.
nutrition
nullable
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

ApiError

Every error uses one JSON envelope. Switch on the stable error.code, never the human message.

interface ApiError {
  error: {
    code: string;
    message: string;
  };
}
KeyTypeValue in /dinnerDescription
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.