# Troubleshooting

Common issues and how to resolve them when integrating with the Hungry Machines API.

## 401 with a fresh token

**Symptom:** You just signed in, but API calls return `401 Token expired` or `401 Invalid token`.

**Causes:**

- **Clock skew.** JWT validation checks `exp` (expiration) against the server's clock. If your device's clock is more than a few minutes off, tokens may appear expired. Sync your system clock with NTP.
- **Token copied incorrectly.** Make sure you're using the full `access_token` string with no trailing whitespace or line breaks.
- **Wrong header format.** The header must be exactly `Authorization: Bearer <access_token>` — note the space after `Bearer` and no quotes around the token value.
- **Using the wrong token.** `access_token` goes in the `Authorization` header. `refresh_token` is only for exchanging at `POST /auth/refresh`.

**Fix:** Verify your system clock is accurate (`date -u` should match UTC within a few seconds). If the clock is fine, refresh your access token (see [Authentication](/getting-started/authentication/#refresh-an-expired-token)) and confirm the `Authorization` header is formatted correctly.

---

## Access token expired

**Symptom:** API calls were working, now they return `401 Token expired`.

**Cause:** Access tokens last **1 hour**. Your client needs to exchange the refresh token for a new access token before the current one expires.

**Fix:** Exchange your refresh token for a new access token:

```bash
curl -X POST https://api.hungrymachines.io/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "YOUR_REFRESH_TOKEN"}'
```

The response contains a new `access_token` and a new `refresh_token` — save both, then retry your original request with the new access token. If this call itself returns `400 Invalid Refresh Token`, the refresh token is expired (30-day max) — the user needs to log in again.

---

## Readings accepted but no schedule

**Symptom:** `POST /api/v1/readings` returns `201 accepted`, but the schedule still reads `source: "defaults"` with zero savings.

**Cause:** `source` is `"defaults"` until the first plan is stored for the appliance. Pushing readings does not store one. A plan is stored by a planning run: the first recompute (a settings save, or `POST /api/v1/schedule/recompute`) or the next overnight run.

**What to expect:**

1. **Before the first plan:** `GET /api/v1/schedule` returns `source: "defaults"`, a comfort band built from your preferences, and `setpoint_temps: null`. `GET /api/v1/schedules` returns that appliance with `"schedule": {}` and `source: "defaults"`.
2. **From the first plan on:** `source` is `"optimization"` and `setpoint_temps` is a 48-element array, every day. Savings can read `0.0` on early plans, for example before any readings have arrived.
3. **Ongoing:** schedules improve as Hungry Machines sees more readings, more weather, and more of how your home responds.

**Fix:** Call `POST /api/v1/schedule/recompute` and read its `results` array. An entry with `status: "ok"` means a plan was stored. An empty `results` means the run could not start for the account: check that an appliance is registered and that the account's `optimization_mode` is not `"off"`. While `source` is `"defaults"` on `GET /api/v1/schedule`, apply the band midpoint as your setpoint: `(high_temps[idx] + low_temps[idx]) / 2`. On `GET /api/v1/schedules` there is no band to fall back to, so send that appliance no commands.

> **Note**
>
> Gaps in readings (network outages, sensor downtime) slow down learning — there's simply less data to learn from. Aim for continuous 5-minute readings for the fastest ramp-up. Pushing your `weather.*` forecast helps fill outdoor-temperature gaps so a sensor blip doesn't waste data (if you don't push one, a fallback weather source is used automatically).

---

## Schedule looks like flat setpoints

**Symptom:** `source` is `"optimization"` but the `high_temps` and `low_temps` arrays have little variation — they look almost flat.

**Causes:**

- **A narrow comfort band.** The optimizer can only shift load inside the band you give it. The default band depends only on your base temperature and mode: when cooling it runs from 2 °F below base to 1 °F above overnight, and from 5 °F below to 6 °F above between 08:00 and 21:59; heating mirrors it (1 °F below to 2 °F above overnight, 6 °F below to 5 °F above by day). `savings_level` does not change it. If you have saved hourly bands that are tighter than that, widen them via `PUT /api/v1/preferences`.
- **Flat rate plan.** If your utility charges the same rate all day, there's no price signal to optimize against. The optimizer works best with time-of-use (TOU) or real-time pricing.
- **Mild weather.** When outdoor and indoor temps are close, the HVAC runs very little and there's less to optimize.

**Fix:** Check your hourly comfort bands and `optimization_mode` in preferences. If you're on a flat-rate plan, the optimizer may not produce significant schedule variation — that's expected behavior, not a bug. You can inspect (and override) the rate curve the optimizer is using via [GET / PUT /api/v1/rates](/api-reference/rates/) — submitting your own 24-hour curve is the fastest way to give the optimizer a price signal to work with.

---

## Appliance registration returns 403

**Symptom:** `POST /api/v1/appliances` returns `403 Upgrade required`.

**Cause:** Free-tier accounts are limited to **one HVAC appliance**. You'll get a 403 if you try to:

- Register a second appliance (any type)
- Register a non-HVAC device (EV charger, home battery, water heater)

**Fix:** Upgrade to Premium via the dashboard at [hungrymachines.io/dashboard](https://hungrymachines.io/dashboard). Premium accounts can register unlimited appliances of any type.

---

## Rate limit exceeded (429)

**Symptom:** `POST /api/v1/readings` returns `429 Rate limit exceeded`.

**Cause:** The ceiling is 600 readings per active HVAC or dehumidifier appliance, per account, per UTC day (600 for an account with none registered yet). At 5-minute intervals one unit sends 288 a day, well under the cap. Large catch-up batches after an outage, or an uncoordinated set of sensors all pushing at once, can trip it.

**Fix:** Push on a steady cadence rather than in bursts. If you're catching up after an outage, trim to the most recent readings. The ceiling scales with the number of active HVAC and dehumidifier appliances registered on the account; tag each reading with its `appliance_id` so it is attributed to the right unit.

---

## Still stuck?

If none of the above resolves your issue:

- **File a bug** on [GitHub Issues](https://github.com/hungrymachines/hungry-machines/issues) — include the endpoint, request shape, and full error response.
- **Email us** at [info@hungrymachines.io](mailto:info@hungrymachines.io) for account-specific issues.

Canonical page: https://docs.hungrymachines.io/troubleshooting/
