{"openapi":"3.1.0","info":{"title":"GELATOlab API","version":"1.0.0-beta.1","description":"GELATOlab deterministic ingredient lookup and gelato balance calculation API. LLMs should resolve ingredient names first, then call recipes/balance with the returned ingredient IDs."},"servers":[{"url":"https://gelatolab.com.tw/api/v1","description":"Firebase Hosting"},{"url":"https://us-central1-gelatolab-bf3fd.cloudfunctions.net/apiV1/v1","description":"Direct Cloud Function"}],"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","bearerFormat":"gl_live_*"}},"schemas":{"RecipeItem":{"type":"object","additionalProperties":false,"required":["ingredient_id","grams"],"properties":{"ingredient_id":{"type":"string","description":"ID returned by ingredients/search"},"grams":{"type":"number","exclusiveMinimum":0,"maximum":100000}}},"Error":{"type":"object","required":["error","message","request_id"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"request_id":{"type":"string"},"details":{"type":["object","null"]}}}}},"security":[{"ApiKey":[]}],"paths":{"/ingredients/search":{"get":{"operationId":"searchIngredients","summary":"Resolve an ingredient name to GELATOlab ingredient IDs","description":"Call this before balanceRecipe whenever the user supplied ingredient names instead of IDs. Composition data is intentionally not returned.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":80}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":20,"default":10}}],"responses":{"200":{"description":"Matches"},"401":{"description":"Invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/recipes/balance":{"post":{"operationId":"balanceRecipe","summary":"Calculate deterministic GELATOlab balance metrics","description":"Returns PAC, POD, water, fat, protein, MSNF, total solids, freezing point and related metrics. Never calculate these values in the LLM. Send a stable Idempotency-Key so retries are not charged twice.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":8,"maxLength":128}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["items"],"properties":{"recipe_type":{"type":"string","enum":["milk","cream","sorbet","yogurt","vegan","alcohol","pet","highProtein","sugarFree"]},"items":{"type":"array","minItems":1,"maxItems":60,"items":{"$ref":"#/components/schemas/RecipeItem"}}}}}}},"responses":{"200":{"description":"Balance result; costs 1 API unit"},"402":{"description":"Insufficient quota","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency conflict or request in progress","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"Unknown ingredient ID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/usage":{"get":{"operationId":"getUsage","summary":"Get current included and prepaid API units","responses":{"200":{"description":"Current quota"}}}},"/health":{"get":{"operationId":"getHealth","security":[],"summary":"Get API health and engine versions","responses":{"200":{"description":"Healthy"}}}},"/openapi.json":{"get":{"operationId":"getOpenApiDocument","security":[],"summary":"Get this OpenAPI document","responses":{"200":{"description":"OpenAPI 3.1 document"}}}}}}