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
Section titled “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
Section titled “GET /api/v1/rates”Auth: Required (Bearer JWT)
Example
Section titled “Example”curl https://api.hungrymachines.io/api/v1/rates \ -H "Authorization: Bearer YOUR_TOKEN"Response (200) — zone
Section titled “Response (200) — zone”{ "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_kwhholds the raw 24-element override. - dynamic —
source: "dynamic",dynamic_zoneis the chosen slug,pricing_adder_cents_per_kwhis the per-user adder,adder_grid_ruleset_idis the selected delivery/distribution plan (if any),delivery_tod_centsis the user’s own four period prices if set (used in place of the selected plan’s published rates). - tariff —
source: "tariff",tariff_ruleset_idis the linked tariff-library id;rates_cents_per_kwhis in the tariff’s currency (minor units per kWh).
Response fields
Section titled “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
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 401 | "Not authenticated" |
Missing or invalid token |
GET /api/v1/rates/tariffs
Section titled “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
Section titled “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
Section titled “Example”curl "https://api.hungrymachines.io/api/v1/rates/tariffs?country=US&q=tou" \ -H "Authorization: Bearer YOUR_TOKEN"Response (200)
Section titled “Response (200)”{ "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
Section titled “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
Section titled “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
Section titled “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
Section titled “Required vs. optional”All fields are optional. What you send depends on the source you want:
- Custom rates — send
pricing_source: "custom"together withhourly_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 adynamic_zone(required here or already stored).pricing_adder_cents_per_kwhis optional. - Preset zone — send
pricing_source: "zone"plus apricing_locationid. - Tariff — send
pricing_source: "tariff"plus atariff_ruleset_id(required here or already stored) fromGET /api/v1/rates/tariffs. Sendingtariff_ruleset_idalone links the tariff without changing the source;nullclears the link. - Delivery/distribution plan (dynamic only) — send
adder_grid_ruleset_idwith an id fromavailable_delivery_tariffs, e.g. a ComEd Delivery Time-of-Day class. Applies alongsidepricing_source="dynamic";nullclears the selection. - Your own DTOD period prices (dynamic only) — send
delivery_tod_centswith all four keysmorning/midday_peak/evening/overnight, prefilled fromavailable_delivery_tariffs[].period_ratesand edited to match your bill. When set, these prices are used; sendadder_grid_ruleset_idwith them to record which plan the values started from.nullclears 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.nullclears it (no export credit).
Example — custom hourly rates
Section titled “Example — custom hourly rates”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
Section titled “Example — opt into dynamic pricing”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
Section titled “Example — pick a preset zone”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
Section titled “Example — select a tariff from the library”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
Section titled “Example — select a delivery/distribution plan”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
Section titled “Example — edit the four DTOD period prices”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
Section titled “Example — set export rates”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
Section titled “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)
Section titled “Response (200)”Same shape as GET /api/v1/rates, reflecting the updated state.
Errors
Section titled “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 |