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/json

Create 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-metrics lists every accepted identifier.
  • Operators: LESS_THAN is a maximum,GREATER_THAN is 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

UnitMeaningNeeds in nutritionCalculationContext
KCAL, GRAMSAbsolute valueNothing
PERCENT_OF_TDEECalories as a share of daily energy expendituretdeeKcal
GRAMS_PER_KG_BODY_WEIGHTGrams per kg of body weightbodyWeightKg
GRAMS_PER_KG_LEAN_BODY_MASSGrams per kg of lean body massleanBodyMassKg
PERCENT_OF_CALORIESShare of the daily calorie target (carbohydrate, protein, fat, sugars, saturates)A daily calorie target (see below)
GRAMS_PER_1000_KCALGrams 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:

  1. A CALORIES constraint in PERCENT_OF_TDEE.
  2. Exactly one CALORIES constraint in KCAL. A pair of GREATER_THAN and LESS_THAN defines a range, not a target.
  3. 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.

  • foodPreferences are dietary tags carried by individual ingredients, such asvegetarian and vegan. Ingredients without the tag are not used. Valid values come fromGET /food-preferences.
  • allergens: values fromGET /allergens.
  • excludeIngredients accepts ingredient names (GET /ingredients) and food-group names (GET /food-groups). A food group is expanded to all the ingredients that belong to it.
  • includeIngredients is 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,LUNCH and DINNER entries as you need. There is no separate "number of main meals" parameter.
  • Snacks: add one SNACK entry for each snack per day. Repeat the entry for several snacks.
  • Calorie splits: calorieSplit is 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: useweeks[].mealDefinitionsPerDay for a whole week, orweeks[].days[].mealDefinitionsPerDay for one day. Thedays array 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, foodPreferences and 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-GB

Locales and labels

  • GET /locales lists the supported locales.
  • Set locale on the meal plan request to choose which recipes are used. Add locale as 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-FR returns 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.

EndpointReturns
GET /meal-typesMeal types for mealDefinitionsPerDay[].type
GET /allergensValues for allergens
GET /food-preferencesValues for foodPreferences
GET /food-groupsFood groups for excludeIngredients
GET /ingredientsIngredient names for excludeIngredients and includeIngredients
GET /nutrition-metricsMetrics for nutrition constraints
GET /nutrition-metric-unitsUnits, and which metrics accept them
GET /localesSupported locales
GET /labelsDisplay labels for the identifiers above, per locale