For developers
API guides
Worked, copy-and-paste examples for the most common integration tasks. For the full list of fields and endpoints, see theAPI reference.
Before you start
- Base URL:
https://api.platform.mealiq.io/v1. - Every request carries an API key as a bearer token. Requests with a body use JSON.
- Field names are camelCase. Identifiers such as meal types, allergens and food groups are the same in every locale. Fetch the valid values from thereference data endpoints instead of hard-coding them.
Authorization: Bearer <api-key>
Content-Type: application/jsonCreate your first meal plan
Plan generation is asynchronous. POST /meal-plans returns straight away with a plan id, and you poll for the result.
1. Create the plan
This asks for one week of breakfast, lunch and dinner, with a daily calorie target between 1,800 and 2,000 kcal.
POST /v1/meal-plans
Authorization: Bearer <api-key>
Content-Type: application/json
{
"userId": "user-123",
"servings": 1,
"locale": "en-GB",
"duration": {
"value": 1,
"unit": "WEEKS"
},
"programDefinition": {
"mealDefinitionsPerDay": [
{
"type": "BREAKFAST",
"calorieSplit": 0.25
},
{
"type": "LUNCH",
"calorieSplit": 0.4
},
{
"type": "DINNER",
"calorieSplit": 0.35
}
],
"constraints": [
{
"scope": "PER_DAY",
"nutritionMetricConstraints": [
{
"nutritionMetric": "CALORIES",
"operator": "GREATER_THAN",
"value": 1800,
"units": "KCAL"
},
{
"nutritionMetric": "CALORIES",
"operator": "LESS_THAN",
"value": 2000,
"units": "KCAL"
}
],
"excludeIngredients": [],
"includeIngredients": [],
"foodPreferences": [],
"allergens": []
}
]
}
}The response is a 201 with the new plan's ID:
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "pending"
}2. Poll until it is complete
Call GET /meal-plans/{planId} untilstatus is complete. The statuses arepending, generating, completeand failed. A failed plan carries the reason inerror. Poll every few seconds, not in a tight loop.
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"status": "complete",
"weeks": [
{
"weekNumber": 1,
"days": [
{
"dayOfWeek": 1,
"nutrition": { "calories": 1927.1, "...": "..." },
"meals": [
{ "mealId": "meal-4aefee7e", "mealType": "BREAKFAST", "title": "...", "ingredients": [ "..." ], "nutrition": { "...": "..." } }
]
}
]
}
]
}For long plans you can fetch part of the plan with thefromWeekNumber and toWeekNumber query parameters. To get one meal with per-ingredient nutrition, callGET /meal-plans/{planId}/meals/{mealId}.
Apply nutrition constraints
A constraint is a metric, an operator, a value and a unit. Put them inprogramDefinition.constraints withscope: "PER_DAY":
- Metrics: calories and the main nutrients (protein, carbohydrate, fat, fibre, sugars, saturates and salt).
GET /nutrition-metricslists every accepted identifier. - Operators:
LESS_THANis a maximum,GREATER_THANis a minimum. Use one of each to define a range. - Units:
GET /nutrition-metric-unitslists the units, and which ones each metric accepts.
Absolute and calculated units
| Unit | Meaning | Needs in nutritionCalculationContext |
|---|---|---|
KCAL, GRAMS | Absolute value | Nothing |
PERCENT_OF_TDEE | Calories as a share of daily energy expenditure | tdeeKcal |
GRAMS_PER_KG_BODY_WEIGHT | Grams per kg of body weight | bodyWeightKg |
GRAMS_PER_KG_LEAN_BODY_MASS | Grams per kg of lean body mass | leanBodyMassKg |
PERCENT_OF_CALORIES | Share of the daily calorie target (carbohydrate, protein, fat, sugars, saturates) | A daily calorie target (see below) |
GRAMS_PER_1000_KCAL | Grams per 1,000 kcal (fibre) | A daily calorie target (see below) |
Calculated units let one definition serve many users: the person's profile goes in nutritionCalculationContext and MealIQ resolves the numbers. If you don't have a TDEE, estimate it withPOST /body-metrics/tdee:
POST /v1/body-metrics/tdee
{
"weightKg": 85,
"heightCm": 178,
"ageYears": 42,
"biologicalSex": "MALE",
"activityLevel": "MODERATELY_ACTIVE"
}{
"bmrKcal": 1757.5,
"tdeeKcal": 2724.1,
"activityLevel": "MODERATELY_ACTIVE",
"formula": "MIFFLIN_ST_JEOR"
}Then use tdeeKcal in the plan request. This example keeps calories at 80 to 90% of TDEE, protein at 0.8 g per kg of body weight or more, and carbohydrate at 30% of calories or less:
POST /v1/meal-plans
{
"userId": "user-123",
"servings": 1,
"locale": "en-GB",
"duration": {
"value": 1,
"unit": "WEEKS"
},
"nutritionCalculationContext": {
"tdeeKcal": 2724.1,
"bodyWeightKg": 85
},
"programDefinition": {
"mealDefinitionsPerDay": [
{
"type": "BREAKFAST",
"calorieSplit": 0.25
},
{
"type": "LUNCH",
"calorieSplit": 0.4
},
{
"type": "DINNER",
"calorieSplit": 0.35
}
],
"constraints": [
{
"scope": "PER_DAY",
"nutritionMetricConstraints": [
{
"nutritionMetric": "CALORIES",
"operator": "GREATER_THAN",
"value": 80,
"units": "PERCENT_OF_TDEE"
},
{
"nutritionMetric": "CALORIES",
"operator": "LESS_THAN",
"value": 90,
"units": "PERCENT_OF_TDEE"
},
{
"nutritionMetric": "PROTEIN",
"operator": "GREATER_THAN",
"value": 0.8,
"units": "GRAMS_PER_KG_BODY_WEIGHT"
},
{
"nutritionMetric": "CARBOHYDRATE",
"operator": "LESS_THAN",
"value": 30,
"units": "PERCENT_OF_CALORIES"
}
],
"excludeIngredients": [],
"includeIngredients": [],
"foodPreferences": [],
"allergens": []
}
]
}
}How the daily calorie target is chosen
PERCENT_OF_CALORIES andGRAMS_PER_1000_KCAL need a single daily target. MealIQ picks the first of these that applies:
- A
CALORIESconstraint inPERCENT_OF_TDEE. - Exactly one
CALORIESconstraint inKCAL. A pair ofGREATER_THANandLESS_THANdefines a range, not a target. nutritionCalculationContext.dailyCalorieTargetKcal.
Overrides: program, week, day
Constraints can be set for the whole program, a week (weeks[].constraints) or a single day. The most specific level wins, in the order day, then week, then program. A level that sets nutrition constraints replaces the bounds from the levels above it. It doesn't merge with them.
{
"userId": "user-123",
"servings": 1,
"locale": "en-GB",
"duration": {
"value": 1,
"unit": "WEEKS"
},
"programDefinition": {
"mealDefinitionsPerDay": [
{
"type": "BREAKFAST",
"calorieSplit": 0.25
},
{
"type": "LUNCH",
"calorieSplit": 0.4
},
{
"type": "DINNER",
"calorieSplit": 0.35
}
],
"constraints": [
{
"scope": "PER_DAY",
"nutritionMetricConstraints": [
{
"nutritionMetric": "CALORIES",
"operator": "LESS_THAN",
"value": 2000,
"units": "KCAL"
}
],
"excludeIngredients": [],
"includeIngredients": [],
"foodPreferences": [],
"allergens": []
}
],
"weeks": [
{
"constraints": [
{
"scope": "PER_DAY",
"nutritionMetricConstraints": [
{
"nutritionMetric": "CALORIES",
"operator": "LESS_THAN",
"value": 1700,
"units": "KCAL"
}
],
"excludeIngredients": [],
"includeIngredients": [],
"foodPreferences": [],
"allergens": []
}
]
}
]
}
}How meals are sized
Each meal receives its calorieSplit share of the daily range, with a tolerance of ±2% per meal. A daily total can therefore land marginally outside the range you set.
Diets, allergens and exclusions
There are four tools, and each does a different job.
foodPreferencesare dietary tags carried by individual ingredients, such asvegetarianandvegan. Ingredients without the tag are not used. Valid values come fromGET /food-preferences.allergens: values fromGET /allergens.excludeIngredientsaccepts ingredient names (GET /ingredients) and food-group names (GET /food-groups). A food group is expanded to all the ingredients that belong to it.includeIngredientsis a preference. When several ingredients are valid for a recipe, the ones you list are favoured.
{
"userId": "user-123",
"servings": 1,
"locale": "en-GB",
"duration": {
"value": 1,
"unit": "WEEKS"
},
"programDefinition": {
"mealDefinitionsPerDay": [
{
"type": "BREAKFAST",
"calorieSplit": 0.25
},
{
"type": "LUNCH",
"calorieSplit": 0.4
},
{
"type": "DINNER",
"calorieSplit": 0.35
}
],
"constraints": [
{
"scope": "PER_DAY",
"nutritionMetricConstraints": [
{
"nutritionMetric": "CALORIES",
"operator": "LESS_THAN",
"value": 2000,
"units": "KCAL"
}
],
"excludeIngredients": [
"red meat",
"onion"
],
"includeIngredients": [],
"foodPreferences": [
"vegetarian"
],
"allergens": [
"nuts"
]
}
]
}
}Low carb, ketogenic and similar diets
These diets are defined by the nutrition of a whole meal or day, and they depend on quantities. A low-carb plan can include a smaller portion of rice. A "low carb" ingredient filter would remove rice entirely. So express them asnutrition constraints, for example CARBOHYDRATE LESS_THAN a percentage of calories or a number of grams. Pick the thresholds with your clinical team.
Low FODMAP and other trigger lists
Exclude the specific trigger foods withexcludeIngredients, for example onion,garlic and wheat.
Letting users enter a food to avoid
Exclusions only accept supported values, so a free-text "other allergy" can't be enforced. DownloadGET /food-groups and GET /ingredients once, cache them, and use a local typeahead so the user picks a supported value. Send that identifier in excludeIngredients.
Vary meals and snacks per day
Each entry in mealDefinitionsPerDay produces one meal per day. The number of entries sets the number of meals, and a type can repeat.
- Main meals: add as many
BREAKFAST,LUNCHandDINNERentries as you need. There is no separate "number of main meals" parameter. - Snacks: add one
SNACKentry for each snack per day. Repeat the entry for several snacks. - Calorie splits:
calorieSplitis the share of the daily calories for that meal, from 0 to 1. Splits that add up to 1.0 divide the daily target between the meals. The API doesn't require this: splits that add up to more than 1.0 let the day exceed its calorie total when you need it to. - Irregular days: use
weeks[].mealDefinitionsPerDayfor a whole week, orweeks[].days[].mealDefinitionsPerDayfor one day. Thedaysarray is positional and starts on Monday, so use empty{}entries to skip days.
This request creates a week with 3 main meals and 2 snacks each day, and only lunch and dinner on Wednesday:
POST /v1/meal-plans
{
"userId": "user-123",
"servings": 1,
"locale": "en-GB",
"duration": {
"value": 1,
"unit": "WEEKS"
},
"nutritionCalculationContext": {
"dailyCalorieTargetKcal": 2000
},
"programDefinition": {
"mealDefinitionsPerDay": [
{
"type": "BREAKFAST",
"calorieSplit": 0.25
},
{
"type": "SNACK",
"calorieSplit": 0.1
},
{
"type": "LUNCH",
"calorieSplit": 0.3
},
{
"type": "SNACK",
"calorieSplit": 0.1
},
{
"type": "DINNER",
"calorieSplit": 0.25
}
],
"constraints": [
{
"scope": "PER_DAY",
"nutritionMetricConstraints": [
{
"nutritionMetric": "CALORIES",
"operator": "GREATER_THAN",
"value": 1900,
"units": "KCAL"
},
{
"nutritionMetric": "CALORIES",
"operator": "LESS_THAN",
"value": 2100,
"units": "KCAL"
}
],
"excludeIngredients": [],
"includeIngredients": [],
"foodPreferences": [],
"allergens": []
}
],
"weeks": [
{
"days": [
{},
{},
{
"mealDefinitionsPerDay": [
{
"type": "LUNCH",
"calorieSplit": 0.5
},
{
"type": "DINNER",
"calorieSplit": 0.5
}
]
}
]
}
]
}
}Snacks on some days only work the same way, using the day override. For a user who has snacks every day, put the SNACKentries in the default list. For "never", leave them out.
Create and use Programs
A Program is a saved set of meal definitions and constraints. Use Programs when clinicians or your team define nutrition protocols once and your app lets users pick one. You can create Programs through the API or in the MealIQ Platform UI (Studio). Programs are optional. You can always send constraints directly in the meal plan request.
1. Create a Program
POST /v1/programs
{
"name": "Low carb 2000 kcal",
"programDefinition": {
"mealDefinitionsPerDay": [
{
"type": "BREAKFAST",
"calorieSplit": 0.25
},
{
"type": "LUNCH",
"calorieSplit": 0.4
},
{
"type": "DINNER",
"calorieSplit": 0.35
}
],
"constraints": [
{
"scope": "PER_DAY",
"nutritionMetricConstraints": [
{
"nutritionMetric": "CALORIES",
"operator": "LESS_THAN",
"value": 2000,
"units": "KCAL"
},
{
"nutritionMetric": "CARBOHYDRATE",
"operator": "LESS_THAN",
"value": 26,
"units": "PERCENT_OF_CALORIES"
}
],
"excludeIngredients": [],
"includeIngredients": [],
"foodPreferences": [],
"allergens": []
}
]
}
}2. List and fetch Programs
GET /programs returns the list, andGET /programs/{programId} returns one with itsprogramDefinition. Update withPUT /programs/{programId} and remove withDELETE /programs/{programId}.
[
{
"id": 3,
"name": "Low carb 2000 kcal",
"createdAt": "2026-06-19T12:33:42.487785Z",
"updatedAt": "2026-06-19T12:33:42.487785Z"
}
]3. Use it in a meal plan
Copy the programDefinition object intoPOST /meal-plans. If the Program uses calculated units, include nutritionCalculationContext with the user's profile.
{
"userId": "user-123",
"servings": 1,
"locale": "en-GB",
"duration": { "value": 1, "unit": "WEEKS" },
"nutritionCalculationContext": { "tdeeKcal": 2724.1, "bodyWeightKg": 85 },
"programDefinition": { "...": "the programDefinition object from GET /programs/{id}" }
}Replace a meal
Swapping a meal takes three steps: find alternatives that still fit the day's nutrition, choose one, and write it into the plan.
1. Search for alternatives
Send POST /recipes with:
replacedMealType, the type of the meal being replaced.appliedNutritionConstraints, the constraints in force, at program, week or day level.otherMealsTotalNutrition, the summed nutrition of the other meals on the same day. This lets the search fit the new meal into what remains.nutritionCalculationContext, if those constraints use calculated units.- Optionally
excludeIngredients,foodPreferencesand similar filters.
POST /v1/recipes?limit=20
{
"replacedMealType": "LUNCH",
"locale": "en-GB",
"servings": 1,
"appliedNutritionConstraints": {
"program": {
"scope": "PER_DAY",
"nutritionMetricConstraints": [
{
"nutritionMetric": "CALORIES",
"operator": "LESS_THAN",
"value": 2000,
"units": "KCAL"
}
],
"excludeIngredients": [],
"includeIngredients": [],
"foodPreferences": [],
"allergens": []
}
},
"otherMealsTotalNutrition": {
"calories": 1200.4,
"protein": 62.9,
"carbohydrate": 129.7,
"fat": 48.6,
"fibre": 16.1,
"sugars": 23.2,
"saturates": 10.4,
"salt": 1.1
},
"excludeIngredients": [
"white rice"
]
}Results are paged with offset and limit. Each entry's recipe is a complete meal in the same shape as the meals in a plan:
{
"data": [
{ "recipe": { "mealId": "...", "mealType": "LUNCH", "title": "...", "ingredients": [ "..." ], "nutrition": { "...": "..." } }, "fullTextScore": 0 }
],
"nextOffset": 1,
"moreAvailable": true,
"estimatedTotalResults": 12
}2. Write the chosen meal into the plan
Send PATCH /meal-plans/{planId} with the week, day and ID of the meal being replaced, and the chosenrecipe object as meal. The response is the updated plan, with the day's nutrition recalculated.
{
"weekNumber": 1,
"dayOfWeek": 2,
"mealId": "meal-6782edb7",
"meal": { "...": "the object from data[n].recipe in the search response" }
}Shopping lists
GET /meal-plans/{planId}/shopping-list returns the plan's ingredients added up into one list, sorted by food classification. Pass locale to get names in another language.
GET /v1/meal-plans/{planId}/shopping-list?locale=en-GBLocales and labels
GET /localeslists the supported locales.- Set
localeon the meal plan request to choose which recipes are used. Addlocaleas a query parameter onGET /meal-plans/{planId}, the meal endpoint and the shopping list to read titles, steps and ingredient names in that language. GET /labels?locale=fr-FRreturns display text for the identifiers you send, such as allergens, meal types, food groups, food preferences, nutrition metrics and units. Identifiers never change between locales. Only the labels do.
Reference data endpoints
Use these to populate your own UI and to validate the values you send.
| Endpoint | Returns |
|---|---|
GET /meal-types | Meal types for mealDefinitionsPerDay[].type |
GET /allergens | Values for allergens |
GET /food-preferences | Values for foodPreferences |
GET /food-groups | Food groups for excludeIngredients |
GET /ingredients | Ingredient names for excludeIngredients and includeIngredients |
GET /nutrition-metrics | Metrics for nutrition constraints |
GET /nutrition-metric-units | Units, and which metrics accept them |
GET /locales | Supported locales |
GET /labels | Display labels for the identifiers above, per locale |