{"openapi":"3.1.0","info":{"title":"Kairotope","version":"1.0.0","description":"Plans errands and commitments against real opening hours and live traffic, and remembers a household's places and preferences. Every operation returns an envelope whose `kind` says what happened: plan, clarify (ask the user `questions`, then plan again), infeasible (explain `reasons`), limit, not_found, error or ok. Relay plan times exactly as returned. If `suggestions` are present, ask the user each `question` and save only the places they confirm. Map data © HERE and © OpenStreetMap contributors (openstreetmap.org/copyright), powered by Geoapify.","contact":{"email":"hello@kairotope.com"}},"servers":[{"url":"https://kairotope.com/api/v1"}],"security":[{"bearer":[]}],"paths":{"/plans":{"get":{"operationId":"listPlans","summary":"Upcoming plans (one per day, confirmed or draft)","description":"Answer 'what are my plans?' from Kairotope's records rather than memory.","responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"}}},"post":{"operationId":"planErrands","summary":"Plan errands and commitments","description":"Turn a request like 'pick up my prescription, get groceries and be at dinner by 7' into an ordered, traffic-aware plan with a leave-by time. Call once per request; use revise for changes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanInput"}}}},"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"},"429":{"description":"Daily plan limit reached"}}}},"/plans/check":{"get":{"operationId":"checkPlan","summary":"Is a plan still on track? (today's by default)","description":"Answers from Kairotope's live-traffic checks; makes no new plan and uses no allowance. Poll this before the user leaves instead of re-planning.","parameters":[{"name":"date","in":"query","required":false,"schema":{"type":"string"},"description":"YYYY-MM-DD; leave out for today's plan (else the next upcoming one)"}],"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"}}}},"/plans/{planId}/revise":{"post":{"operationId":"revisePlan","summary":"Change an earlier plan","parameters":[{"name":"planId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviseInput"}}}},"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"},"404":{"description":"No such plan"}}}},"/plans/{planId}/confirm":{"post":{"operationId":"confirmPlan","summary":"The user accepted this plan (schedules the evening check-in email)","parameters":[{"name":"planId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"},"404":{"description":"No such plan"}}}},"/plans/{planId}/cancel":{"post":{"operationId":"cancelPlan","summary":"Drop a plan before its day (no check-in, hidden from list)","parameters":[{"name":"planId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"},"404":{"description":"No such plan"}}}},"/plans/{planId}/outcome":{"post":{"operationId":"reportOutcome","summary":"How the plan went","parameters":[{"name":"planId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutcomeInput"}}}},"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"},"404":{"description":"No such plan"}}}},"/household":{"get":{"operationId":"getHousehold","summary":"What Kairotope knows: saved places and preferences","responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Household"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"}}}},"/household/places":{"post":{"operationId":"rememberPlace","summary":"Save a place ('home', 'my pharmacy')","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RememberPlace"}}}},"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"},"404":{"description":"Couldn't find that place"},"409":{"description":"Something different is already saved under that name (kind confirm_replace). Nothing changed: ask the user, then send again with replace: true and the result's confirm_token"}}}},"/household/preferences/{key}":{"put":{"operationId":"setPreference","summary":"Save a preference (e.g. max_drive_minutes)","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PreferenceValue"}}}},"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"},"409":{"description":"Something different is already saved under that name (kind confirm_replace). Nothing changed: ask the user, then send again with replace: true and the result's confirm_token"}}}},"/household/{name}":{"delete":{"operationId":"forget","summary":"Delete a saved place or preference","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Result envelope (check `kind`)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope"}}}},"401":{"description":"Missing or invalid connector token"},"404":{"description":"Nothing saved under that name"}}}}},"components":{"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"Kairotope connector token (kt_…)"}},"schemas":{"PlanInput":{"type":"object","properties":{"intention":{"type":"string","minLength":1,"description":"The user's request in their own words"},"date":{"description":"YYYY-MM-DD if known; otherwise Kairotope infers it","type":"string"},"start_location":{"description":"Only when the user starts somewhere other than their saved home. Don't paste the home address here; save it with remember.","type":"string"},"commitments":{"description":"Fixed events from the user's calendar that the plan must respect","type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"start":{"type":"string","description":"ISO 8601 (2026-10-01T08:00:00-05:00) or HH:MM local"},"end":{"type":"string"},"location":{"type":"string"}},"required":["title","start"]}}},"required":["intention"]},"ReviseInput":{"type":"object","properties":{"change":{"type":"string","minLength":1,"description":"What to change, in the user's words"}},"required":["change"]},"OutcomeInput":{"type":"object","properties":{"status":{"type":"string","description":"completed | partial | abandoned (the day didn't go to plan) | cancelled (the plan was dropped before the day)"},"stop_seq":{"description":"A single stop this is about, if any","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"rating":{"type":"integer","minimum":1,"maximum":5},"note":{"type":"string"}},"required":["status"]},"RememberPlace":{"type":"object","properties":{"label":{"type":"string","minLength":1},"where":{"type":"string","minLength":1},"kind":{"type":"string"},"replace":{"description":"Only after the user said yes to a `confirm_replace` question: replace what's saved. Send it with that result's confirm_token. Never set it on the first call.","type":"boolean"},"confirm_token":{"description":"With replace: the `confirm_token` from the `confirm_replace` result the user said yes to. It expires after 10 minutes.","type":"string"}},"required":["label","where"]},"PreferenceValue":{"type":"object","properties":{"value":{"type":["string","number","boolean"]},"replace":{"description":"Only after the user said yes to a `confirm_replace` question: replace what's saved. Send it with that result's confirm_token. Never set it on the first call.","type":"boolean"},"confirm_token":{"description":"With replace: the `confirm_token` from the `confirm_replace` result the user said yes to. It expires after 10 minutes.","type":"string"}},"required":["value"]},"Envelope":{"type":"object","properties":{"kind":{"type":"string","enum":["plan","clarify","infeasible","probe","limit","error","not_found","ok","confirm_replace"]},"planId":{"description":"Pass to revise / confirm / outcome","type":"string"},"plan":{"type":"object","properties":{"planId":{"type":"string"},"status":{"type":"string","enum":["on_track","at_risk","disrupted","live"]},"leaveBy":{"type":"string","description":"ISO time the user must leave the start location"},"stops":{"type":"array","items":{"type":"object","properties":{"seq":{"type":"integer","minimum":0,"maximum":9007199254740991},"label":{"type":"string"},"placeName":{"type":"string"},"address":{"type":"string"},"arriveAt":{"type":"string","description":"ISO 8601 with offset, e.g. 2026-10-03T16:05:00-05:00"},"departAt":{"type":"string"},"legMinutes":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Drive time from the previous stop, traffic-aware"},"freeMinutesBefore":{"description":"Free time after the previous stop before driving here (go home, take a break). Leave the previous stop at arriveAt minus legMinutes.","type":"integer","minimum":0,"maximum":9007199254740991},"isFixed":{"type":"boolean","description":"True for commitments the user cannot move (reservations, pickups)"},"note":{"description":"Why this stop is here or ordered this way, e.g. 'closes 5:30 PM'","type":"string"}},"required":["seq","label","placeName","arriveAt","departAt","legMinutes","isFixed"]}},"totalDriveMinutes":{"type":"integer","minimum":0,"maximum":9007199254740991},"minutesSavedVsNaive":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"unscheduled":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"reason":{"type":"string"}},"required":["label","reason"]},"description":"Errands that could not fit, with why. Tell the user; do not silently drop them."},"warnings":{"type":"array","items":{"type":"string"}},"explanation":{"type":"string","description":"Relay verbatim; every time and number in it comes from the fields above"}},"required":["planId","status","leaveBy","stops","totalDriveMinutes","minutesSavedVsNaive","unscheduled","warnings","explanation"]},"questions":{"description":"Ask the user these, then plan again with the answers","type":"array","items":{"type":"string"}},"reasons":{"type":"array","items":{"type":"string"}},"message":{"type":"string","description":"A short summary safe to relay as-is"},"suggestions":{"description":"Places to offer saving. Ask `question`; if the user agrees, remember(type=place, label, where, kind).","type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"where":{"type":"string"},"kind":{"type":"string"},"question":{"type":"string"}},"required":["label","where","kind","question"]}},"reused":{"description":"True when this identical request was already planned a moment ago","type":"boolean"},"plans":{"description":"list_plans: upcoming days, one plan each","type":"array","items":{"type":"object","properties":{"planId":{"type":"string"},"date":{"type":"string","description":"YYYY-MM-DD, the household's local date"},"weekday":{"type":"string"},"status":{"type":"string","enum":["confirmed","draft"],"description":"confirmed = the user locked it in; draft = planned but never confirmed"},"leaveBy":{"type":"string"},"stops":{"type":"array","items":{"type":"object","properties":{"arriveAt":{"type":"string"},"departAt":{"type":"string"},"label":{"type":"string"},"place":{"type":"string"},"startsAt":{"description":"Commitments only: when it starts (arriveAt is earlier, with the buffer)","type":"string"}},"required":["arriveAt","departAt","label","place"]}},"otherDrafts":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Other unconfirmed plans for the same day, not shown"},"latestAlert":{"description":"Today's plan only: the newest traffic alert Kairotope emailed for a drive still to come","type":"object","properties":{"sentAt":{"type":"string","description":"When Kairotope emailed it"},"subject":{"type":"string"},"lines":{"type":"array","items":{"type":"string"},"description":"The alert's sentences; relay their times exactly"}},"required":["sentAt","subject","lines"]}},"required":["planId","date","weekday","status","leaveBy","stops","otherDrafts"]}},"localNow":{"description":"The household's current local date and time. Use it for 'today', 'tomorrow' and how far away things are.","type":"string"},"today":{"description":"The household's local date, YYYY-MM-DD","type":"string"},"nextStep":{"description":"An instruction for the assistant (not to read out): what to do after relaying this result","type":"string"},"notices":{"description":"Traffic alerts the user hasn't seen through an assistant yet, and 'how did it go?' check-ins. Pass them on before your answer. Each comes once.","type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["traffic_alert","checkin"]},"planId":{"type":"string"},"message":{"type":"string","description":"What to tell or ask the user, before your answer"},"nextStep":{"type":"string","description":"An instruction for the assistant, not to read out"},"sentAt":{"description":"traffic_alert: when Kairotope emailed it","type":"string"}},"required":["kind","planId","message","nextStep"]}},"check":{"description":"check_plan: whether the plan is still on track","type":"object","properties":{"planId":{"type":"string"},"date":{"type":"string"},"status":{"type":"string","enum":["confirmed","draft"]},"liveTraffic":{"type":"boolean","description":"Kairotope is checking this plan's drives against live traffic"},"drives":{"type":"array","items":{"type":"object","properties":{"seq":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"to":{"type":"string"},"place":{"type":"string"},"leaveAt":{"type":"string","description":"Planned time to leave for this stop"},"plannedMinutes":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"liveMinutes":{"description":"The latest live-traffic drive time, once checked","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"checkedAt":{"type":"string"}},"required":["seq","to","place","leaveAt","plannedMinutes"]},"description":"Drives still to come"},"alert":{"description":"The newest traffic alert for a drive still to come: relay it","type":"object","properties":{"sentAt":{"type":"string","description":"When Kairotope emailed it"},"subject":{"type":"string"},"lines":{"type":"array","items":{"type":"string"},"description":"The alert's sentences; relay their times exactly"}},"required":["sentAt","subject","lines"]}},"required":["planId","date","status","liveTraffic","drives"]}},"required":["kind","message"]},"Household":{"type":"object","properties":{"kind":{"type":"string","const":"ok"},"message":{"type":"string"},"timezone":{"type":"string"},"places":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"address":{"type":"string"}},"required":["label","kind","name"]}},"preferences":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["kind","message","timezone","places","preferences"]}}}}