{"openapi":"3.1.0","info":{"title":"Kitchen Timer — plan a meal and manage it","version":"kt-v86","summary":"Make a meal plan whose timers ring on the cook's phone, then keep managing them from a chat.","description":"No sign-in. `plan_meal` makes a plan and returns a link for the person to open on their phone, plus a secret control key. The person taps **Start** once on the phone (nothing starts before that). With the plan's code and key you can then pause, resume, add or take off time, skip ahead, add a dish or cancel: before the tap those change the plan, after it they change that plan's timers on the phone and nothing else. Alarms ring on the phone, not in the chat.\\n\\nThese endpoints are the same functions, validation and limits as the MCP tools at `/mcp/open`. Every failure is `{ok:false, error}` with one full sentence.\\n\\nLimits: 12 dishes and 12 hours per plan; 40 plans per source address per hour (80 for everyone); 600 requests per source address per minute; per control key 30 changes per 10 minutes and 200 in total."},"servers":[{"url":"https://kt.tnyforge.com"}],"paths":{"/open/v1/plan_meal":{"post":{"operationId":"plan_meal","summary":"Plan a meal","description":"Make a meal plan: each dish starts at the right moment so everything is ready at the same time. Returns a link (open it on the phone in the kitchen, where the alarms ring), a QR code for that link, and the schedule. YOU do the cooking knowledge: break a recipe into dishes, each with its total cooking minutes, and put the timed moments inside a dish as steps (\"simmer 20, rest 5\" is a dish of 25 minutes with a step \"Rest\" at 20). If the person did not say when they want to eat, ASK before calling; use `readyInMinutes` for \"we eat in 90 minutes\", `readyAt` (an ISO date-time with a UTC offset, or local time plus `timeZone`) for \"at 7\", or `startNow` to start cooking now (a single timer is a one-dish plan with `startNow`). Keep times realistic and use only what the person gave you; at most 12 dishes and 12 hours. Dish names are plain text, no links. The reply link works for anyone who has it and the plan is deleted about a day after the meal. Tell the person the alarms ring on the phone page, not in this chat. The reply also holds a secret CONTROL KEY (starts with ktpk_): remember it for the rest of the chat, do not show it or put it in any link, and pass it with the plan's code to the control tools to keep managing this plan. Once it works you may offer the person to manage their whole kitchen from now on (⋯ → Link an agent in the app gives the connector address and a 6-digit code).","tags":["plans"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"dishes":{"type":"array","minItems":1,"maxItems":12,"description":"What is being cooked. A single timer is one dish.","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"Plain text, 80 characters at most. For example \"Roast chicken\"."},"minutes":{"type":"number","minimum":0.1,"maximum":720,"description":"Total cooking time in minutes, from putting it in until it is done."},"steps":{"type":"array","maxItems":8,"description":"Optional moments inside the cooking time, such as turning or resting.","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200,"description":"For example \"Turn the chicken\"."},"atMinutes":{"type":"number","minimum":0.1,"maximum":720,"description":"Minutes after this dish goes in; before its total time."}},"required":["label","atMinutes"],"additionalProperties":false}}},"required":["name","minutes"],"additionalProperties":false}},"readyInMinutes":{"type":"number","minimum":1,"maximum":720,"description":"Everything ready this many minutes from now. Give exactly one of readyInMinutes, readyAt, startNow."},"readyAt":{"type":["string","number"],"maxLength":64,"description":"Everything ready at this time: ISO 8601 with a UTC offset (2026-10-17T19:00:00+01:00), or without an offset when timeZone is given, or epoch milliseconds."},"startNow":{"type":"boolean","description":"True: start every dish now (the longest dish sets when it is ready)."},"timeZone":{"type":"string","maxLength":64,"description":"Optional IANA time zone such as Europe/London. Only used to word the schedule text; the card and the phone show times in the viewer's own clock."}},"required":["dishes"],"additionalProperties":false}}}},"responses":{"200":{"description":"Done. `text` is the sentence to relay to the person.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string","description":"The same sentence(s) the MCP tool returns."},"code":{"type":"string"},"url":{"type":"string","description":"Open this on the phone in the kitchen."},"readyAtMs":{"type":"integer"},"expiresAtMs":{"type":"integer"},"serverNowMs":{"type":"integer"},"dishes":{"type":"array","items":{"type":"object"}},"schedule":{"type":"array","items":{"type":"object","properties":{"atMs":{"type":"integer"},"kind":{"type":"string","enum":["start","step","ready"]},"label":{"type":"string"}},"required":["atMs","kind","label"]}},"alarms":{"type":"string"},"controlKey":{"type":"string","description":"The secret control key for this plan (starts with ktpk_). Shown once. Keep it for the chat; never put it in the link."}},"required":["ok","text","code","url","readyAtMs","schedule"]}}}},"400":{"description":"The request was wrong (a missing or invalid parameter, a dish that does not fit). `error` says what to fix. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The control key does not match this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"There is no such plan, or it has ended (plans are deleted about a day after the meal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Nothing is running yet (the person has not tapped Start on the phone), or the phone is still starting the timers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests, plans or changes. `error` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/open/v1/meal_status":{"post":{"operationId":"meal_status","summary":"What is next in a meal plan","description":"Say what is next in a meal plan made with plan_meal, and how long until it. Give the plan's code (the last part of its link). Only reads; never changes a plan. Give the control key too, if you have it, to also see the timers on the phone.","tags":["plans"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":200,"description":"The plan's code, or its whole link."},"key":{"type":"string","minLength":1,"maxLength":80,"description":"Optional: the plan's secret control key. With it the answer also says whether the person has started the timers on the phone and what each one is doing."}},"required":["code"],"additionalProperties":false}}}},"responses":{"200":{"description":"Done. `text` is the sentence to relay to the person.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string","description":"The same sentence(s) the MCP tool returns."},"code":{"type":"string"},"state":{"type":"string","enum":["waiting","cooking","ready"]},"serverNowMs":{"type":"integer"},"readyAtMs":{"type":"integer"},"next":{"type":["object","null"]},"upcoming":{"type":"array","items":{"type":"object"}},"claimed":{"type":"boolean","description":"Only with the key: a phone has tapped Start."},"timers":{"type":"array","items":{"type":"object"},"description":"Only with the key, once the phone has started: each dish's state on the phone."}},"required":["ok","text","code","state","readyAtMs"]}}}},"400":{"description":"The request was wrong (a missing or invalid parameter, a dish that does not fit). `error` says what to fix. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The control key does not match this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"There is no such plan, or it has ended (plans are deleted about a day after the meal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Nothing is running yet (the person has not tapped Start on the phone), or the phone is still starting the timers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests, plans or changes. `error` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/open/v1/pause_meal":{"post":{"operationId":"pause_meal","summary":"Pause the meal","description":"Pause every dish of this plan that is running on the phone (the cook can resume on the phone or you can call resume_meal). Needs the plan's code and control key. Only works after the person has tapped Start on the phone; before that it says so.","tags":["plan-control"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":200,"description":"The plan's code, or its whole link."},"key":{"type":"string","minLength":1,"maxLength":80,"description":"The secret control key plan_meal gave you for this plan (starts with ktpk_). Never show it or put it in a link."}},"required":["code","key"],"additionalProperties":false}}}},"responses":{"200":{"description":"Done. `text` is the sentence to relay to the person.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string","description":"The same sentence(s) the MCP tool returns."},"message":{"type":"string"}},"required":["ok","text","message"]}}}},"400":{"description":"The request was wrong (a missing or invalid parameter, a dish that does not fit). `error` says what to fix. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The control key does not match this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"There is no such plan, or it has ended (plans are deleted about a day after the meal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Nothing is running yet (the person has not tapped Start on the phone), or the phone is still starting the timers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests, plans or changes. `error` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/open/v1/resume_meal":{"post":{"operationId":"resume_meal","summary":"Resume the meal","description":"Resume the paused dishes of this plan on the phone. Needs the plan's code and control key.","tags":["plan-control"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":200,"description":"The plan's code, or its whole link."},"key":{"type":"string","minLength":1,"maxLength":80,"description":"The secret control key plan_meal gave you for this plan (starts with ktpk_). Never show it or put it in a link."}},"required":["code","key"],"additionalProperties":false}}}},"responses":{"200":{"description":"Done. `text` is the sentence to relay to the person.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string","description":"The same sentence(s) the MCP tool returns."},"message":{"type":"string"}},"required":["ok","text","message"]}}}},"400":{"description":"The request was wrong (a missing or invalid parameter, a dish that does not fit). `error` says what to fix. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The control key does not match this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"There is no such plan, or it has ended (plans are deleted about a day after the meal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Nothing is running yet (the person has not tapped Start on the phone), or the phone is still starting the timers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests, plans or changes. `error` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/open/v1/adjust_dish":{"post":{"operationId":"adjust_dish","summary":"Add or take off time","description":"Add time to a dish (a positive number of minutes) or take time off (negative). Before the person taps Start this changes the plan, and the ready time stays; afterwards it changes that dish's timer on the phone. Needs the plan's code and control key.","tags":["plan-control"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":200,"description":"The plan's code, or its whole link."},"key":{"type":"string","minLength":1,"maxLength":80,"description":"The secret control key plan_meal gave you for this plan (starts with ktpk_). Never show it or put it in a link."},"dish":{"type":"string","minLength":1,"maxLength":200,"description":"The dish's name as in the plan, or its number in the plan (1 is the first dish)."},"minutes":{"type":"number","minimum":-720,"maximum":720,"description":"Minutes to add (positive) or take off (negative), such as 2 or -1.5. Not zero."}},"required":["code","key","dish","minutes"],"additionalProperties":false}}}},"responses":{"200":{"description":"Done. `text` is the sentence to relay to the person.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string","description":"The same sentence(s) the MCP tool returns."},"message":{"type":"string"}},"required":["ok","text","message"]}}}},"400":{"description":"The request was wrong (a missing or invalid parameter, a dish that does not fit). `error` says what to fix. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The control key does not match this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"There is no such plan, or it has ended (plans are deleted about a day after the meal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Nothing is running yet (the person has not tapped Start on the phone), or the phone is still starting the timers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests, plans or changes. `error` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/open/v1/skip_step":{"post":{"operationId":"skip_step","summary":"Skip ahead on a dish","description":"Skip the rest of the current wait for one dish: a dish still waiting to go in starts now, and a dish that is cooking finishes now. Only works after the person has tapped Start on the phone. Needs the plan's code and control key.","tags":["plan-control"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":200,"description":"The plan's code, or its whole link."},"key":{"type":"string","minLength":1,"maxLength":80,"description":"The secret control key plan_meal gave you for this plan (starts with ktpk_). Never show it or put it in a link."},"dish":{"type":"string","minLength":1,"maxLength":200,"description":"The dish's name as in the plan, or its number in the plan (1 is the first dish)."}},"required":["code","key","dish"],"additionalProperties":false}}}},"responses":{"200":{"description":"Done. `text` is the sentence to relay to the person.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string","description":"The same sentence(s) the MCP tool returns."},"message":{"type":"string"}},"required":["ok","text","message"]}}}},"400":{"description":"The request was wrong (a missing or invalid parameter, a dish that does not fit). `error` says what to fix. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The control key does not match this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"There is no such plan, or it has ended (plans are deleted about a day after the meal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Nothing is running yet (the person has not tapped Start on the phone), or the phone is still starting the timers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests, plans or changes. `error` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/open/v1/add_dish":{"post":{"operationId":"add_dish","summary":"Add a dish to the plan","description":"Add one more dish to this plan. The ready time stays, so the new dish goes in at the ready time minus its cooking time and must still fit. Before the person taps Start it joins the plan on the sheet; afterwards it goes onto the phone too. Same rules as plan_meal: a plain-text name, at most 12 dishes. Needs the plan's code and control key.","tags":["plan-control"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":200,"description":"The plan's code, or its whole link."},"key":{"type":"string","minLength":1,"maxLength":80,"description":"The secret control key plan_meal gave you for this plan (starts with ktpk_). Never show it or put it in a link."},"name":{"type":"string","minLength":1,"maxLength":200,"description":"Plain text, 80 characters at most."},"minutes":{"type":"number","minimum":0.1,"maximum":720,"description":"Total cooking time in minutes."},"steps":{"type":"array","maxItems":8,"description":"Optional moments inside the cooking time.","items":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":200},"atMinutes":{"type":"number","minimum":0.1,"maximum":720}},"required":["label","atMinutes"],"additionalProperties":false}}},"required":["code","key","name","minutes"],"additionalProperties":false}}}},"responses":{"200":{"description":"Done. `text` is the sentence to relay to the person.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string","description":"The same sentence(s) the MCP tool returns."},"message":{"type":"string"}},"required":["ok","text","message"]}}}},"400":{"description":"The request was wrong (a missing or invalid parameter, a dish that does not fit). `error` says what to fix. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The control key does not match this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"There is no such plan, or it has ended (plans are deleted about a day after the meal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Nothing is running yet (the person has not tapped Start on the phone), or the phone is still starting the timers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests, plans or changes. `error` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/open/v1/cancel_meal":{"post":{"operationId":"cancel_meal","summary":"Cancel the meal","description":"Cancel this plan: its link stops working, and if the person already started it, its timers are removed from the phone. Only that plan's timers; nothing else on the phone. Needs the plan's code and control key. Ask the person first.","tags":["plan-control"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"maxLength":200,"description":"The plan's code, or its whole link."},"key":{"type":"string","minLength":1,"maxLength":80,"description":"The secret control key plan_meal gave you for this plan (starts with ktpk_). Never show it or put it in a link."}},"required":["code","key"],"additionalProperties":false}}}},"responses":{"200":{"description":"Done. `text` is the sentence to relay to the person.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"text":{"type":"string","description":"The same sentence(s) the MCP tool returns."},"message":{"type":"string"}},"required":["ok","text","message"]}}}},"400":{"description":"The request was wrong (a missing or invalid parameter, a dish that does not fit). `error` says what to fix. Nothing was changed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The control key does not match this plan.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"There is no such plan, or it has ended (plans are deleted about a day after the meal).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Nothing is running yet (the person has not tapped Start on the phone), or the phone is still starting the timers.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many requests, plans or changes. `error` says how long to wait.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"ok":{"type":"boolean","const":false},"error":{"type":"string","description":"One full sentence a chatbot can relay to the person."}},"required":["ok","error"]}}},"tags":[{"name":"plans","description":"Make a plan and read it."},{"name":"plan-control","description":"Change a plan with its control key."}]}