# Labelify API - LLM Integration Guide Labelify does not use LLMs in its product. This document helps LLM applications (ChatGPT, Claude, etc.) interact with the Labelify API on behalf of users for nutrition label compliance tasks. ## Preferred: MCP Server If your AI client supports the Model Context Protocol (Claude Desktop, Claude.ai Connectors, ChatGPT Apps, Cursor, Windsurf, Cline, Zed, VS Code Copilot, JetBrains AI), connect via: - MCP endpoint: `https://apie.labelify.ca/mcp` - OAuth metadata: `https://apie.labelify.ca/.well-known/oauth-authorization-server` - Auth: OAuth 2.1 + Dynamic Client Registration (PKCE S256). No API key paste. The MCP server exposes: `labelify_brief` (orientation), `list_endpoints` + `get_endpoint` (discover one endpoint's schema on demand instead of loading this whole reference up front), `search_foods` (`all=true`), `list_reference_categories` + `get_reference_category` (Table of Reference Amounts), and `labelify_request` (generic REST passthrough). This cheatsheet is also available as the `labelify://docs` resource. ### Destructive operations (DELETE) require explicit user confirmation Calling `labelify_request` with `method=DELETE` and no `confirmation_token` returns an error result containing a single-use token. You MUST: (1) tell the user exactly what you're about to do, including the full path; (2) wait for explicit chat confirmation; (3) re-call the tool with `confirmation_token=`. Tokens expire in 120s, are single-use, and bound to the originally-requested method+path. Never invent or reuse a token. There is no undo on DELETE. If your AI client does not support MCP, keep reading — this document covers the REST API directly. ## What is Labelify? Labelify is online software that lets food businesses create their own Nutrition Facts tables in minutes. Users build recipes from a database of 5,000+ ingredients, and Labelify calculates nutritional values and generates regulation-compliant labels for Canadian and US markets—including bilingual formats and front-of-package symbols. ## API Basics Base URL: https://apie.labelify.ca Version: v1 Content-Type: application/json OpenAPI Specification: https://apie.labelify.ca/labelify_openapi.json Interactive Docs: https://apie.labelify.ca/summer The OpenAPI spec contains complete request/response schemas for all endpoints. LLMs should fetch this spec for detailed field definitions, validation rules, and enum values not fully documented here. ## Terminology Note In the Labelify UI, "Foods" are displayed as "Ingredients" to users. When a user mentions "ingredients," they may mean: - **Foods** (API): Raw ingredients in the database with nutritional data (e.g., "flour", "sugar") - **Recipe ingredients** (API): Foods added to a recipe with quantities (the `ingredients` array in a recipe) Context usually clarifies: "add an ingredient to my database" → create a Food; "add an ingredient to my recipe" → add to recipe's ingredients list. ## Authentication All API requests require the `Authorization` header with an API key. Header: Authorization Format: API-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX Example: ``` Authorization: API-12345678-ABCD-1234-EFGH-567890ABCDEF ``` ### How to Get an API Key Log in at https://app.labelify.ca → user avatar (top-left) → "API" → copy the key. "Regenerate API key" invalidates the previous one. Requires an active subscription with API permissions. ## Rate Limits Authenticated requests (with API key): | Metric | Limit | |--------|-------| | Requests per minute | 500 | | Requests per hour | 5,000 | | Concurrent requests | 10 | When limits are exceeded, the API returns HTTP 429 with a `Retry-After` header indicating seconds to wait. ## Error Responses All errors return JSON with this structure: ```json { "code": 1005, "type": "InvalidOrMissing", "message_en": "Recipe not found", "message_fr": "Recette introuvable" } ``` Rate limit errors (429) include additional fields: ```json { "code": 4029, "type": "RateLimitExceeded", "message_en": "Rate limit exceeded. Please try again later.", "message_fr": "Limite de taux dépassée. Veuillez réessayer plus tard.", "retry_after": 60, "details": "500 per 1 minute" } ``` Common error codes: | Code | Type | Meaning | |------|------|---------| | 1001 | ValidationError | Invalid request data | | 1005 | InvalidOrMissing | Resource not found | | 1010 | VersionConflict | Stale data, refetch and retry | | 1101 | Unauthorized | Invalid or missing API key | | 1429 | ConcurrencyLimitExceeded | Too many concurrent requests | | 4029 | RateLimitExceeded | Rate limit exceeded | HTTP status codes: 400 (bad request), 403 (forbidden/unauthorized), 404 (not found), 422 (validation), 429 (rate limit) Entities return a `version_id` for optimistic concurrency. Pass it on updates — if it's stale, the server returns 1010 (VersionConflict); refetch and retry. --- ## App URLs (share with users after create/update) After creating or updating a resource, share the app URL so the user can view/edit it in the browser. App base: `https://app.labelify.ca` | Resource | URL pattern | |----------|-------------| | Recipe | `https://app.labelify.ca/recipes/{recipe_id}` | | Food / Ingredient | `https://app.labelify.ca/ingredients/{food_id}` | | Label | `https://app.labelify.ca/labels/{label_id}` | | FOP label | `https://app.labelify.ca/fop/{fop_label_id}` | The URLs also accept an optional trailing slug for readability (e.g. `/recipes/{id}/chocolate-chip-cookies`), but the ID alone is sufficient — the app ignores the slug. Tags, labels-index, and other listing pages are not linkable per-item. Surface the link whenever you create a resource, update an existing one, or reference one in a reply. Example: "Created recipe — view it at https://app.labelify.ca/recipes/abc123". --- ## Core Resources ### Foods Foods are raw ingredients with nutritional data per reference amount (typically 100g). The database includes 5,000+ pre-loaded ingredients from CNF (Canadian Nutrient File) and USDA sources. **Always search existing foods before creating new ones** — most common ingredients already exist with complete nutritional data. **List foods** ``` GET /v1/foods?page=1&page_size=20&short=true ``` Query parameters: - search: text search in name fields - all: **must be `true` to include public database ingredients (CNF, USDA).** Default `false` returns only the user's custom ingredients. - source: filter by origin (only works with `all=true`). Values: `USER`, `CNF`, `USDA`. Comma-separated for multiple: `USER,CNF` - tags: comma-separated tag IDs - short: true returns minimal fields (recommended for search) - expand: nutriments_100g,tags (additional data) To find an existing ingredient: `GET /v1/foods?search=flour&all=true&short=true`. Try different search terms if the first doesn't match (e.g., "all-purpose flour", "wheat flour", "flour"). **Get single food** ``` GET /v1/foods/{food_id} ``` **Get food by product code (SKU)** ``` GET /v1/foods/by_product_code/{product_code} ``` **Create food** ``` POST /v1/foods Content-Type: application/json { "name": "All-purpose flour", "mass_quantity": "100", "mass_unit": "g", "nutriments": { "calories": {"amount": "364", "unit": "kcal"}, "protein": {"amount": "10.33", "unit": "g"}, "carbohydrate": {"amount": "76.31", "unit": "g"}, "fat": {"amount": "0.98", "unit": "g"}, "fibre": {"amount": "2.7", "unit": "g"}, "sugars": {"amount": "0.27", "unit": "g"}, "sodium": {"amount": "2", "unit": "mg"} } } ``` Required: name Optional: brand, source, product_code, allergies, volume_quantity/volume_unit, common_name_en/fr `mass_quantity` + `mass_unit` define the **reference basis** for the nutriments — i.e., the nutriments are the values per that mass. If a supplier spec is "per 28 g serving", set `mass_quantity: "28"`, `mass_unit: "g"` and submit the supplier's values as-is. No need to convert to per-100g. **Always set `allergies` on custom foods.** Check the supplier's "Contains:" statement and scan the ingredient list for hidden allergens. Get valid keys from `GET /v1/utils/allergies`. Capture "may contain" statements verbatim. Allergens flagged `to_specify: true` in that list (Fish, Crustaceans, Shellfish, Other) require the **specific species/variety** on the label — submit it directly in `name_en` / `name_fr` (e.g., `"Salmon"` / `"Saumon"`, not `"Fish"` / `"Poissons"`). There is no separate "specify" field on the payload. **Fill `_fr` fields.** Canadian federal labels require the common name, ingredient list (with allergen declarations), and NFt to be bilingual. Custom food names, common names, and ingredient-list overrides all need French equivalents — CNF has them for standard foods; don't machine-translate regulatory terms. `common_name_en` / `common_name_fr` is the ingredient's **display name on the final label's ingredient list** — the regulated plain-language term, not a brand/SKU. Example: `"Butter - Unsalted - Organic Grass-Fed"` → `common_name_en: "butter"`. Plain words only (`"sugar"` not `"sucrose"`). Set both EN and FR. CFIA mandates specific forms for some ingredients: starches/lecithin must declare plant source (`"wheat starch"`, `"soy lecithin"`); fish/shellfish use species name (`"salmon"`, `"shrimp"`). For standardized foods (mayonnaise, milk chocolate, etc.) use the exact name from the Canadian Food Compositional Standards. **Update food** ``` POST /v1/foods/{food_id} ``` Updates provided fields. Sub-collections (nutriments, custom_units) are replaced entirely—items not in the request are deleted. **Update food (merge)** ``` PATCH /v1/foods/{food_id} ``` Updates provided fields. Sub-collections are merged—adds/updates items without deleting missing ones. **Delete food** ``` DELETE /v1/foods/{food_id} ``` **Silent orphan warning**: deleting a food does not remove ingredients that reference it. Recipes still using the food end up with an ingredient whose food link is broken and whose nutriments stop updating — no error is raised. Before deleting, search recipes (`GET /v1/recipes?food_id=...`) and either remove the ingredient or swap it to a replacement food. **Technical file (supplier documentation)** Attach one supplier document (spec sheet, CoA, product label) to a food. Uploading replaces any existing file. Not available on trial accounts. ``` POST /v1/foods/{food_id}/technical_file (multipart, field: upload_file) GET /v1/foods/{food_id}/technical_file (binary download) DELETE /v1/foods/{food_id}/technical_file (body: {}) ``` --- ### Recipes Recipes combine foods with quantities. Nutritional values are calculated automatically. **List recipes** ``` GET /v1/recipes?page=1&page_size=20 ``` Query parameters: - search: text search in name - food_id: recipes containing this food - recipe_id: recipes containing this subrecipe - tags: comma-separated tag IDs - expand: nutriments,nutriments_100g,previews_CAN2016,previews_US,tags **Get single recipe** ``` GET /v1/recipes/{recipe_id} ``` Optional query parameters: - resized_view_qty: calculate nutrition for specific serving (e.g., "150") - resized_view_unit: unit for resized view (e.g., "g") **Get recipe by product code (SKU)** ``` GET /v1/recipes/by_product_code/{product_code} ``` **Create recipe** ``` POST /v1/recipes Content-Type: application/json { "name": "Chocolate Chip Cookies", "ingredients": [ {"food_id": "abc123", "quantity": "250", "unit": "g", "order": 1}, {"food_id": "def456", "quantity": "200", "unit": "g", "order": 2}, {"food_id": "ghi789", "quantity": "100", "unit": "g", "order": 3} ] } ``` Required: name Optional: instructions_en/fr, notes_en/fr, product_code, common_name_en/fr **Update recipe** ``` POST /v1/recipes/{recipe_id} ``` Updates provided fields. Ingredients list is replaced entirely—ingredients not in the request are deleted. **Update recipe (merge)** ``` PATCH /v1/recipes/{recipe_id} ``` Updates provided fields. Ingredients are merged—adds/updates without deleting missing ones. **Delete recipe** ``` DELETE /v1/recipes/{recipe_id} ``` Deletes the recipe's labels and FOP labels in one step — don't ask the user to confirm those separately. Labels that reference the recipe only via `label_additional_recipes` survive (minus that entry). **Get recipe nutriments** ``` GET /v1/recipes/{recipe_id}/nutriments?qty=100&unit=g ``` Returns nutritional values for specified quantity. Omit parameters for full recipe. #### Yield and Refuse **Yield** captures weight change during cooking (usually water loss). Set `yield_qty` + `yield_unit` on the recipe — either `%` directly, or a mass unit (g, kg) and Labelify computes the percentage. 500 g raw → 400 g finished = 80% yield. Per-serving values are calculated against the finished weight. Volume yield units trigger WN-6. For cooked/baked/simmered/dehydrated products, **ask the user to weigh the finished product** — without yield, concentrations are understated. **Refuse** (per-ingredient) is the inedible portion that **stays in the recipe** but is discarded by the consumer (bones in chicken, shells in shrimp, pits in olives). The weight counts toward the package but not toward edible weight or nutrition. Set `refuse_pct: "10"` so the recipe separates total weight from edible weight. Foods can carry a suggested refuse that pre-fills or auto-applies when the ingredient is added. **Decide before you set it.** Set `refuse_pct` only when the `quantity` you entered is the **whole purchased weight** (whole bird with bones, head of garlic, mango with pit and skin). Leave it null when `quantity` is already the **prepped/edible weight** (chicken meat trimmed, peeled garlic cloves, diced peeled mango). A food's `food_refuse_pct` is a *suggestion*; it only auto-applies on add if `food_auto_apply_refuse` is true on that food — otherwise the recipe author has to decide. Setting refuse on already-prepped weights silently inflates per-serving nutrient values. #### Nutriment Overrides When calculated values don't match reality (e.g., lab-tested values on the finished product), override at the recipe level: `nutriments_overwrite` with `nutriments_overwrite_qty` / `nutriments_overwrite_unit` as the basis. Bypasses per-ingredient calculation. Requires `expand=nutriments_overwrite` on the GET to see it. Missing the qty/unit triggers WN-11 (values ignored). --- ### Ingredients Manage individual ingredients within a recipe. **Add ingredient to recipe** ``` POST /v1/recipes/{recipe_id}/ingredients Content-Type: application/json { "food_id": "abc123", "quantity": "100", "unit": "g", "order": 1 } ``` Alternative: use `source_recipe_id` instead of `food_id` to add a sub-recipe as ingredient. Optional fields: - ingredient_list_display_as_overwrite_en/fr: per-recipe override of the displayed name (use only when this recipe needs a different label term than the food's default) - ingredient_list_class_name: renaming/grouping class (see below) - ingredient_list_at_end: place at end of list (boolean, requires compatible class_name) - ingredient_list_hide_subingredients: hide sub-recipe ingredients in parentheses (boolean) - ingredient_list_merge_subingredients: merge sub-recipe ingredients into main list by weight (boolean) - refuse_pct: inedible portion that stays in the recipe (e.g., "10" for 10% bones). Set only when `quantity` is the whole purchased weight; leave null when already prepped. See "Yield and Refuse". - order: render position. Lower renders first. Gaps are accepted on input — the system normalizes to contiguous on save, so don't bother renumbering everything when you insert one in the middle. **Displayed name precedence** (verified against rendered output): 1. ingredient's `ingredient_list_display_as_overwrite_en/fr` (per-recipe) 2. food's `common_name_en/fr` (default across all recipes — **set this on the food**, not the override field, unless only one recipe needs a different name) 3. food's `name` For sub-recipe ingredients, precedence is: override → sub-recipe's `common_name_en/fr` → sub-recipe's `name`. #### Ingredient List Class Names Assign `ingredient_list_class_name` to control renaming and grouping. `ingredient_list_at_end: true` only moves an ingredient to the end when the class_name is one of the end-compatible classes below — with any other class (e.g. `sugar`) or no class, `at_end` is silently ignored. **CAN2016 behavior:** | Class Name | Rename | End allowed | |------------|--------|-------------| | sugar | Groups as "Sugars (item1, item2, ...)" | No | | spice | Renamed to "Spices" | Yes (requires `at_end: true`) | | seasoning | Renamed to "Seasonings" | Yes (requires `at_end: true`) | | herb | Renamed to "Herbs" | Yes (requires `at_end: true`) | | natural_flavour | Renamed to "Natural flavours" | Yes (requires `at_end: true`) | | artificial_flavour | Renamed to "Artificial flavours" | Yes (requires `at_end: true`) | | colour | Keeps original name | Yes (requires `at_end: true`) | | at_end | Keeps original name | Yes (requires `at_end: true`) | **US behavior:** All class names stay in weight order (`at_end` has no effect). Only renaming applies: spice→"spices", natural_flavour→"natural flavor", artificial_flavour→"artificial flavor". Others keep original names. **Sugar grouping example (CAN2016):** ``` POST /v1/recipes/{recipe_id}/ingredients { "food_id": "glucose-syrup-id", "quantity": "15", "unit": "g", "ingredient_list_class_name": "sugar" } ``` Multiple ingredients with class_name "sugar" are combined: - Input: glucose syrup (15g), fructose (10g), honey (5g) - all tagged as "sugar" - Output ingredient list: "...Sugars (glucose syrup, fructose, honey)..." CAN2016 **requires** sugar grouping when a recipe has 2+ sugars-based ingredients (sugar, honey, syrup, molasses, glucose, etc.). Skip only if: single sugars-based ingredient whose name contains "sugar", recipe declares 0g sugars, product is the sweetener itself, or it's unsweetened fruit/vegetable juice/purée. **Placing spices at end (CAN2016):** ``` POST /v1/recipes/{recipe_id}/ingredients { "food_id": "cumin-id", "quantity": "2", "unit": "g", "ingredient_list_class_name": "spice", "ingredient_list_at_end": true } ``` Without `at_end: true`, the ingredient would be renamed to "Spices" but stay in weight order. CFIA Table 2 permits these class names (spices, herbs, seasonings, natural/artificial flavours, colour) to consolidate small-quantity items under one term; `at_end: true` uses the regulation's "end-of-list items in any order" allowance. Rendered ingredient lists are auto-cased (CAN2016: first word capitalized, rest lowercase). Submitted name casing is not preserved. #### Nested Recipes (Recipes as Ingredients) > Note: this is the live `source_recipe_id` approach — not the legacy `subrecipe` food-level endpoint in the OpenAPI spec (static snapshots, no auto-update). Always use `source_recipe_id`. Use nested recipes when a component is reused across multiple products (e.g., a base sauce, a pre-made dough, a spice blend) or when you want to maintain its nutrition and allergens in one place. Add a recipe as an ingredient by passing `source_recipe_id` instead of `food_id`. The nested recipe's nutrition, allergens, may-contains, and ingredient list roll up into the parent automatically — change it and every parent recalculates. #### Nested Recipe Display Control how the nested recipe's ingredients render on the parent's ingredient list: | Mode | Flag | Output (nested "Tomato Sauce") | |------|------|-------------------------------| | Default | — | `Tomato Sauce (tomatoes, water, salt, spices), Cheese, Flour` | | Hide sub-ingredients | `ingredient_list_hide_subingredients: true` | `Tomato Sauce, Cheese, Flour` | | Merge sub-ingredients | `ingredient_list_merge_subingredients: true` | `Flour, Tomatoes, Cheese, Water, Salt, Spices` (by weight) | Example: ``` POST /v1/recipes/{recipe_id}/ingredients { "source_recipe_id": "tomato-sauce-recipe-id", "quantity": "200", "unit": "g", "ingredient_list_merge_subingredients": true } ``` **Get ingredient** ``` GET /v1/recipes/{recipe_id}/ingredients/{ingredient_id} ``` **Update ingredient** ``` POST /v1/recipes/{recipe_id}/ingredients/{ingredient_id} ``` **Delete ingredient** ``` DELETE /v1/recipes/{recipe_id}/ingredients/{ingredient_id} ``` --- ### Labels (Nutrition Facts Tables) Labels render regulation-compliant Nutrition Facts tables from recipes. **List labels** ``` GET /v1/labels?recipe_id={recipe_id} ``` **Create label** ``` POST /v1/labels Content-Type: application/json { "recipe_id": "abc123", "concrete_type": "CAN2016", "serving_qty": "30", "serving_unit": "g", "reference_qty": "100", "reference_unit": "g", "household_serving_en": "1 cookie", "household_serving_fr": "1 biscuit", "label_format": "standard_bilingual" } ``` concrete_type options: - "CAN2016" - Canadian 2016 regulations (current, recommended) - "US" - United States FDA format #### Choosing Serving and Reference Amounts - **`serving_qty` / `serving_unit`**: the amount one person eats at a time. This is what the consumer sees on the label. - **`reference_qty` / `reference_unit`**: the regulatory reference amount for the food category — set by law, not a free choice. Nutrient rounding rules and "low/high" / "source of" claims are relative to this. **Always look up the value for the specific product before creating a label** — never infer from a similar product or from memory. Wrong reference amount = non-compliant label and incorrect claims. - **MCP clients**: call `list_reference_categories` to find the matching code, then `get_reference_category` for HM/MM presentation patterns. Snapshot date is in every response — warn the user if it looks stale. - **REST consumers**: the same tables are at [Health Canada (CAN2016)](https://www.canada.ca/en/health-canada/services/technical-documents-labelling-requirements/table-reference-amounts-food/nutrition-labelling.html) and [FDA RACC (US)](https://www.fda.gov/regulatory-information/search-fda-guidance-documents/guidance-industry-reference-amounts-customarily-consumed-list-products-each-product-category). - **`household_serving_en` / `household_serving_fr`**: plain-language description of the serving — count + unit only, e.g., `"1 cookie"`, `"3 crackers"`, `"1 cup"`. **Do NOT append the metric weight/volume yourself** — Labelify auto-appends `(serving_qty serving_unit)` from the metric fields when rendering, so `"3 crackers (30 g)"` would render as `"Per 3 crackers (30 g) (30 g)"`. Required when the metric serving doesn't map to an obvious household measure. #### Volume Servings and Density If `serving_unit` or `reference_unit` is a volume (mL, cup, tbsp, etc.) and the recipe has no density, the label assumes 1 g/mL and emits **WN-7** (serving), **WN-8** (reference), or **WN-12** (nutriments_overwrite). Wrong for most products (oils, syrups, doughs, dairy drinks). Help the user measure and set `density_overwrite` (g/mL) by weighing a known volume of the finished product. Out-of-range values trigger **WN-4**. #### CAN2016 Label Formats | label_format | Description | Use Case | |--------------|-------------|----------| | standard | Standard (English only) | Single-language products | | standard_bilingual | Standard Bilingual | Most common - English/French side-by-side | | narrow | Narrow Standard | Tall, narrow packages | | horizontal_bilingual | Horizontal Bilingual | Wide packages | | simplified | Simplified Standard | Small packages with limited nutrients | | simplified_bilingual | Simplified Bilingual | Small packages, bilingual | | horizontal_simplified_bilingual | Horizontal Simplified Bilingual | Wide small packages | | linear | Linear | Very small packages, single line | | linear_simplified | Linear Simplified | Tiny packages | | dual | Dual - Foods Requiring Preparation | "As sold" vs "As prepared" (e.g., cake mix) | | dual_bilingual | Dual Bilingual | Preparation dual, bilingual | | dual_amount | Dual - Different Amounts | Multiple serving sizes | | dual_amount_bilingual | Dual Bilingual - Different Amounts | Multiple servings, bilingual | | aggregate | Aggregate - Different Foods | Assorted products (e.g., variety pack) | | aggregate_bilingual | Aggregate Bilingual | Assorted products, bilingual | | aggregate_amount | Aggregate - Different Amounts | Assorted with varying amounts | | aggregate_amount_bilingual | Aggregate Bilingual - Different Amounts | Assorted amounts, bilingual | #### US Label Formats | label_format | Description | Use Case | |--------------|-------------|----------| | standard | Standard Vertical | Most common US format | | tabular | Tabular | Side-by-side layout | | simplified | Simplified | Products with limited nutrients | | linear | Linear | Very small packages | | dual | Dual Column | "As packaged" vs "As prepared" | | aggregate | Aggregate | Assorted/variety products | | tabular_dual | Tabular Dual | Tabular with dual columns | **Get label** ``` GET /v1/labels/{label_id} ``` Response includes a `warnings` array flagging compliance or accuracy issues (e.g., missing nutriments, serving size problems). Always review warnings after creating or updating a label. **Update label** ``` POST /v1/labels/{label_id} ``` Updates provided fields. **Update label (merge)** ``` PATCH /v1/labels/{label_id} ``` Updates provided fields. Use when you want to preserve unspecified sub-entity values. **Delete label** ``` DELETE /v1/labels/{label_id} ``` **Render label** ``` GET /v1/labels/{label_id}/render.pdf GET /v1/labels/{label_id}/render.png GET /v1/labels/{label_id}/render.jpg GET /v1/labels/{label_id}/render.bmp GET /v1/labels/{label_id}/render.eps ``` **For print: prefer PDF** (scales losslessly). Raster images (PNG, JPG, BMP) are rendered at 300 PPI — they work for print but must be imported into design software with their DPI honored, otherwise they'll display at incorrect physical dimensions. EPS is also available for design workflows that require it. Returns binary file. For HTML content in JSON: ``` GET /v1/labels/{label_id}/render.html?json=true ``` **Measure label dimensions** ``` GET /v1/labels/{label_id}/measure ``` Returns dimensions in centimeters: - width, height, area: numeric values - display_width, display_height, display_area: formatted strings (e.g., "5.08 cm") --- ### FOP Labels (Front-of-Package) Canadian Front-of-Package nutrition symbols showing high sodium, sugars, or saturated fat warnings. **Mandatory since 2026-01-01** for CAN2016 products that exceed Health Canada thresholds. **Always create a FOP label for CAN2016 products to check.** If the product is below all thresholds, `GET /v1/fop_labels/{fop_label_id}/render.png` (or any render.*) returns HTTP 400 with error code `1457` / type `NoFopLabelRequired` — that's the "no symbol needed" signal, not a failure. If it succeeds, the product requires the symbol on pack. **List FOP labels** ``` GET /v1/fop_labels?recipe_id={recipe_id} ``` **Create FOP label** ``` POST /v1/fop_labels Content-Type: application/json { "recipe_id": "abc123", "concrete_type": "CAN2016", "serving_qty": "30", "serving_unit": "g", "reference_qty": "100", "reference_unit": "g" } ``` **Get FOP label** ``` GET /v1/fop_labels/{fop_label_id} ``` **Delete FOP label** ``` DELETE /v1/fop_labels/{fop_label_id} ``` **Render FOP label** ``` GET /v1/fop_labels/{fop_label_id}/render.pdf GET /v1/fop_labels/{fop_label_id}/render.png GET /v1/fop_labels/{fop_label_id}/render.jpg GET /v1/fop_labels/{fop_label_id}/render.bmp GET /v1/fop_labels/{fop_label_id}/render.eps ``` **For print: prefer PDF** (scales losslessly). Raster images (PNG, JPG, BMP) are rendered at 300 PPI — they work for print but must be imported into design software with their DPI honored, otherwise they'll display at incorrect physical dimensions. EPS is also available for design workflows that require it. --- ### Tags Tags organize foods and recipes with custom metadata. Useful for system-to-system workflows like marking imported items, flagging incomplete data, or categorization. Food tags and recipe tags are separate systems. **Use tags instead of destructive or silent actions.** When unsure whether a change is safe, tag the resource and surface the tag to the user rather than committing silently. Common LLM patterns: - **Human review needed** — tag a food/recipe you created or modified with incomplete or low-confidence data (e.g., OCR'd values you couldn't fully verify, ambiguous allergen statements, guessed densities). Use a tag like `needs-review` so the user can filter and audit later. - **Metadata** — tag with source/context the user will want to retrieve later (e.g., `imported-2026-04`, `supplier-acme`, `batch-12`). Don't bury this in `notes` fields where it can't be filtered on. - **Soft-delete / mark for deletion** — never call `DELETE` when you're uncertain. Tag with `pending-deletion` (or similar) and tell the user what you flagged and why, so they can confirm before the destructive action. Same pattern for merges/consolidations you're not fully confident about. Create the tag if it doesn't exist, then associate it. **List food tags** ``` GET /v1/tags/food_tags ``` **Create food tag** ``` POST /v1/tags/food_tags Content-Type: application/json { "name": "imported", "color": "#3B82F6" } ``` **Edit food tag** ``` POST /v1/tags/food_tags/{food_tag_id} ``` **Delete food tag** ``` DELETE /v1/tags/food_tags/{food_tag_id} ``` **List recipe tags** ``` GET /v1/tags/recipe_tags ``` **Create recipe tag** ``` POST /v1/tags/recipe_tags Content-Type: application/json { "name": "missing-values", "color": "#EF4444" } ``` **Edit/Delete recipe tags**: Same pattern as food tags at `/v1/tags/recipe_tags/{recipe_tag_id}` #### Tag Associations Associate tags with foods: **List tags on a food** ``` GET /v1/tags/food_associations/{food_id}/tags ``` **Add tag to food** ``` POST /v1/tags/food_associations/{food_id}/tags Content-Type: application/json { "food_tag_id": "tag123" } ``` **Set all tags on a food (replace)** ``` POST /v1/tags/food_associations/{food_id} Content-Type: application/json { "food_tag_ids": ["tag123", "tag456"] } ``` **Remove tag from food** ``` DELETE /v1/tags/food_associations/{food_id}/tags/{food_tag_id} ``` #### Filtering by Tags Include `tags` parameter in list queries: ``` GET /v1/foods?tags=tag123,tag456 GET /v1/recipes?tags=tag789 ``` Include tag data in responses with expand: ``` GET /v1/foods?expand=tags GET /v1/recipes?expand=tags ``` #### Batch Tag Operations **Merge tags** (combine multiple into one): ``` POST /v1/tags/food_tags/batch_merge?ids=tag1,tag2,tag3 POST /v1/tags/recipe_tags/batch_merge?ids=tag1,tag2 ``` **Batch delete tags**: ``` POST /v1/tags/food_tags/batch_remove?ids=tag1,tag2 POST /v1/tags/recipe_tags/batch_remove?ids=tag1,tag2 ``` --- ## Common Workflows ### 1. Analyze Recipe Nutrition ``` GET /v1/recipes/{recipe_id}?expand=nutriments,nutriments_100g ``` Response includes: - `nutriments`: values for the complete recipe - `nutriments_100g`: values per 100g (for comparison) - `ingredients`: list with individual nutriment breakdowns ### 2. Generate Compliance Label Step 1: Verify recipe exists ``` GET /v1/recipes/{recipe_id} ``` Step 2: Create label ``` POST /v1/labels { "recipe_id": "{recipe_id}", "concrete_type": "CAN2016", "serving_qty": "30", "serving_unit": "g", "reference_qty": "55", "reference_unit": "g", "household_serving_en": "1 serving" } ``` Step 3: Check recipe and label warnings, and sanity-check the computed output ``` GET /v1/recipes/{recipe_id}?expand=previews_CAN2016,previews_US GET /v1/labels/{label_id} ``` Review the `warnings` array in both responses. Warnings flag issues like missing nutriments, ingredient list problems, or values that may need attention. Address any warnings before finalizing. Then read the computed output back to the user in plain language and flag anything suspicious. **Use `previews_*` for QA**, not the raw `ingredients[]` array — class-name resolution, allergen aggregation, "and"/"," joining, and ordering are all applied in the preview, so reading `ingredients[]` will give you the wrong picture of what shows up on the label. - `previews_CAN2016.ingredient_list_en` / `ingredient_list_fr` — rendered ingredient list as it will appear on the label (use `previews_US` for US regulations) - `previews_CAN2016.allergies_list_en` / `allergies_list_fr` — formatted "Contains:" statement - `previews_CAN2016.may_contains_list_en` / `may_contains_list_fr` — formatted "May contain:" statement - Top-level `allergies` and `may_contains` — combined allergen arrays aggregated from ingredients + extras Flag missing allergens, "may contain" statements that should be there given the ingredients, or ingredient list text that doesn't match what the user described. These outputs are computed mechanically from the underlying foods; a review catches cases where the source food entries were incomplete. Step 4: Download rendered label ``` GET /v1/labels/{label_id}/render.pdf ``` Step 5: Share the app links with the user so they can view/edit in the browser: - Recipe: `https://app.labelify.ca/recipes/{recipe_id}` - Label: `https://app.labelify.ca/labels/{label_id}` ### 3. Build Recipe from Scratch Step 1: Search existing ingredients (use `all=true` for CNF/USDA). ``` GET /v1/foods?search=flour&all=true&short=true ``` Step 2: Create the recipe with the found food IDs. ``` POST /v1/recipes { "name": "Shortbread Cookies", "ingredients": [ {"food_id": "{flour_id}", "quantity": "200", "unit": "g"}, {"food_id": "{butter_id}", "quantity": "150", "unit": "g"}, {"food_id": "{sugar_id}", "quantity": "75", "unit": "g"} ] } ``` Step 3: `GET /v1/recipes/{recipe_id}` and review the `warnings` array before creating labels. Step 4: Share `https://app.labelify.ca/recipes/{recipe_id}`. ### 4. Collect Supplier Data for Custom Ingredients When an ingredient isn't in the database, collect raw data from the supplier rather than guessing — inaccurate values on a finished label have legal and health consequences for the user's customers. Step 1: Ask for the supplier's spec sheet, CoA, or product label. If vision-capable, offer to extract from a photo/scan and **confirm the extracted values back to the user before submitting** (OCR misreads decimals, units, and allergen lists). Required from the document: - Nutritionals per reference amount (typically 100g or one serving) — at minimum: calories, fat, saturated fat, trans fat, cholesterol, sodium, carbohydrate, fibre, sugars, protein (see Nutriments Reference for more) - Ingredient list, verbatim, preserving order and sub-ingredient parentheses - Allergens and any "may contain" / cross-contamination statements - Product code / SKU (optional, useful for later lookup via `/v1/foods/by_product_code/{product_code}`) Step 2: Create the food ``` POST /v1/foods { "name": "...", "mass_quantity": "100", "mass_unit": "g", "nutriments": {...}, "allergies": [...] } ``` Step 3: Attach the source document for traceability (not available on trial accounts) ``` POST /v1/foods/{food_id}/technical_file (multipart, field: upload_file) ``` Step 4: Share the ingredient link: `https://app.labelify.ca/ingredients/{food_id}` ### 5. Update Food, Propagate to Recipes ``` PATCH /v1/foods/{food_id} { "nutriments": { "sodium": {"amount": "150"} } } ``` All recipes using this food automatically recalculate their nutritional values. --- ## Pagination All list endpoints support pagination: ``` GET /v1/recipes?page=1&page_size=50 ``` Parameters: - page: integer starting at 1 - page_size: 1-200 (default 20) Response includes page_info: ```json { "items": [...], "page_info": { "page": 1, "page_size": 20, "total": 150, "has_next": true, "has_previous": false } } ``` --- ## Utility Endpoints **List allergens** ``` GET /v1/utils/allergies ``` Returns standard allergen names (milk, eggs, fish, etc.) **List available units** ``` GET /v1/utils/available_units ``` Returns valid measurement units (g, kg, ml, L, cup, tbsp, etc.) **List yield units** ``` GET /v1/utils/yield_units ``` Returns valid recipe yield units (%, servings, pieces, etc.) **List ingredient-list class names** ``` GET /v1/utils/class_names ``` Authoritative list of valid `ingredient_list_class_name` IDs + bilingual display labels. The `end` flag is CAN2016-specific and not relevant for US — FDA requires strict descending weight order, so `ingredient_list_at_end` has no effect on US labels. Classes only affect naming there (e.g., "natural flavor"). **List concrete types** ``` GET /v1/utils/concrete_types ``` Returns valid label formats (CAN2016, US) **List nutriment rules** ``` GET /v1/utils/nutriments_rules ``` Returns metadata for all nutrients (names, units, daily values) --- ## Nutriments Reference When creating or updating foods, each nutriment requires `amount` and `unit` fields. ### Core Nutriments | Key | Unit | Name | |-----|------|------| | calories | kcal | Calories | | fat | g | Total Fat | | fat_saturated | g | Saturated Fat | | fat_trans | g | Trans Fat | | cholesterol | mg | Cholesterol | | sodium | mg | Sodium | | carbohydrate | g | Carbohydrate | | fibre | g | Fibre | | sugars | g | Sugars | | protein | g | Protein | | potassium | mg | Potassium | | calcium | mg | Calcium | | iron | mg | Iron | | added_sugars | g | Added Sugars (US only) | `added_sugars` is **required on US labels** and is **not** derived from `sugars` — it's the sugars added during processing (cane sugar, honey, syrups, etc.), excluding naturally occurring sugars (fruit, dairy). When creating foods destined for US labels, ask the user for added sugars explicitly. ### Common Additional Nutriments | Key | Unit | Name | |-----|------|------| | vitamin_a_RAE | ug | Vitamin A (RAE) | | vitamin_c | mg | Vitamin C | | vitamin_d | ug | Vitamin D | | vitamin_e | mg | Vitamin E | | vitamin_k | ug | Vitamin K | | thiamin | mg | Thiamine | | riboflavin | mg | Riboflavin | | niacin_NE | mg | Niacin (NE) | | vitamin_b6 | mg | Vitamin B6 | | folate_DFE | ug | Folate (DFE) | | vitamin_b12 | ug | Vitamin B12 | | biotin | ug | Biotin | | pantothenic_acid | mg | Pantothenic Acid | | phosphorus | mg | Phosphorus | | magnesium | mg | Magnesium | | zinc | mg | Zinc | | selenium | ug | Selenium | | copper | mg | Copper | | manganese | mg | Manganese | Full list available via `GET /v1/utils/nutriments_rules`. --- ## Warnings Reference Recipe and label responses include a `warnings` array. Each entry has a `code`, `severity`, and `message_en` / `message_fr`. CRITICAL usually requires action; INFO is advisory. Surface the message to the user and suggest a fix. | Code | Severity | Meaning | |------|----------|---------| | WN-2 | CRITICAL | A nutriment's mass exceeds the ingredient's total mass (data error) | | WN-5 | CRITICAL | No nutritional values on a food | | WN-11 | CRITICAL | `nutriments_overwrite` set without `nutriments_overwrite_qty` — values ignored | | WN-15 | — | Seasoning ingredients exceed 2% of recipe weight — may need individual declaration for compliance | | WN-22 | INFO | Some ingredients lack data for listed nutrients — values shown are minimums; request the missing data from the supplier | | WN-23 / WN-24 | WARNING | Sub-recipe ingredient references a deleted recipe (frozen/unmergeable) | | WN-25 | WARNING | Ingredient is missing a quantity | Other codes (WN-1, WN-3, WN-4, WN-6 through WN-10, WN-12 through WN-14, WN-16 through WN-18, WN-21) cover yield ranges, density, volume-unit-without-density, parsing anomalies, and formatting notes — read the `message_en` field when they fire. --- ## Resources - CFIA Industry Labelling Tool (Canada, CAN2016): https://inspection.canada.ca/en/food-labels/labelling/industry - FDA Food Labeling Guide (US): https://www.fda.gov/regulatory-information/search-fda-guidance-documents/guidance-industry-food-labeling-guide If the user is stuck or needs help, they can email **support@labelify.ca** — we answer within 24 hours. --- ## Suggested User Prompts Short task-oriented prompts AIs can surface as quick-actions or conversation starters: - "Create a Canadian Nutrition Facts label for my recipe" - "Check if my product needs a Front-of-Package symbol" - "Extract nutrition data from this supplier spec sheet" - "Build a recipe from this ingredient list" - "Review my ingredient list for CFIA compliance" - "Find [ingredient] in the database" - "Tag recipes missing allergen data for review" - "Generate both Canadian and US labels for my product" - "Explain the nutrition of my recipe" - "Update the sodium value on this ingredient" - "Find a substitute for [ingredient]" - "Bulk-create foods from this spreadsheet"