Ice Cream Calculator REST API Reference
Introduction
This document provides complete technical reference for all Ice Cream Calculator REST API endpoints. Each endpoint is documented with request format, response structure, and example code.
📘 New to the API? Start with the Getting Started Guide to learn how to generate your API key and make your first request.
Base URL
https://icecreamcalc.app/api
All endpoints are relative to this base URL.
Authentication
All endpoints (except /recipes/test) require authentication using an API key.
Include API Key in Header
X-API-Key: icc_your_api_key_here
Authentication Errors
| Error | HTTP Code | Response |
|---|---|---|
| Missing API Key | 401 |
{"error": "API key is required", "details": "Include X-API-Key header with your API key"} |
| Invalid API Key | 401 |
{"error": "Invalid API key", "details": "The provided API key is not valid or the user account is inactive"} |
Endpoints Overview
| Endpoint | Method | Description |
|---|---|---|
/recipes |
GET | List all your recipes (compact summaries) |
/recipes/{id} |
GET | Get a single recipe with ingredients |
/recipes/{id}/calculated |
GET | Get calculated data for a recipe |
/recipes |
POST | Create a new recipe |
/recipes/{id} |
PUT | Update a recipe (full replacement) |
/recipes/{id} |
DELETE | Delete a recipe permanently |
/recipes/{id}/balance |
POST | Run Smart Balance and get a proposal (nothing is saved) |
/ingredients |
GET | List all accessible ingredients |
/ingredients/{id} |
GET | Get a single ingredient with full details |
Recipe Endpoints
GET /api/recipes
Retrieve a list of all your recipes with basic information. This endpoint returns compact summaries – use GET /api/recipes/{id} for the full recipe.
Request
GET /api/recipes
Headers:
X-API-Key: icc_your_api_key_here
Response (200 OK)
{
"success": true,
"count": 2,
"recipes": [
{
"id": 1,
"name": "Classic Vanilla Ice Cream",
"description": "Traditional vanilla base",
"info": "Traditional vanilla base",
"externalId": "ERP-REC-0042",
"totalWeight": 1000.0,
"createdDate": "2025-01-15T10:30:00Z",
"ingredientCount": 6
}
]
}
Recipe Summary Fields
| Field | Type | Description |
|---|---|---|
id |
integer | Unique recipe identifier |
name |
string | Recipe name |
description |
string | Recipe description – legacy alias of info, kept for compatibility |
info |
string | Recipe description (same value as description; matches the field name on the single-recipe endpoint) |
externalId |
string or null | Your external-system ID for this recipe – lets you match recipes from one list call |
totalWeight |
number | Total mix weight in grams |
createdDate |
string (ISO 8601) | When the recipe was created |
ingredientCount |
integer | Number of ingredients in the recipe |
Note: The list endpoint does not include ingredients, notes, category, tags, or metadata – those are on GET /api/recipes/{id}.
Example (curl)
curl -H "X-API-Key: icc_your_key" \
https://icecreamcalc.app/api/recipes
Example (Python)
import requests
response = requests.get(
"https://icecreamcalc.app/api/recipes",
headers={"X-API-Key": "icc_your_key"}
)
recipes = response.json()
for recipe in recipes['recipes']:
print(f"{recipe['name']} - {recipe['totalWeight']}g")
GET /api/recipes/{id}
Retrieve detailed information for a single recipe, including all ingredients.
Request
GET /api/recipes/1
Headers:
X-API-Key: icc_your_api_key_here
Response (200 OK)
{
"success": true,
"recipe": {
"id": 1,
"name": "Classic Vanilla Ice Cream",
"info": "Traditional vanilla base",
"notes": "Heat base to 85C, cool overnight before churning.",
"externalId": "ERP-REC-0042",
"metadata": { "externalId": "repo-123", "version": 3 },
"category": "IceCream",
"tags": ["Home", "LowSugar"],
"bestBeforeDays": 180,
"overrun": -1,
"evaporation": 0.05,
"createdDate": "2025-01-15T10:30:00Z",
"changedDate": "2025-11-08T14:20:00Z",
"hasChart": true,
"chartName": "Ice Cream Base",
"mixWeight": 1000.0,
"finalWeight": 950.0,
"ingredients": [
{
"id": 1,
"ingredientId": 42,
"ingredientName": "Heavy Cream",
"ingredientSource": "Default",
"isOverride": false,
"weight": 500.0,
"inclusion": false,
"infusion": false,
"excludeFromEvaporation": false,
"position": 0,
"amountMin": 0.0,
"amountMax": 1.0
}
]
}
}
Recipe Object Fields
| Field | Type | Description |
|---|---|---|
id |
integer | Unique recipe identifier |
name |
string | Recipe name |
info |
string | Recipe description |
notes |
string | Free-text notes (up to 2000 characters) |
externalId |
string or null | Your external-system ID (up to 50 characters); null when unset |
metadata |
object or null | Your own JSON object, stored and returned verbatim (see “The metadata field” below); null when unset |
category |
string or null | Recipe category (see accepted values below); null when uncategorized |
tags |
array of strings | Recipe tags (see accepted values below); empty when none |
bestBeforeDays |
integer or null | Shelf life in days; null when unspecified |
overrun |
number | Per-recipe overrun percentage. -1 means “inherit the account’s global overrun default” |
evaporation |
number | Evaporation percentage (0.05 = 5%) |
hasChart / chartName |
boolean / string | Whether a chart is linked, and its name |
mixWeight |
number | Total mix weight in grams (before freezing) |
finalWeight |
number | Final weight in grams (after evaporation) |
ingredients |
array | List of ingredients with weights, positions, and amount bounds |
Error Responses
| HTTP Code | Response | Meaning |
|---|---|---|
404 |
{"error": "Recipe not found"} |
Recipe with this ID doesn’t exist (temporary recipes also return 404) |
403 |
{"error": "Access denied", "details": "You don't have permission to access this recipe"} |
Recipe belongs to another user |
Example (curl)
curl -H "X-API-Key: icc_your_key" \
https://icecreamcalc.app/api/recipes/1
GET /api/recipes/{id}/calculated
Retrieve calculated data for a recipe including PAC, POD, serving temperatures, and all other ice cream science properties.
Note: This endpoint performs full recipe calculations, so it may take slightly longer than other endpoints.
Request
GET /api/recipes/1/calculated
Headers:
X-API-Key: icc_your_api_key_here
Response (200 OK)
{
"success": true,
"data": {
"recipeId": 1,
"recipeName": "Classic Vanilla Ice Cream",
"values": {
"PAC": {
"name": "PAC",
"displayValue": "285.43",
"numericValue": 285.43,
"info": "Freezing point depression",
"isTemperature": false
},
"POD": {
"name": "POD",
"displayValue": "32.15",
"numericValue": 32.15,
"info": "Relative sweetness",
"isTemperature": false
},
"Serving temp ice cream": {
"name": "Serving temp ice cream",
"displayValue": "-14.2°C",
"numericValue": -14.2,
"info": "Recommended serving temperature for ice cream",
"isTemperature": true
}
// ... many more calculated values
}
}
}
Calculated Values Included
- PAC – Freezing point depression
- POD – Relative sweetness
- Serving temperatures – For ice cream and gelato
- Nutritional data – Fat, protein, sugars, etc.
- Milk solids – MSNF, milk fat, milk protein
- Percentages – Water, solids, overrun, etc.
- Weights – Mix weight, final weight, additions
- And more – All calculated recipe properties
Example (Python)
import requests
response = requests.get(
"https://icecreamcalc.app/api/recipes/1/calculated",
headers={"X-API-Key": "icc_your_key"}
)
data = response.json()['data']
pac = data['values']['PAC']['numericValue']
pod = data['values']['POD']['numericValue']
print(f"PAC: {pac}, POD: {pod}")
POST /api/recipes
Create a new recipe with ingredients.
Request
POST /api/recipes
Headers:
X-API-Key: icc_your_api_key_here
Content-Type: application/json
{
"name": "Mango Sorbet",
"info": "Fruity sorbet base",
"notes": "Blend fruit before adding syrup.",
"externalId": "ERP-REC-0043",
"category": "Sorbet",
"tags": ["Vegan", "DairyFree"],
"bestBeforeDays": 180,
"overrun": -1,
"metadata": { "externalId": "repo-123", "version": 1 },
"evaporation": 0,
"chartName": "Sorbet Base",
"ingredients": [
{
"ingredientId": 42,
"weight": 500,
"inclusion": false,
"infusion": false,
"amountMin": 0,
"amountMax": 0
}
]
}
Request Fields
| Field | Type | Required | Constraints and omitted behaviour |
|---|---|---|---|
name |
string | Yes | Must be non-empty; max 200 characters (longer → 400) |
info |
string | No | Description; empty when omitted |
notes |
string | No | Max 2000 characters (longer → 400); empty when omitted |
externalId |
string | No | Max 50 characters (longer → 400); empty when omitted |
category |
string | No | Must match an accepted category value, case-insensitively (unknown → 400); uncategorized when omitted |
tags |
array of strings | No | Each tag must match an accepted tag value, case-insensitively (unknown → 400); duplicates are collapsed; no tags when omitted |
bestBeforeDays |
integer | No | Must not be negative (→ 400); unspecified when omitted |
overrun |
number | No | -1 (inherit the global default) or any value ≥ 0; other negatives → 400. Omitted = -1 |
metadata |
object | No | Top level must be a JSON object; compact serialization max 4096 characters (violations → 400); unset when omitted. See “The metadata field” below |
evaporation |
number | No | Default 0 |
chartName |
string | No | Looked up by name, case-insensitively. If not found, the recipe is still created without a chart |
ingredients |
array | Yes | At least one. Each: ingredientId (required – must exist and be accessible to you), weight (grams), inclusion, infusion, excludeFromEvaporation, amountMin, amountMax (all booleans/numbers optional, defaulting to false/0) |
Accepted category values
IceCream, Gelato, Sorbet, Sherbet, FrozenYogurt, Granita, SoftServe, Special, Confectionery, Other, Ingredient, Topping
Accepted tags values
Vegan, EggFree, DairyFree, LowSugar, Home, BatchFreezer, NinjaCreami, PacoJet, Alcohol, Ganache, Commercial
💡 Tip: Matching is case-insensitive on write ("vegan" is accepted), but responses always return the canonical names listed above – so what you read back can be sent again unchanged.
Response (201 Created)
{
"success": true,
"recipeId": 15,
"message": "Recipe created successfully",
"recipe": { /* the full recipe object, as returned by GET /api/recipes/{id} */ }
}
Error Responses
| HTTP Code | Example | Meaning |
|---|---|---|
400 |
{"error": "Validation failed", "details": "Recipe name is required"} |
Missing name, no ingredients, bad field value (unknown category/tag, negative bestBeforeDays, invalid overrun, metadata not an object or over the cap) |
400 |
{"error": "Invalid ingredient", "details": "Ingredient with ID 999 does not exist or you don't have access to it"} |
An ingredient ID doesn’t exist or isn’t accessible to your account |
403 |
{"error": "Recipe limit reached", "details": "..."} |
Your plan’s recipe limit is reached |
The metadata field
metadata is an opaque JSON key-value store for your own use – for example syncing your system’s IDs, versions, or timestamps. Ice Cream Calculator stores and returns it but never reads, interprets, or indexes its contents.
- The top level must be a JSON object (
{...}). Arrays, strings, numbers, and other bare values are rejected with400. Nested values may be anything. - The compact (no-whitespace) serialization must be at most 4096 characters; larger →
400. - Returned as a nested JSON object on
GET /api/recipes/{id}, the create response, and the update response – never as a quoted string. Not included in the list endpoint. - Full replacement on update: omitting
metadataon aPUTclears it.
Example (curl)
curl -X POST \
-H "X-API-Key: icc_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"Mango Sorbet","category":"Sorbet","tags":["Vegan"],"ingredients":[{"ingredientId":42,"weight":500}]}' \
https://icecreamcalc.app/api/recipes
PUT /api/recipes/{id}
Update an existing recipe. This is a full replacement: the ingredient list is completely replaced, and every optional field resets to its default when omitted – the API does not merge with the previous values.
Request
Same body format and validation as POST /api/recipes (see the request-field table above). Key differences:
| Field | When omitted on PUT |
|---|---|
info, notes, externalId |
Cleared (set to empty) |
category, tags, bestBeforeDays, metadata |
Cleared (unset / no tags / null) |
overrun |
Reset to -1 (inherit the global default) |
chartName |
Chart link cleared (also cleared if the name isn’t found) |
ingredients |
Not allowed to omit – at least one is required; the previous list is always fully replaced |
⚠️ Warning: Because updates are full replacements, always send every field you want to keep. A safe pattern is: GET /api/recipes/{id}, modify the fields you need, then PUT the whole thing back.
Response (200 OK)
{
"success": true,
"recipe": { /* the full updated recipe object */ }
}
Error Responses
Same validation errors as create (400), plus: 404 when the recipe doesn’t exist, 403 when it belongs to another user, and 400 when attempting to update a temporary recipe.
DELETE /api/recipes/{id}
Permanently delete a recipe and all its ingredients. This cannot be undone.
Request
DELETE /api/recipes/1
Headers:
X-API-Key: icc_your_api_key_here
Response (200 OK)
{
"success": true,
"message": "Recipe deleted successfully",
"recipeId": 1
}
Error Responses
| HTTP Code | Meaning |
|---|---|
404 |
Recipe doesn’t exist |
403 |
Recipe belongs to another user |
400 |
Temporary recipes cannot be deleted via the API |
Example (curl)
curl -X DELETE \
-H "X-API-Key: icc_your_key" \
https://icecreamcalc.app/api/recipes/1
POST /api/recipes/{id}/balance
Run Smart Balance on a recipe against its linked chart and get back a full diagnostic proposal.
💡 Non-mutating: This endpoint never saves anything. It balances a copy of the stored recipe and returns the proposed weights with diagnostics. If you want to keep the proposal, send a PUT /api/recipes/{id} with the new ingredient weights.
Request
No request body. The balance run is reconstructed entirely from stored recipe state:
- Targets come from the recipe’s linked chart (the chart items marked for balancing).
- Locked ingredients (set in the recipe editor) keep their weights.
- Classification chips saved on the recipe’s ingredients (Flavor, Stabilizer, Emulsifier, etc.) drive the per-ingredient bounds; ingredients without a saved chip use their own min/max amount settings.
POST /api/recipes/1/balance
Headers:
X-API-Key: icc_your_api_key_here
Response (200 OK)
{
"success": true,
"result": {
"status": "Converged",
"message": null,
"mixWeightBefore": 1000.0,
"mixWeightAfter": 1000.0,
"wasReverted": false,
"bestIteration": 4,
"initialOutOfRangeCount": 3,
"proposedOutOfRangeCount": 0,
"elapsedMs": 412,
"iterationCount": 4,
"targets": [
{
"propertyKey": "Total fat",
"target": 10.0,
"initialValue": 8.2,
"finalValue": 10.0,
"isLinear": true,
"priority": 1.0,
"achievableMin": 6.5,
"achievableMax": 14.8,
"isUnmovable": false,
"unmovableReason": null,
"rangeAchievable": true,
"bindingIngredients": []
}
],
"iterations": [
{ "iteration": 1, "objective": 2.31, "maxWeightChange": 42.5, "elapsedMs": 118 }
],
"ingredients": [
{
"ingredientId": 42,
"name": "Heavy Cream",
"initialWeight": 250.0,
"finalWeight": 291.3,
"class": "Unknown",
"lowerBoundGrams": 0.0,
"upperBoundGrams": 1000.0,
"locked": false,
"excluded": false
}
],
"classification": {
"success": true,
"errorMessage": null,
"classifications": [
{ "name": "Vanilla Extract", "class": "Flavor", "recMinPct": null, "recMaxPct": null, "reason": null }
]
}
}
}
Result Fields
| Field | Type | Description |
|---|---|---|
status |
string | One of Converged, MaxIterationsReached, Infeasible, SolverUnavailable, Failed |
message |
string or null | Human-readable explanation (e.g. why the run stopped early or reverted) |
mixWeightBefore / mixWeightAfter |
number | Total mix weight before and after (mass is held fixed, so these should match) |
wasReverted |
boolean | True when the safety net kept the original weights because no proposal beat the input recipe |
bestIteration |
integer | Which state the returned weights come from: 0 = the input recipe, N = after N accepted solver steps |
initialOutOfRangeCount / proposedOutOfRangeCount |
integer | How many targets were out of range before, and in the proposal |
elapsedMs |
integer | Total run time in milliseconds |
iterationCount |
integer | Number of solver iterations (the iterations array has one entry per iteration) |
Per-Target Fields (targets)
| Field | Type | Description |
|---|---|---|
propertyKey |
string | The chart property (e.g. “Total fat”) |
target / initialValue / finalValue |
number | What was asked for, where the property started, and where the proposal lands |
isLinear |
boolean | Whether the property responds linearly to weight changes |
priority |
number | Weighting of this target in the optimization |
achievableMin / achievableMax |
number or null | The locally achievable interval under the ingredient bounds; null when not computed. Treat “out of reach” as a strong warning rather than a proof |
isUnmovable / unmovableReason |
boolean / string or null | True when no redistribution of the adjustable ingredients can move this property at all, with the reason |
rangeAchievable |
boolean or null | false when the requested range lies outside the achievable interval; null when not computed |
bindingIngredients |
array of strings | When the range is unachievable: the ingredient bounds standing in the way (up to three, ranked by influence) |
Per-Ingredient Fields (ingredients)
| Field | Type | Description |
|---|---|---|
ingredientId / name |
integer / string | Which ingredient |
initialWeight / finalWeight |
number | Weight in grams before, and in the proposal – use these for the follow-up PUT |
class |
string | The functional class used for bound assignment (Flavor, Stabilizer, Emulsifier, Fiber, HighIntensitySweetener, Structural, Other, or Unknown when unclassified) |
lowerBoundGrams / upperBoundGrams |
number | The weight bounds the solver used for this ingredient |
locked |
boolean | Weight was held fixed |
excluded |
boolean | Excluded from balancing (inclusions and infusions) |
The iterations array traces the solver (iteration, objective, maxWeightChange, elapsedMs per step), and classification echoes the classification set the run used (success, errorMessage, and per-ingredient name/class/recMinPct/recMaxPct/reason entries).
Error Responses
| HTTP Code | Example | Meaning |
|---|---|---|
400 |
{"error": "No chart linked", "details": "This recipe has no linked chart. Link a chart (its ranges define the balance targets) and try again."} |
The recipe has no linked chart (or the linked chart couldn’t be loaded) |
404 / 403 |
As for GET /api/recipes/{id} |
|
429 |
{"success": false, "error": "Rate limit exceeded", "details": "..."} |
The balance endpoint’s tighter rate limit was exceeded (see Rate Limiting) |
Note: If the chart has no properties marked for balancing, or every ingredient is locked or an add-in, the endpoint still returns 200 – with status: "Converged", a message explaining the early exit (e.g. “Chart has no balancing targets”), unchanged weights, and empty targets/iterations/ingredients arrays. Check the message field, not just the status.
Typical agent workflow
POST /api/recipes/1/balance– get the proposal- Inspect
status,wasReverted, the per-target achievability, and the per-ingredientfinalWeightvalues - If you like the proposal:
GET /api/recipes/1, set each ingredient’sweightto the proposal’sfinalWeight, andPUTthe recipe back
Example (curl)
curl -X POST \
-H "X-API-Key: icc_your_key" \
https://icecreamcalc.app/api/recipes/1/balance
Ingredient Endpoints
GET /api/ingredients
Retrieve a list of all ingredients you have access to (personal ingredients + shared/default ingredients).
Note: This endpoint returns basic information only. For full nutritional data, use /api/ingredients/{id}
Request
GET /api/ingredients
Headers:
X-API-Key: icc_your_api_key_here
Response (200 OK)
{
"success": true,
"count": 153,
"ingredients": [
{
"id": 1,
"name": "Whole Milk",
"category": "Dairy",
"source": "Default",
"isOverride": false,
"isRecipeDerived": false,
"created": "2024-01-01T00:00:00Z",
"changed": "2024-01-01T00:00:00Z"
},
{
"id": 42,
"name": "My Custom Sugar",
"category": "Sugar",
"source": "Personal",
"isOverride": false,
"isRecipeDerived": false,
"created": "2025-11-01T10:30:00Z",
"changed": "2025-11-08T14:20:00Z"
}
]
}
Ingredient Summary Fields
| Field | Type | Description |
|---|---|---|
id |
integer | Unique ingredient identifier |
name |
string | Ingredient name |
category |
string | Category (Dairy, Sugar, Fat, etc.) |
source |
string | “Default”, “Personal”, or “Modified Default” |
isOverride |
boolean | True if this is a modified default ingredient |
isRecipeDerived |
boolean | True if created from a recipe |
Example (curl)
curl -H "X-API-Key: icc_your_key" \
https://icecreamcalc.app/api/ingredients
GET /api/ingredients/{id}
Retrieve complete details for a single ingredient including all nutritional data and ice cream properties.
Request
GET /api/ingredients/1
Headers:
X-API-Key: icc_your_api_key_here
Response (200 OK)
{
"success": true,
"ingredient": {
"id": 1,
"name": "Whole Milk",
"category": "Dairy",
"info": "Fresh whole milk",
"source": "Default",
"isOverride": false,
// Nutritional data (per 100g)
"water": 87.5,
"totalFat": 3.5,
"saturatedFat": 2.1,
"protein": 3.3,
"carbohydrates": 4.8,
"totalSugars": 4.8,
"lactose": 4.8,
// Ice cream specific
"msnf": 8.9,
"milkSolids": 12.4,
"milkProtein": 3.3,
"milkFat": 3.5,
"pac": 5.2,
"pod": 0.15,
// Allergens
"contains": ["Milk"],
"mayContain": [],
"created": "2024-01-01T00:00:00Z",
"changed": "2024-01-01T00:00:00Z"
}
}
Complete Ingredient Fields
Basic Information:
id,name,category,info,source
Nutritional Data (per 100g):
water,totalFat,saturatedFat,transFat,cholesterolsodium,salt,carbohydrates,fiber,totalSugarsprotein,alcohol,lactose,polyolsvitaminD,calcium,iron,potassium
Ice Cream Properties:
msnf– Milk solids non-fatmilkSolids– Total milk solidsmilkProtein– Milk protein contentmilkFat,cocoaFat,cocoaSolidspac– Freezing point depressionpod– Relative sweetnesshf– Hardening factorstabilizers,emulsifiers
Allergen Information:
contains– Array of allergens this ingredient containsmayContain– Array of potential cross-contamination allergens
Example (JavaScript)
fetch("https://icecreamcalc.app/api/ingredients/1", {
headers: {"X-API-Key": "icc_your_key"}
})
.then(r => r.json())
.then(data => {
const ing = data.ingredient;
console.log(`${ing.name}: ${ing.totalFat}g fat, ${ing.protein}g protein`);
console.log(`PAC: ${ing.pac}, POD: ${ing.pod}`);
});
Response Format
Success Response
All successful responses include a success field set to true:
{
"success": true,
// ... response data
}
Error Response
Error responses include success: false and error details:
{
"success": false,
"error": "Brief error message",
"details": "More detailed explanation"
}
HTTP Status Codes
| Code | Name | Meaning |
|---|---|---|
200 |
OK | Request succeeded |
201 |
Created | Recipe created (POST /api/recipes) |
400 |
Bad Request | Invalid request data – see the error/details fields |
401 |
Unauthorized | Missing or invalid API key |
403 |
Forbidden | You don’t have access to this resource (or plan limit reached) |
404 |
Not Found | Resource doesn’t exist |
429 |
Too Many Requests | Rate limit exceeded – wait for the seconds in the Retry-After header |
500 |
Server Error | Server-side error occurred |
Rate Limiting
The API enforces per-key rate limits to keep the service responsive for everyone. Limits use a fixed one-minute window:
| Scope | Limit | Window |
|---|---|---|
| All API endpoints, per API key | 100 requests | 1 minute |
POST /recipes/{id}/balance, per API key |
20 requests | 1 minute |
| Requests without an API key (shared) | 10 requests | 1 minute |
The balance endpoint is computation-heavy, so it has its own tighter per-key limit (its calls are counted against the balance limit rather than the general one). Requests without an X-API-Key header share a single small bucket across all callers.
When you hit a limit
You’ll receive 429 Too Many Requests with a Retry-After header (seconds to wait) and an error body:
{
"success": false,
"error": "Rate limit exceeded",
"details": "Too many API requests. Retry after 60 seconds (see the Retry-After header)."
}
Playing nicely
- Cache responses when appropriate
- Don’t make unnecessary repeated requests
- On
429, wait forRetry-Afterbefore retrying – don’t hammer - Implement exponential backoff for retries
Note: The limits above are starting defaults and may be tuned. Read them from this page rather than hardcoding assumptions into your integration’s logic – and always honour Retry-After.
Data Access Rules
What You Can Access
- ✅ Your own recipes (all types) – read and write
- ✅ Your personal ingredients
- ✅ Shared/default ingredients (public database)
- ✅ Your modified default ingredients (overrides)
What You Cannot Access
- ❌ Other users’ recipes
- ❌ Other users’ personal ingredients
- ❌ Temporary recipes (not accessible via API – reading, updating, deleting, and balancing them all return errors)
What You Can Do
- ✅ Create recipes (
POST /recipes) – counts against your plan’s recipe limit - ✅ Update recipes (
PUT /recipes/{id}, full replacement) - ✅ Delete recipes (
DELETE /recipes/{id}) - ✅ Run Smart Balance (
POST /recipes/{id}/balance) – returns a proposal, saves nothing - ✅ Store your own JSON in each recipe’s
metadatafield for syncing
Best Practices
Security
- Always use HTTPS (required by the API)
- Store API keys in environment variables, not in code
- Never expose API keys in client-side JavaScript
- Regenerate your key if compromised – remember the API can now modify and delete your recipes
Performance
- Use
/api/recipes/{id}when you need one recipe, not/api/recipes - Use the list endpoint’s
externalIdto match your system’s recipes in one call - Use
/api/ingredientsfor browsing,/api/ingredients/{id}for details - Cache calculated data – it doesn’t change unless the recipe changes
- Handle errors gracefully with retry logic, honouring
Retry-Afteron429
Updating safely
- Because
PUTis a full replacement, alwaysGETfirst, modify, thenPUTthe whole recipe back - Use
metadata(e.g. a version number) to detect concurrent changes from your own tooling - Preview balancing with
POST /recipes/{id}/balancebefore committing weights withPUT
Error Handling
try {
response = requests.get(url, headers=headers)
response.raise_for_status() # Raise exception for 4xx/5xx
data = response.json()
if not data.get('success'):
print(f"API Error: {data.get('error')}")
return None
return data
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
return None
Changelog
Version 1.1 (July 2026)
- Recipe write endpoints: create (POST), update (PUT, full replacement), delete (DELETE)
- New recipe fields:
notes,externalId,category,tags,bestBeforeDays,overrun - Opaque
metadataJSON store per recipe for external-system syncing - List endpoint additions:
infoandexternalId(descriptionkept as a legacy alias) - Smart Balance endpoint:
POST /recipes/{id}/balance(non-mutating proposal with full diagnostics) - Per-key rate limiting (100/min general, 20/min balance) with
429+Retry-After
Version 1.0 (November 2025)
- Initial API release
- Recipe endpoints (list, single, calculated)
- Ingredient endpoints (list, single)
- API key authentication
Need Help?
- 📘 Getting Started Guide – Learn how to generate an API key and make your first request
- 🔬 API Test Page – Test endpoints interactively from your Account page
- 📧 Support – Contact us if you encounter issues
💡 Pro Tip: Use the API Test Page (accessible from your Account page) to explore endpoints and see example responses before writing code!