Skip to content

Authentication (Reference)

This is reference documentation for paths that talk to the API directly — Custom Home Assistant integration and Build Your Own Client. If you installed the Energy Dashboard, you don’t need any of this — the dashboard handles tokens for you.

Always sign up at hungrymachines.io/signup. That flow handles billing, subscription state, email verification, and account onboarding — none of which the API alone takes care of. Once your account exists, the two endpoints below are how you exchange your credentials for an API token.

POST /auth/login and POST /auth/refresh both return the same shape:

{
"access_token": "eyJhbGciOiJFUzI1NiIs...",
"refresh_token": "v1.Mfg2...",
"expires_in": 3600,
"expires_at": 1735689600,
"token_type": "bearer",
"user": { "id": "550e8400-...", "email": "you@example.com" }
}
TokenLifetimePurpose
access_token1 hourSent with every API request as Authorization: Bearer ...
refresh_token30 daysExchanged for a new access token without re-entering credentials

The refresh token is rotated on each /auth/refresh call — always save the new one and discard the old.

Terminal window
curl -X POST https://api.hungrymachines.io/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "your-secure-password"}'
Terminal window
curl -X POST https://api.hungrymachines.io/auth/refresh \
-H "Content-Type: application/json" \
-d '{"refresh_token": "v1.Mfg2..."}'

Every authenticated request uses the Bearer scheme:

Terminal window
curl https://api.hungrymachines.io/auth/me \
-H "Authorization: Bearer $ACCESS_TOKEN"

Full response shape in Auth Reference.

Terminal window
curl -X POST https://api.hungrymachines.io/api/v1/readings \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "readings": [...] }'

When the API returns 401 Unauthorized, the token is missing, malformed, or expired. The detail field tells you which:

{"detail": "Not authenticated"} // no Authorization header
{"detail": "Token expired"} // past the 1-hour lifetime
{"detail": "Invalid token"} // malformed or wrong signature

Recovery pattern for automated clients:

  1. Make the API call.
  2. If 401 Token expired, call POST /auth/refresh with your saved refresh token.
  3. Save the new access_token and refresh_token.
  4. Retry the original request with the new access token.
  5. If refresh itself returns 400 Invalid Refresh Token, the refresh token is stale (30-day expiry, or the user logged out elsewhere). Surface a “please sign in again” error.

Users reset their password via hungrymachines.io/reset-password — the web flow sends a reset link to their registered email. Password reset is not currently exposed as an API endpoint.

import httpx
from datetime import datetime, timezone
API = "https://api.hungrymachines.io"
class Client:
def __init__(self, email: str, password: str):
resp = httpx.post(f"{API}/auth/login", json={"email": email, "password": password})
resp.raise_for_status()
session = resp.json()
self.access_token = session["access_token"]
self.refresh_token = session["refresh_token"]
self.expires_at = session["expires_at"]
def _refresh(self):
resp = httpx.post(f"{API}/auth/refresh", json={"refresh_token": self.refresh_token})
resp.raise_for_status()
session = resp.json()
self.access_token = session["access_token"]
self.refresh_token = session["refresh_token"]
self.expires_at = session["expires_at"]
def request(self, method: str, path: str, **kwargs):
# Refresh proactively if we're within 5 minutes of expiry.
now = int(datetime.now(timezone.utc).timestamp())
if now >= self.expires_at - 300:
self._refresh()
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {self.access_token}"
return httpx.request(method, f"{API}{path}", headers=headers, **kwargs)
client = Client("you@example.com", "secure-password")
resp = client.request("GET", "/auth/me")
print(resp.json())
class HMClient {
constructor(session) {
this.access_token = session.access_token;
this.refresh_token = session.refresh_token;
this.expires_at = session.expires_at;
}
static async login(email, password) {
const resp = await fetch('https://api.hungrymachines.io/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!resp.ok) throw new Error('login failed');
return new HMClient(await resp.json());
}
async _refresh() {
const resp = await fetch('https://api.hungrymachines.io/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: this.refresh_token }),
});
if (!resp.ok) throw new Error('refresh failed — sign in again');
const session = await resp.json();
this.access_token = session.access_token;
this.refresh_token = session.refresh_token;
this.expires_at = session.expires_at;
}
async request(path, opts = {}) {
if (Math.floor(Date.now() / 1000) >= this.expires_at - 300) {
await this._refresh();
}
return fetch(`https://api.hungrymachines.io${path}`, {
...opts,
headers: { ...opts.headers, Authorization: `Bearer ${this.access_token}` },
});
}
}
const client = await HMClient.login('you@example.com', 'secure-password');
const me = await (await client.request('/auth/me')).json();