# Authentication (Reference)

Token lifecycle reference — login, refresh, 401 handling. Only needed if you're building a custom Home Assistant integration or a custom client app.

This is reference documentation for paths that talk to the API directly — [Custom Home Assistant integration](/guides/custom-home-assistant/) and [Build Your Own Client](/guides/build-your-own-client/). If you installed the [Energy Dashboard](/home-assistant/), you don't need any of this — the dashboard handles tokens for you.

## How accounts are created

**Always sign up at [hungrymachines.io/signup](https://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.

> **Caution**
>
> The API does expose `POST /auth/signup` for programmatic account creation, but it bypasses the Stripe customer setup and onboarding state. Use it only for test fixtures or short-lived dev accounts. Real users belong on the website signup flow. See [Auth Reference](/api-reference/auth/) for the full signup request/response shape.

## Two tokens, two lifetimes

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

```json
{
  "access_token": "eyJhbGciOiJFUzI1NiIs...",
  "refresh_token": "v1.Mfg2...",
  "expires_in": 3600,
  "expires_at": 1735689600,
  "token_type": "bearer",
  "user": { "id": "550e8400-...", "email": "you@example.com" }
}
```

| Token | Lifetime | Purpose |
|---|---|---|
| `access_token` | 1 hour | Sent with every API request as `Authorization: Bearer ...` |
| `refresh_token` | 30 days | Exchanged 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.

## Log in

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

## Refresh an expired token

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

## Attaching the access token

Every authenticated request uses the `Bearer` scheme:

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

Full response shape in [Auth Reference](/api-reference/auth/).

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

## Handling 401 errors

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

```json
{"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.

## Password reset

Users reset their password via [hungrymachines.io/reset-password](https://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.

## Language-specific examples

### Python (httpx)

```python
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())
```

### JavaScript (fetch)

```js
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();
```

> **Tip**
>
> Working on something advanced that needs direct database SDK access (e.g. OAuth, magic links, row-level realtime subscriptions)? Email [info@hungrymachines.io](mailto:info@hungrymachines.io) and we'll send you the project URL + publishable key for your project.

## Next steps

- [Custom Home Assistant integration](/guides/custom-home-assistant/) — REST sensors and rest_command setup
- [Build Your Own Client](/guides/build-your-own-client/) — full integration loop with Python + ESP32 examples
- [API Reference — Readings](/api-reference/readings/) — start pushing sensor data
- [OpenAPI description](/api-reference/openapi/) — the auth endpoints' exact request/response shapes, machine-readable

Canonical page: https://docs.hungrymachines.io/getting-started/authentication/
