Schedule
The schedule endpoint returns today’s temperature plan for one HVAC unit: 48 half-hour intervals in a small fixed shape. It suits a client that drives a single thermostat by setpoint.
For multi-appliance schedules (HVAC + EV charger + battery + water heater in one call), and for the per-slot mode and fan schedules, use /api/v1/schedules. This route never returns hvac_mode_schedule, fan_mode_schedule or room_target_temps.
A plan for today can be replaced during the day, after a settings change and when the API re-plans the rest of the day. Poll GET /api/v1/schedules/updated-at and fetch again when its value changes.
GET /api/v1/schedule
Section titled “GET /api/v1/schedule”Auth: Required (Bearer JWT)
Query parameters
Section titled “Query parameters”| Param | Required | Notes |
|---|---|---|
appliance_id |
Conditional | UUID of the HVAC unit whose schedule to return. Required (returns 400) when the account has 2+ HVAC units — otherwise the endpoint can’t tell which one you mean. Single-HVAC accounts may omit it; it resolves to their only HVAC. An id that is not one of the caller’s own HVAC units returns the defaults payload below, not an error. |
Example
Section titled “Example”curl "https://api.hungrymachines.io/api/v1/schedule?appliance_id=a1b2c3d4-..." \ -H "Authorization: Bearer YOUR_TOKEN"Response (200) — stored plan
Section titled “Response (200) — stored plan”{ "date": "2025-11-18", "schedule": { "intervals": [0, 1, 2, "...", 47], "high_temps": [74.0, 74.0, "...(48 values)"], "low_temps": [70.0, 70.0, "...(48 values)"], "setpoint_temps": [72.0, 71.5, "...(48 values)"], "temp_trajectory": [72.1, 71.6, "...(48 values)"] }, "mode": "cool", "estimated_savings_pct": 18.5, "model_confidence": null, "generated_at": "2025-11-18T04:15:00+00:00", "stale": false, "source": "optimization"}Response fields
Section titled “Response fields”Every key below is present in every response. A value that does not apply is null, never missing.
| Field | Type | Description |
|---|---|---|
date |
string | The date of the plan returned (YYYY-MM-DD); today’s date on the defaults path |
schedule.intervals |
int[48] | Always [0, 1, ..., 47] — 48 half-hour slots |
schedule.high_temps |
float[48] | Per-interval upper comfort bound (°F). Display-only — the thermostat never sees this value. |
schedule.low_temps |
float[48] | Per-interval lower comfort bound (°F). Display-only. |
schedule.setpoint_temps |
float[48] or null | The 48-element command to apply to the thermostat at each 30-min boundary, regardless of HVAC mode. null when source is "defaults" (and on plans stored before the field existed) — fall back to (high_temps[i] + low_temps[i]) / 2. |
schedule.temp_trajectory |
float[48] or null | Predicted indoor-temperature line for charting. Display-only. null when source is "defaults", and on a warm-up day. |
mode |
string | "cool" or "heat" on a stored plan. On the defaults path it echoes the account’s optimization_mode preference, so it can also be "auto" or "off". |
estimated_savings_pct |
float | Display-only estimated % savings vs. a thermostat held at the middle of your comfort band. 0.0 on the defaults path. |
model_confidence |
float or null | Display-only, reserved. Currently always null; don’t build on it. |
generated_at |
string | When the plan was produced (UTC ISO 8601); the time of the request on the defaults path |
stale |
boolean | false when the plan returned is for today, and always false on the defaults path. true when there is no plan for today and the most recent stored plan is returned instead. |
source |
string | "optimization" (a stored plan) or "defaults" (no stored plan yet) |
The 48-interval shape
Section titled “The 48-interval shape”Every schedule contains exactly 48 intervals, each a 30-minute window:
| Interval | Time |
|---|---|
| 0 | 00:00–00:30 |
| 1 | 00:30–01:00 |
| … | … |
| 16 | 08:00–08:30 |
| 34 | 17:00–17:30 |
| 47 | 23:30–00:00 |
Slot 0 begins at local midnight (the account’s configured timezone). Compute the current interval as interval = hour * 2 + (1 if minute >= 30 else 0) using local time.
Response variants
Section titled “Response variants”Stale schedule
Section titled “Stale schedule”If there is no plan for today, the most recent stored one is returned with stale: true, and date tells you which day it was made for:
{ "date": "2025-11-17", "stale": true, "source": "optimization", "...": "..." }Defaults (no stored plan yet)
Section titled “Defaults (no stored plan yet)”Until the first plan is stored for the unit, the response carries a comfort band built from the account’s preferences, with setpoint_temps and temp_trajectory set to null:
{ "date": "2025-11-18", "schedule": { "intervals": [0, 1, "...", 47], "high_temps": [73.5, 73.5, "...(all same)"], "low_temps": [70.5, 70.5, "...(all same)"], "temp_trajectory": null, "setpoint_temps": null }, "mode": "cool", "estimated_savings_pct": 0.0, "model_confidence": null, "generated_at": "2025-11-18T12:00:00+00:00", "stale": false, "source": "defaults"}Integration pattern
Section titled “Integration pattern”- Call
GET /api/v1/schedulewhen your client starts and after the overnight run. - Cache the response locally.
- Poll
GET /api/v1/schedules/updated-atevery few minutes and repeat step 1 when its value changes. - Every 30 minutes, look up the current interval
idxand applysetpoint_temps[idx]. Ifsetpoint_tempsisnull, apply the midpoint ofhigh_temps[idx]andlow_temps[idx].
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 400 | "User has multiple HVAC appliances; pass appliance_id to disambiguate." |
2+ HVAC units and no appliance_id supplied |
| 401 | "Not authenticated", "Invalid token" or "Token expired" |
No Authorization header, or a bad or expired token |
POST /api/v1/schedule/recompute
Section titled “POST /api/v1/schedule/recompute”Recompute schedules for the account now, wait for the result, and return it. Built for “user just hit Save on a constraint editor — show the new chart now” flows. The response has exactly three keys: date, appliances (the same per-appliance entries as GET /api/v1/schedules) and a results array reporting each active appliance’s outcome for this run. No request body.
Auth: Required (Bearer JWT)
Example
Section titled “Example”curl -X POST https://api.hungrymachines.io/api/v1/schedule/recompute \ -H "Authorization: Bearer YOUR_TOKEN"Response (200)
Section titled “Response (200)”The response does not carry the top-level optimization_enabled, integration_health or divergence_summary that GET /api/v1/schedules returns. Call that route when you need them.
{ "date": "2025-11-18", "appliances": ["<same entries as GET /api/v1/schedules>"], "results": [ { "appliance_id": "uuid-1", "appliance_type": "hvac", "name": "Office AC", "status": "ok" }, { "appliance_id": "uuid-2", "appliance_type": "hvac", "name": "Front AC", "status": "failed", "detail": "internal error" } ]}results[].status values:
| Value | Meaning |
|---|---|
ok |
A fresh schedule was stored for this appliance. An ok entry may carry extra boolean diagnostic keys; ignore them. |
failed |
This appliance was not re-planned; detail carries a backend-defined reason string (log or display it, don’t parse it). Its previous schedule is still in appliances. Tell the user it didn’t update. |
calibration |
A warm-up day is in effect for this HVAC; constraint changes won’t show on its chart until it completes. |
forecast |
Solar — nothing to schedule, but a forecast row IS written (generation, self-consumption, surplus, export revenue). |
observe |
Dehumidifier — data-collection only. |
skipped |
Unknown appliance type. |
results is an empty array, and appliances holds the previous entries, when the run could not start for the whole account: no profile yet, no active appliances, the account’s optimization_mode is "off", the weather forecast could not be loaded, or the service is temporarily unable to plan.
An appliance that still has no stored plan appears in appliances with "schedule": {} and "source": "defaults", exactly as on GET /api/v1/schedules.
Latency
Section titled “Latency”Usually a few seconds; allow for about 20 seconds per HVAC unit before timing out. Show an “Optimizing…” overlay while the request is in flight.
When to use it
Section titled “When to use it”- Right after the user changes constraints, comfort bands, or
optimization_modeand expects to see the impact immediately. - After saving a custom rate curve via
PUT /api/v1/rates.
For the regular fetch, use GET /api/v1/schedule or GET /api/v1/schedules — they read the most recent stored plan without triggering a recompute.
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 401 | "Not authenticated", "Invalid token" or "Token expired" |
No Authorization header, or a bad or expired token |