Set Recipe Ingredients
④ of 5 | Next: ⑤ addRecipeServing
Save the full ingredient list for a recipe in one commit — add, edit, delete, and reorder are all expressed as the complete ordered list you send. Each item is a structured form or resolution row; there is no free-text parsing on this path.
The list is discriminated by source:
• form — a structured search → pick → quantity row. Omit id for a new row (the server mints one) or send an existing irow_... id to edit/reorder it.
• resolution — an existing row born from the resolve workflow. It must carry an existing irow_... id; immutable lineage is inherited server-side and cannot be supplied by the client.
ingredients_entry_method must equal the recipe's locked entry method. A recipe locked to form rejects any resolution row.
position is taken verbatim, so drag-reorder is a resend with the new positions. An empty items list deletes all ingredients.
On success, recipe-level nutrition (L1) and any existing per-serving panels (L2) are recomputed atomically in the same commit.
Next: ⑤ addRecipeServing — add a serving size to generate the final nutrition facts label.
Path Parameters
Header Parameters
Optimistic-concurrency token. Pass the version_id returned by the previous mutation (or by GET /recipes/{id}/versions) so the server can reject the request when another writer has advanced the recipe in the meantime. A stale token (header present but HEAD has moved) surfaces HTTP 409 with the current version_id in the response body. An absent token on a versioned recipe surfaces HTTP 412. Only consumed on API versions that declare the recipe_versioning capability — older versions ignore the header.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
curl -X POST "https://api.bettermenu.live/studio/recipes/string/ingredients" \ -H "If-Match: string" \ -H "Content-Type: application/json" \ -d '{ "ingredients_entry_method": "resolution", "items": [ { "position": 0, "selected_ingredient_id": "string", "selected_ingredient_name": "string", "display_name": "string", "metric_quantity_g": 0, "search_provenance": "system_matched", "unit_conversion_provenance": "identity" } ] }'{
"data": {
"recipe_id": "string",
"resolution_id": "string",
"ingredients": [
{
"id": "string",
"position": 0,
"selected_ingredient_id": "string",
"selected_ingredient_name": "string",
"display_name": "string",
"metric_quantity_g": 0,
"search_provenance": "system_matched",
"unit_conversion_provenance": "identity",
"source": "resolution",
"original_text": "string",
"parsed_quantity": 0,
"parsed_unit": "string",
"parsed_ingredient_name": "string",
"portion_id": "string"
}
],
"ingredient_conversions": [
{
"conversion_source": "string",
"reasoning": "string",
"portion_reference": {
"portion_bmid": "string",
"measure_unit_name": "string",
"gram_weight": "string",
"amount": "string"
}
}
],
"ingredient_resolutions": [
{
"original_text": "string",
"parsed": {
"quantity_text": "string",
"quantity": 0,
"unit": "string",
"ingredient_name": "string"
},
"converted": {
"quantity": 0,
"unit": "string",
"steps": [
{
"from_quantity": 0,
"from_unit": "string",
"to_quantity": 0,
"to_unit": "string",
"source": "string",
"reasoning": "string"
}
]
},
"matches": {
"query_text": "string",
"candidates": [
{
"ingredient_id": "string",
"ingredient_name": "string"
}
],
"selected_match": {
"ingredient_id": "string",
"ingredient_name": "string"
},
"match_confidence": 0,
"search_strategy_used": "standard"
}
}
],
"updated_at": "string"
},
"meta": {
"version": "string",
"request_id": "string",
"timestamp": "string"
},
"version_id": "string"
}{
"type": "https://api.bettermenu.com/errors/recipe_not_found",
"title": "Recipe Not Found",
"status": 400,
"detail": "Recipe rcpe_abc123 does not exist",
"code": "recipe_not_found",
"retryable": true,
"instance": "/api/v1/recipes/rcpe_abc123",
"trace_id": "req_xyz789",
"errors": [
{}
],
"invalid_ids": [
"f_abc123",
"f_xyz789"
]
}