# Rates

Fetch the electricity rate curve and choose a pricing source (zone, custom, dynamic, or tariff) via /api/v1/rates.

The rates endpoints expose the electricity rate curve the optimizer saves against, plus the metadata to render and edit the user's pricing selection. Use them to color a schedule timeline by peak / off-peak pricing, or to let the user pick how their electricity is priced.

All values are in **cents per kWh**. Clients displaying dollars divide by 100; clients submitting dollars multiply by 100 first. For a tariff or a dynamic zone outside the US, read the same numbers as that plan's own currency in minor units per kWh (pence, euro cents, and so on); nothing is converted. The response's `currency` field names which ISO 4217 currency that is.

The rate curve has the same **48 × 30-minute** shape as schedules — each interval is a half-hour slot of the local day, indexed `0` (00:00–00:30) through `47` (23:30–00:00).

## Pricing sources

A user's rates come from one of four sources, chosen via `PUT /api/v1/rates`:

| Source | What it is |
|---|---|
| `zone` | A preset time-of-use plan from the API-served `available_pricing_zones` catalog (real residential utility plans). The default. |
| `custom` | The user's own 24-hour `hourly_rates_cents_per_kwh` array, expanded to 48 half-hour slots. |
| `dynamic` | Day-ahead pricing that changes daily, for the user's chosen `dynamic_zone`. Zones priced from a wholesale market add a flat per-kWh amount (`pricing_adder_cents_per_kwh`) for delivery and tax riders. The user can also select a delivery/distribution plan (`adder_grid_ruleset_id`, options in `available_delivery_tariffs`) — e.g. ComEd Hourly Pricing users on ComEd's Delivery Time-of-Day plan — and can enter their own four period prices (`delivery_tod_cents`) to match a real bill. When `delivery_tod_cents` is set it is used; otherwise the selected plan's published rates are used. When day-ahead prices are not available, the response carries the zone curve and reports `source: "zone"` while `pricing_source` still reads `"dynamic"`. |
| `tariff` | A plan from the tariff library, linked via `tariff_ruleset_id` (browse with `GET /api/v1/rates/tariffs`). Rates are in the tariff's own currency (minor units per kWh — USD/CAD cents, GBP pence, EUR cents); the dynamic adder does not apply. When the linked tariff is missing or no longer in force, the response carries the zone curve and reports `source: "zone"` while `pricing_source` still reads `"tariff"`. |

---

## GET /api/v1/rates

**Auth:** Required (Bearer JWT)

### Example

```bash
curl https://api.hungrymachines.io/api/v1/rates \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Response (200) — zone

```json
{
  "pricing_location": 1,
  "intervals": [0, 1, 2, "...", 47],
  "rates_cents_per_kwh": [24.7, 24.7, "... 48 values total ..."],
  "source": "zone",
  "season": "summer",
  "hourly_rates_cents_per_kwh": null,
  "pricing_source": "zone",
  "dynamic_zone": null,
  "pricing_adder_cents_per_kwh": null,
  "tariff_ruleset_id": null,
  "available_dynamic_zones": [
    { "slug": "comed", "label": "ComEd — Hourly Pricing (Northern Illinois)" },
    { "slug": "ameren", "label": "Ameren Illinois (Power Smart Pricing)" },
    "... more entries (UK, EU/Nordic, Spain, Australia and California plans) ..."
  ],
  "available_pricing_zones": [
    { "id": 1, "slug": "sdge-tou-dr1", "utility": "SDG&E", "plan": "TOU-DR1", "region": "San Diego, CA", "label": "SDG&E TOU-DR1 — San Diego", "notes": "peak 4-9pm" },
    "... more entries ..."
  ],
  "adder_grid_ruleset_id": null,
  "available_delivery_tariffs": [
    { "id": 12, "external_id": "comed-dtod-sf", "plan_name": "Delivery Time-of-Day — Single-Family", "utility": "ComEd", "region": "Northern Illinois", "period_rates": { "morning": 4.009, "midday_peak": 10.712, "evening": 3.747, "overnight": 2.984 } },
    "... more entries ..."
  ],
  "delivery_tod_cents": null,
  "unit": "cents/kWh",
  "export_rates_cents_per_kwh": null,
  "currency": "USD"
}
```

The `custom`, `dynamic`, and `tariff` responses use the **same shape**; only `source` and the source-specific fields change. A response can carry keys beyond those documented here; ignore them.

- **custom** — `source: "custom"`, `hourly_rates_cents_per_kwh` holds the raw 24-element override.
- **dynamic** — `source: "dynamic"`, `dynamic_zone` is the chosen slug, `pricing_adder_cents_per_kwh` is the per-user adder, `adder_grid_ruleset_id` is the selected delivery/distribution plan (if any), `delivery_tod_cents` is the user's own four period prices if set (used in place of the selected plan's published rates).
- **tariff** — `source: "tariff"`, `tariff_ruleset_id` is the linked tariff-library id; `rates_cents_per_kwh` is in the tariff's currency (minor units per kWh).

### Response fields

| Field | Type | Description |
|---|---|---|
| `pricing_location` | integer | Catalog id from `available_pricing_zones`; defaults to `1` when unset |
| `intervals` | array[int] | Indices `0`–`47` (30-minute slots of the local day) |
| `rates_cents_per_kwh` | array[float] | Exactly 48 values for the source reported in `source` (cents/kWh, or the plan currency's minor units for a tariff or a non-US dynamic zone). Display only. |
| `source` | string | Which pricing source this response's curve came from: `"zone"`, `"custom"`, `"dynamic"`, or `"tariff"`. Differs from `pricing_source` when dynamic or tariff pricing was unavailable and the zone curve was returned instead. |
| `season` | string | `"summer"` or `"winter"`, from the user's local date. Only some plans differ by season; others report the season but return the same curve. |
| `hourly_rates_cents_per_kwh` | array[float] \| null | The user's raw 24-element override, whenever one is stored — regardless of the active `source`. `null` when none is stored. |
| `pricing_source` | string | The user's stored preference: `"zone"`, `"custom"`, `"dynamic"`, or `"tariff"`. Reflect this in your UI toggle. |
| `dynamic_zone` | string \| null | The user's chosen dynamic-pricing slug. `null` when none selected. |
| `pricing_adder_cents_per_kwh` | number \| null | Per-user flat adder for dynamic pricing. `null` = the server default for the zone. |
| `tariff_ruleset_id` | integer \| null | The user's linked tariff-library id (browse via `GET /api/v1/rates/tariffs`). `null` when no tariff linked. |
| `available_dynamic_zones` | `array[{slug, label}]` | Catalog for the dynamic source. Use `slug` when sending `dynamic_zone`; render `label` — it names the plan or market (e.g. `"SCE TOU-D-4-9PM (California)"`, `"Netherlands day-ahead"`). |
| `available_pricing_zones` | `array[{id, slug, utility, plan, region, label, notes}]` | Catalog of preset plans. Use `id` when sending `pricing_location`; render `label`. |
| `adder_grid_ruleset_id` | integer \| null | The user's selected delivery/distribution plan (browse via `available_delivery_tariffs`). Only meaningful alongside `pricing_source="dynamic"`. `null` = no delivery plan selected. When `delivery_tod_cents` is set, those prices are used and this field records which plan they started from. |
| `available_delivery_tariffs` | `array[{id, external_id, plan_name, utility, region, period_rates}]` | Delivery/distribution plans available for `adder_grid_ruleset_id`. `period_rates` is that plan's published `{morning, midday_peak, evening, overnight}` rates — use it to prefill `delivery_tod_cents` when the user selects this plan; `null` if it can't be derived. Empty when none apply to your utility. |
| `delivery_tod_cents` | object \| null | The user's own four period prices — `{morning, midday_peak, evening, overnight}`, cents/kWh. `null` when unset. When set, these prices are used in place of the selected plan's published rates. |
| `unit` | string | Always `"cents/kWh"`. For a tariff or a non-US dynamic zone, read it as that plan's currency minor units per kWh — see `currency` below for which one. |
| `export_rates_cents_per_kwh` | array[float] \| null | What your utility pays for exported solar — 48 half-hour slots (slot 0 = local midnight), same units as `rates_cents_per_kwh`. `null` = no export credit configured. Settable via `PUT`. |
| `currency` | string | ISO 4217 code of the curve actually served: `"USD"` for `zone`/`custom` sources and a USD dynamic zone, the plan's own code for a tariff, the zone's own code for a non-US dynamic zone (e.g. `"GBP"`, `"EUR"`, `"AUD"`). |

### Errors

| Status | Detail | Cause |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid token |

---

## GET /api/v1/rates/tariffs

Browse the tariff library to find a `tariff_ruleset_id` to select on `PUT /api/v1/rates`. Returns plan metadata only — the rate values appear in `GET /api/v1/rates` once a tariff is linked.

**Auth:** Required (Bearer JWT)

### Query parameters

All parameters are optional — with none, the full library is returned page by page.

| Param | Type | Required | Notes |
|---|---|---|---|
| `country` | string | No | Exact country code, case-insensitive (`US`, `CA`). |
| `region` | string | No | Case-insensitive substring match on `region`. |
| `utility` | string | No | Exact utility name, case-insensitive. |
| `q` | string | No | Free-text search over `utility`, `plan_name`, and `region` (case-insensitive substring). |
| `limit` | integer | No | Page size, 1–500 (default 50). |
| `offset` | integer | No | Rows to skip (default 0). |

### Example

```bash
curl "https://api.hungrymachines.io/api/v1/rates/tariffs?country=US&q=tou" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Response (200)

```json
{
  "total": 2,
  "limit": 50,
  "offset": 0,
  "country": "US",
  "region": null,
  "utility": null,
  "q": "tou",
  "tariffs": [
    {
      "id": 7,
      "utility": "SCE",
      "plan_name": "TOU-D-4-9PM",
      "region": "Southern California",
      "country": "US",
      "currency": "USD",
      "verified": false,
      "effective_from": "2026-01-01",
      "disclaimer": "Community-sourced rate — verify against your utility bill"
    },
    "... more entries ..."
  ]
}
```

### Response fields

| Field | Type | Description |
|---|---|---|
| `total` | integer | Full match count **before** pagination — drive "page X of Y" from this, not `tariffs.length` |
| `limit` / `offset` | integer | Echo of the applied pagination |
| `country` / `region` / `utility` / `q` | string \| null | Echo of the applied filters |
| `tariffs[].id` | integer | Send as `tariff_ruleset_id` on `PUT /api/v1/rates` |
| `tariffs[].currency` | string | The tariff's currency (`USD`, `CAD`, `GBP`, `EUR`). Its rates are in this currency's minor units per kWh. |
| `tariffs[].verified` | boolean | `true` once the tariff has been checked against the utility's filed rate sheet |
| `tariffs[].disclaimer` | string | Present **only** on unverified tariffs: `"Community-sourced rate — verify against your utility bill"`. Render it beside the plan; verified tariffs omit the key. |
| `tariffs[].effective_from` | string \| null | First day the tariff is in force (`YYYY-MM-DD`); `null` = open start |

### Errors

| Status | Detail | Cause |
|---|---|---|
| 401 | `"Not authenticated"` | Missing or invalid token |
| 422 | array `detail` | `limit` / `offset` out of range or not an integer |
| 503 | `"Database unavailable"` or `"Failed to load tariff store"` | The tariff library could not be read |

---

## PUT /api/v1/rates

Set or clear any subset of the user's pricing configuration. **Only fields you explicitly send are updated** — omitted fields are left unchanged. A request with none of the fields below changes nothing and returns the current state.

**Auth:** Required (Bearer JWT)

### Required vs. optional

All fields are optional. What you send depends on the source you want:

- **Custom rates** — send `pricing_source: "custom"` together with `hourly_rates_cents_per_kwh` (24 values), unless one is already stored. Send `{"hourly_rates_cents_per_kwh": null}` to clear the stored array.
- **Dynamic** — send `pricing_source: "dynamic"` plus a `dynamic_zone` (required here or already stored). `pricing_adder_cents_per_kwh` is optional.
- **Preset zone** — send `pricing_source: "zone"` plus a `pricing_location` id.
- **Tariff** — send `pricing_source: "tariff"` plus a `tariff_ruleset_id` (required here or already stored) from `GET /api/v1/rates/tariffs`. Sending `tariff_ruleset_id` alone links the tariff without changing the source; `null` clears the link.
- **Delivery/distribution plan** (dynamic only) — send `adder_grid_ruleset_id` with an id from `available_delivery_tariffs`, e.g. a ComEd Delivery Time-of-Day class. Applies alongside `pricing_source="dynamic"`; `null` clears the selection.
- **Your own DTOD period prices** (dynamic only) — send `delivery_tod_cents` with all four keys `morning`/`midday_peak`/`evening`/`overnight`, prefilled from `available_delivery_tariffs[].period_rates` and edited to match your bill. When set, these prices are used; send `adder_grid_ruleset_id` with them to record which plan the values started from. `null` clears it.
- **Export rates** (solar) — send `export_rates_cents_per_kwh` (48 values) with what your utility pays per half-hour slot for power you send back. Independent of every other pricing field. `null` clears it (no export credit).

### Example — custom hourly rates

```bash
curl -X PUT https://api.hungrymachines.io/api/v1/rates \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pricing_source": "custom", "hourly_rates_cents_per_kwh": [22.0, 22.0, 22.0, 22.0, 22.0, 22.0, 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 28.0, 45.0, 45.0, 45.0, 45.0, 45.0, 28.0, 28.0, 22.0]}'
```

Send the source and the array together. Clear the stored array with `{"hourly_rates_cents_per_kwh": null}`.

### Example — opt into dynamic pricing

```bash
curl -X PUT https://api.hungrymachines.io/api/v1/rates \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pricing_source": "dynamic", "dynamic_zone": "comed", "pricing_adder_cents_per_kwh": 6.5}'
```

### Example — pick a preset zone

```bash
curl -X PUT https://api.hungrymachines.io/api/v1/rates \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pricing_source": "zone", "pricing_location": 7}'
```

### Example — select a tariff from the library

```bash
curl -X PUT https://api.hungrymachines.io/api/v1/rates \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pricing_source": "tariff", "tariff_ruleset_id": 7}'
```

### Example — select a delivery/distribution plan

```bash
curl -X PUT https://api.hungrymachines.io/api/v1/rates \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"adder_grid_ruleset_id": 12}'
```

Send `{"adder_grid_ruleset_id": null}` to clear the selection.

### Example — edit the four DTOD period prices

```bash
curl -X PUT https://api.hungrymachines.io/api/v1/rates \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"adder_grid_ruleset_id": 12, "delivery_tod_cents": {"morning": 4.5, "midday_peak": 11.0, "evening": 3.8, "overnight": 3.0}}'
```

When `delivery_tod_cents` is set, those prices are used; sending `adder_grid_ruleset_id` with it records which plan the values started from. It can also be sent alone; `{"delivery_tod_cents": null}` clears it.

### Example — set export rates

```bash
curl -X PUT https://api.hungrymachines.io/api/v1/rates \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"export_rates_cents_per_kwh": [3.0, 3.0, "... 48 values total ..."]}'
```

Clear it with `{"export_rates_cents_per_kwh": null}`.

### Fields

| Field | Type | Notes |
|---|---|---|
| `hourly_rates_cents_per_kwh` | array[float] \| null | Exactly 24 numbers (`00:00` → `23:00`), each in `[0, 200]`. Send it with `pricing_source: "custom"`. `null` clears the stored array. |
| `pricing_source` | string \| null | `"zone"`, `"custom"`, `"dynamic"`, or `"tariff"` (case-insensitive). `null` clears the explicit selection. Required (here or already stored) when `"custom"`: send `hourly_rates_cents_per_kwh` too unless one is already stored. |
| `dynamic_zone` | string \| null | Slug from `available_dynamic_zones` (case-insensitive). Required (here or already stored) when `pricing_source="dynamic"`. |
| `pricing_adder_cents_per_kwh` | number \| null | Flat per-kWh adder for dynamic pricing, in `[0, 50]`. `null` clears the per-user override. |
| `pricing_location` | integer | Catalog id from `available_pricing_zones`. Has no cleared state: `null` is rejected. |
| `tariff_ruleset_id` | integer \| null | Id from `GET /api/v1/rates/tariffs`. Must reference a tariff that exists and is currently in force. `null` clears the link. Required (here or already stored) when `pricing_source="tariff"`. |
| `adder_grid_ruleset_id` | integer \| null | Id from `available_delivery_tariffs`. Must reference an available delivery/distribution plan. `null` clears the selection. |
| `delivery_tod_cents` | object \| null | Exactly the four keys `morning`/`midday_peak`/`evening`/`overnight`, each a number in `[0, 50]` cents/kWh. `null` clears it. Settable in the same request as `adder_grid_ruleset_id`. |
| `export_rates_cents_per_kwh` | array[float] \| null | Exactly 48 floats (slot 0 = local midnight), each in `[0, 200]`. `null` clears it (no export credit). |

### Response (200)

Same shape as `GET /api/v1/rates`, reflecting the updated state.

### Errors

| Status | Detail | Cause |
|---|---|---|
| 400 | `"hourly_rates_cents_per_kwh must have exactly 24 values"` | Array length is not 24 |
| 400 | `"rate at hour <i> must be between 0 and 200 cents/kWh"` | A value outside `[0, 200]` |
| 400 | `"pricing_source must be one of ['zone', 'custom', 'dynamic', 'tariff']"` | Unknown source |
| 400 | `"pricing_source custom requires hourly_rates_cents_per_kwh"` | `custom` selected with no override sent (or explicitly nulled) and none already stored |
| 400 | `"pricing_source='dynamic' requires dynamic_zone (one of: [...])"` | Dynamic selected without a resolvable zone |
| 400 | `"dynamic_zone must be one of: [...]"` | Unknown `dynamic_zone` slug |
| 400 | `"pricing_adder_cents_per_kwh must be a number between 0 and 50 cents/kWh"` | Adder outside `[0, 50]` |
| 400 | `"pricing_location must be one of the catalog ids: [...]"` | `pricing_location` not in `available_pricing_zones`, or sent as `null` |
| 400 | `"tariff_ruleset_id must be a positive integer id"` | Non-positive id sent |
| 400 | `"tariff_ruleset_id <id> does not exist (browse GET /api/v1/rates/tariffs)"` | Id matches no tariff |
| 400 | `"tariff_ruleset_id <id> is not currently in force"` | Tariff's effective window excludes today |
| 400 | `"pricing_source='tariff' requires tariff_ruleset_id (browse GET /api/v1/rates/tariffs)"` | Tariff selected with no id sent or stored |
| 400 | `"adder_grid_ruleset_id <id> does not reference an available delivery tariff (see available_delivery_tariffs on GET /api/v1/rates)"` | Id doesn't match a delivery/distribution plan |
| 400 | `"delivery_tod_cents must be an object with exactly the keys [...]"` | Payload isn't an object |
| 400 | `"delivery_tod_cents must have exactly the keys [...] (missing [...]; unexpected [...])"` | A key is missing or an unexpected key is present |
| 400 | `"delivery_tod_cents.<key> must be a number between 0 and 50 cents/kWh"` | A period value is a bool, non-numeric, non-finite, or out of range |
| 400 | `"export_rates_cents_per_kwh must have exactly 48 values"` | Array length is not 48 |
| 400 | `"export rate at slot <i> must be between 0 and 200 cents/kWh"` | Value is a bool, non-numeric, non-finite, or out of range |
| 422 | array `detail` | A field of the wrong JSON type (`hourly_rates_cents_per_kwh` not an array of numbers, a non-numeric adder, or a non-integer id) |
| 401 | `"Not authenticated"` | Missing or invalid token |
| 503 | `"Database unavailable"`, `"Failed to update rates configuration"`, `"Failed to validate tariff_ruleset_id"` or `"Failed to validate adder_grid_ruleset_id"` | The database could not be reached or the write failed |

Canonical page: https://docs.hungrymachines.io/api-reference/rates/
