Customer Metrics
The /api/v1/me endpoints return the signed-in customer’s own headline numbers — savings, peaks avoided, flexibility, comfort, plan adherence, connected machines, and a ranking against comparable homes. Use them to build a “what did my system do for me” dashboard.
Every query is filtered to the signed-in user: these routes return only the caller’s own figures and never expose another customer’s data. Metrics are distilled from the caller’s daily data once a day, so a newly-connected home fills in over its first days of operation.
GET /api/v1/me/metrics/summary
Section titled “GET /api/v1/me/metrics/summary”Trailing-window aggregate of the caller’s savings, peaks avoided, flexibility, comfort, plan adherence and connected machines — one call for a headline row.
Auth: Required (Bearer JWT)
Query parameters
Section titled “Query parameters”| Param | Required | Type | Notes |
|---|---|---|---|
window | No | integer | Trailing days to aggregate. Default 30, minimum 1, maximum 90. A larger value returns 400. |
Cumulative fields (dollars saved, hours idle, override events) are summed over the window; intensity fields (percentages, degree-hours) are meaned, so those figures stay comparable regardless of how many days of data a home has.
Example
Section titled “Example”curl "https://api.hungrymachines.io/api/v1/me/metrics/summary?window=30" \ -H "Authorization: Bearer YOUR_TOKEN"Response (200)
Section titled “Response (200)”{ "window": 30, "latest_date": "2026-08-02", "days_counted": 2, "savings": { "modeled_usd": 4.0, "realized_usd": 2.0, "realized_days": 1, "modeled_days": 2, "savings_pct": 20.0, "degenerate": false }, "peaks": { "peak_hours_idle": 6.0, "top_peak_hours_idle": 3.0, "off_peak_kwh_share_pct": 55.0 }, "flexibility": { "degree_hours": 336.0, "percent_hours": null }, "comfort": { "in_band_home_pct": 96.0 }, "adherence": { "conformance_pct": 95.0, "override_events": 3, "override_by_hour": [0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0] }, "machines": { "active": 2, "by_type": { "hvac": 1, "ev_charger": 1 } }}Response fields
Section titled “Response fields”| Field | Type | Description |
|---|---|---|
latest_date | string | null | Most recent day the window is anchored on (YYYY-MM-DD); null when the customer has no history yet. |
days_counted | integer | Number of days inside the window that contributed data. |
savings.modeled_usd | float | null | Projected savings the plan aimed for, summed over the window (USD). |
savings.realized_usd | float | null | Savings measured from actual consumption, summed over the window (USD). null on days without trusted per-machine power. |
savings.realized_days / savings.modeled_days | int | How many days in the window contributed each total. The two differ whenever a day could not be measured, so present the day counts alongside the sums rather than comparing them directly. 0 when nothing contributed. |
savings.savings_pct | float | null | Mean daily savings percent. |
savings.degenerate | boolean | true when savings_pct is ≤ 0 or ≥ 95 — a near-zero or implausibly-high figure a client should flag as a caution, not a win. false when savings_pct is null. |
peaks.peak_hours_idle | float | null | Total hours the home’s machines sat idle during the day’s priciest quarter, summed over the window. |
peaks.top_peak_hours_idle | float | null | Same measure over the three priciest hours only (max 3.0 per day), summed over the window. Always ≤ peak_hours_idle. |
peaks.off_peak_kwh_share_pct | float | null | Mean share of daily kWh that landed in the cheapest slots. |
flexibility.degree_hours | float | null | Mean daily comfort-band width, in degree-hours (thermal machines). Room the customer gave the plan to shift load. |
flexibility.percent_hours | float | null | Mean charge-window slack percent (shiftable machines — EV, battery). |
comfort.in_band_home_pct | float | null | Mean percent of the day the home stayed inside its comfort band. Show this alongside flexibility so the page never reads as “give up comfort to win”. |
adherence.conformance_pct | float | null | Mean plan-delivery conformance across the customer’s machines — how reliably we delivered the plan. A shortfall is our delivery problem, kept separate from the customer’s own overrides. |
adherence.override_events | float | null | Total manual-override events over the window. |
adherence.override_by_hour | array[int] (length 24) | null | Hour-of-day histogram (index 0 = local midnight) of when the customer overrode the plan, summed over the window — render it as a 24-bar chart. null (never a fabricated 24 zeros) until there is histogram data. Reflects the customer’s decisions only; never combined with delivery conformance. |
machines.active | integer | Count of the customer’s active machines. A real count — a 0 here is legitimate, not a missing metric. |
machines.by_type | object | Per-type count of active machines, keyed by machine type (hvac, ev_charger, home_battery, water_heater, solar). |
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 400 | "window exceeds 90 days" | window greater than 90 |
| 401 | "Not authenticated" | Missing or invalid token |
GET /api/v1/me/metrics/daily
Section titled “GET /api/v1/me/metrics/daily”The same savings / peaks / flexibility / comfort metrics as /summary, but per day and un-aggregated, so a dashboard can chart a trend rather than a single number.
Auth: Required (Bearer JWT)
Query parameters
Section titled “Query parameters”| Param | Required | Type | Notes |
|---|---|---|---|
start | No | string (ISO date YYYY-MM-DD) | Window start, inclusive. Default end − 30 days. |
end | No | string (ISO date YYYY-MM-DD) | Window end, inclusive. Default the caller’s local today. |
The window is capped at 90 days; a wider span, or start after end, returns 400.
Example
Section titled “Example”curl "https://api.hungrymachines.io/api/v1/me/metrics/daily?start=2026-07-04&end=2026-08-03" \ -H "Authorization: Bearer YOUR_TOKEN"Response (200)
Section titled “Response (200)”{ "start": "2026-07-04", "end": "2026-08-03", "days": [ { "date": "2026-08-01", "savings": { "modeled_usd": 1.0, "realized_usd": 0.5, "savings_pct": 10.0, "degenerate": false }, "peaks": { "peak_hours_idle": 2.0, "top_peak_hours_idle": 1.5, "off_peak_kwh_share_pct": 50.0 }, "flexibility": { "degree_hours": 336.0, "percent_hours": null }, "comfort": { "in_band_home_pct": 96.0 } } ]}Per-day field meanings match the corresponding /summary fields above; savings.degenerate is computed for that single day.
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 400 | "window exceeds 90 days" / "start must be <= end" | Span too wide, or reversed range |
| 401 | "Not authenticated" | Missing or invalid token |
GET /api/v1/me/metrics/day/{date}
Section titled “GET /api/v1/me/metrics/day/”One local calendar day’s plan versus actual for the caller’s HVAC: the plan the system intended (the 48-slot setpoints, the comfort band, the price curve), the measured indoor temperature, and where plan and reality diverged — split into two separate lists so an override (the customer’s own decision) is never conflated with a conformance defect (a plan the service failed to deliver).
Auth: Required (Bearer JWT)
Path parameters
Section titled “Path parameters”| Param | Required | Type | Notes |
|---|---|---|---|
date | Yes | string (ISO date YYYY-MM-DD) | The local calendar day to render. A non-ISO value returns 400. |
Query parameters
Section titled “Query parameters”| Param | Required | Type | Notes |
|---|---|---|---|
appliance_id | No | string (uuid) | Which HVAC to render. Defaults to the caller’s first active HVAC. Ignored if it doesn’t belong to the caller. |
Example
Section titled “Example”curl "https://api.hungrymachines.io/api/v1/me/metrics/day/2026-06-08" \ -H "Authorization: Bearer YOUR_TOKEN"Response (200)
Section titled “Response (200)”{ "date": "2026-06-08", "appliance_id": "9191a36c-9b61-444a-ac3a-666b7912514e", "setpoints": [72.0, 72.0, "… 48 …"], "comfort_band": { "high": [74.0, "… 48 …"], "low": [70.0, "… 48 …"] }, "price_curve": [12.4, 12.1, "… 48 …"], "indoor_temps": [71.0, null, "… 48 …"], "overrides": [ { "start": "2026-06-08T10:10:00+00:00", "end": "2026-06-08T10:20:00+00:00", "start_hour": 10, "readings": 3 } ], "conformance_defects": [ { "start": "2026-06-08T12:10:00+00:00", "end": "2026-06-08T12:15:00+00:00", "start_hour": 12, "readings": 2 } ]}Response fields
Section titled “Response fields”| Field | Type | Description |
|---|---|---|
appliance_id | string | null | The HVAC this view describes; null when the caller has no active HVAC. |
setpoints | array[float] | null | The 48-slot planned setpoints (slot 0 = local midnight). null when there is no plan for the day. |
comfort_band.high / low | array[float] | null | The 48-slot comfort-band bounds, for display. null when absent. |
price_curve | array[float] | null | The 48-slot price curve in the feed’s own minor units (¢/kWh, pence, etc.). null when unavailable. |
indoor_temps | array[float | null] | Measured indoor temperature averaged into the 48 local half-hour slots; a slot with no reading is null. |
overrides | array[object] | Episodes where the customer changed the plan (the plan was commanded correctly; the thermostat persistently showed something else). Each: {start, end, start_hour, readings} — start / end are UTC ISO timestamps, start_hour is the local hour, readings is the run length. |
conformance_defects | array[object] | Runs where the commanded setpoint itself diverged from the plan — the service’s delivery failure, kept strictly apart from overrides. Same shape. Never mix these into an override count. |
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 400 | "date must be ISO YYYY-MM-DD" | Malformed date path value |
| 401 | "Not authenticated" | Missing or invalid token |
GET /api/v1/me/rankings
Section titled “GET /api/v1/me/rankings”How the caller stacks up against comparable homes — without revealing anything about those homes. For each of four higher-is-better metrics, the route places the caller’s own windowed value against a precomputed distribution for the tightest comparable cohort, and returns only the caller’s rank, percentile and own value — never the cohort’s size, and never another home’s value.
Auth: Required (Bearer JWT)
Query parameters
Section titled “Query parameters”| Param | Required | Type | Notes |
|---|---|---|---|
window | No | integer | Trailing days the ranking window spans. Default 30, minimum 1, maximum 90. A larger value returns 400. Rankings are materialized for the 30-day window today; another window may return null ranks. |
Ranked metrics — all higher-is-better, so rank is 1-based with #1 the best:
| Metric | What it measures |
|---|---|
savings_pct | Savings percent (percent, not dollars, so big homes on punitive plans aren’t flattered). |
off_peak_kwh_share_pct | Share of energy drawn in the cheapest hours. |
flexibility_degree_hours | Comfort-band width offered to the plan. |
active_appliances | Count of connected machine types. |
Cohort ladder. The caller is ranked in the tightest cohort they belong to that has enough comparable homes to rank against, walking outward: zip5 → zip3 → rate_peers → fleet. cohort_kind names which tier the rank is against; cohort_label is a generic, share-safe string — "your ZIP code", "your area", "your rate plan", "all Hungry Machines homes" — that never contains raw ZIP digits or any identifier. It is safe to render on a share card.
Example
Section titled “Example”curl "https://api.hungrymachines.io/api/v1/me/rankings?window=30" \ -H "Authorization: Bearer YOUR_TOKEN"Response (200)
Section titled “Response (200)”{ "window": 30, "latest_date": "2026-08-03", "rankings": [ { "metric": "savings_pct", "cohort_kind": "zip5", "cohort_label": "your ZIP code", "rank": 1, "percentile": 100.0, "your_value": 22.5 }, { "metric": "off_peak_kwh_share_pct", "cohort_kind": "rate_peers", "cohort_label": "your rate plan", "rank": 3, "percentile": 62.5, "your_value": 55.0 }, { "metric": "flexibility_degree_hours", "cohort_kind": "fleet", "cohort_label": "all Hungry Machines homes", "rank": null, "percentile": null, "your_value": null }, { "metric": "active_appliances", "cohort_kind": "zip5", "cohort_label": "your ZIP code", "rank": 1, "percentile": 100.0, "your_value": 2.0 } ]}Response fields
Section titled “Response fields”| Field | Type | Description |
|---|---|---|
latest_date | string | null | The date the ranking is anchored on (YYYY-MM-DD); null when the caller has no data. |
rankings[].metric | string | One of the four ranked metrics above. All four are always present. |
rankings[].cohort_kind | string | null | The ladder tier the rank is against (zip5 / zip3 / rate_peers / fleet); null when no cohort qualifies. |
rankings[].cohort_label | string | null | Generic, share-safe label for that cohort. Never contains ZIP digits. null when no cohort qualifies. |
rankings[].rank | integer | null | 1-based, higher-is-better rank within the cohort. null (never 0) when the caller has no eligible value or no cohort. |
rankings[].percentile | float | null | Percent of the cohort at or below the caller’s value (higher is better). null when unranked. |
rankings[].your_value | float | null | The caller’s own windowed value for the metric. null when they have no eligible day. |
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 400 | "window exceeds 90 days" | window greater than 90 |
| 401 | "Not authenticated" | Missing or invalid token |