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.
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 resolve 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 | Live day-ahead wholesale pricing for the user’s chosen grid region, plus a flat per-kWh adder for delivery/tax riders. Falls back to the zone curve if data is unavailable — the response then 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 resolve in the tariff’s own currency (minor units per kWh — USD/CAD cents, GBP pence, EUR cents); the dynamic adder does not apply. Falls back to the zone curve when the linked tariff is missing or no longer in force — the response then reports source: "zone" while pricing_source still reads "tariff". California users can skip browsing and send the Rate Identification Number (RIN) printed on their bill (rate_rin): if the RIN’s tariff is already in the library it links immediately; otherwise it links automatically once the tariff lands in the library (refreshed weekly). |
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, "rate_rin": null, "rin_status": null, "available_dynamic_zones": [ { "slug": "comed", "iso": "PJM", "label": "ComEd (Northern Illinois)" }, { "slug": "ameren", "iso": "MISO", "label": "Ameren Illinois (Power Smart Pricing)" } ], "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 ..." ], "unit": "cents/kWh"}The custom, dynamic, and tariff responses use the same shape; only source and the source-specific fields change:
- 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. - tariff —
source: "tariff",tariff_ruleset_idis the linked tariff-library id;rates_cents_per_kwhis in the tariff’s currency (minor units per kWh).rate_rin/rin_statusreport a stored RIN and whether it is"linked"or still"pending".
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 resolved source (cents/kWh, or the tariff currency’s minor units for "tariff") |
source | string | What the resolver actually used this call: "zone", "custom", "dynamic", or "tariff". May differ from pricing_source when dynamic or tariff falls back. |
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, only when source is "custom" |
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 = use the global default. |
tariff_ruleset_id | integer | null | The user’s linked tariff-library id (browse via GET /api/v1/rates/tariffs). null when no tariff linked. |
rate_rin | string | null | The stored Rate Identification Number (California bills), e.g. USCA-SCSC-TOD4-0000. Deprecated — California retail plans are now selectable as dynamic zones; still accepted for back-compat. null when none stored. |
rin_status | string | null | "linked" (a tariff is linked) or "pending" (the RIN is stored but not linked). A "pending" RIN is dormant — select a matching dynamic zone for your plan instead. null when no RIN is stored. |
available_dynamic_zones | array[{slug, iso, label}] | Catalog for the dynamic source. Use slug when sending dynamic_zone; render label; iso is informational. |
available_pricing_zones | array[{id, slug, utility, plan, region, label, notes}] | Catalog of preset plans. Use id when sending pricing_location; render label. |
unit | string | Always "cents/kWh" |
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 401 | "Not authenticated" | Missing or invalid token |
| 503 | "Database not configured" | DB not configured on the server |
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 — rate values resolve through 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). Rates resolve 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 | (validation detail) | limit / offset out of range |
| 503 | "Database not configured" | DB not configured on the server |
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.
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
hourly_rates_cents_per_kwh(24 values), ornullto clear. - 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. - Tariff by RIN (California, deprecated) — California retail plans are now selectable as dynamic zones; pick the one matching your plan instead of entering a RIN.
rate_rinis still accepted: if the RIN’s tariff is already in the library it links immediately (pricing_sourceflips to"tariff"); otherwise the RIN is stored withrin_status: "pending"and stays dormant (no longer auto-linked). An explicitly senttariff_ruleset_idorpricing_sourcein the same request wins over the RIN-derived values;{"rate_rin": null}clears the stored RIN without touching an existing tariff link.
Example — custom hourly override
Section titled “Example — custom hourly override”curl -X PUT https://api.hungrymachines.io/api/v1/rates \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"hourly_rates_cents_per_kwh": [40.0, 40.0, "... 24 values total ..."]}'Clear the override 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 — enter the RIN printed on a California bill
Section titled “Example — enter the RIN printed on a California bill”curl -X PUT https://api.hungrymachines.io/api/v1/rates \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"rate_rin": "USCA-SCSC-TOD4-0000"}'The response’s rin_status reports "linked" or "pending".
Fields
Section titled “Fields”| Field | Type | Notes |
|---|---|---|
hourly_rates_cents_per_kwh | array[float] | null | Exactly 24 floats (00:00 → 23:00), each in [0, 200]. null clears the override. |
pricing_source | string | null | "zone", "custom", "dynamic", or "tariff". null clears the explicit selection. |
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. Validated against the live catalog. |
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". |
rate_rin | string | null | Deprecated — use a matching dynamic zone instead. The Rate Identification Number printed on California bills (four hyphen-separated groups, e.g. USCA-SCSC-TOD4-0000; lowercase is uppercased). Invalid format → 422. Links only if the RIN’s tariff is already in the library, else stored dormant-pending. null or a blank string clears the stored RIN (an existing tariff link is kept). |
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" | Value non-finite or outside [0, 200] |
| 400 | "pricing_source must be one of ['zone', 'custom', 'dynamic', 'tariff']" | Unknown source |
| 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 non-numeric or out of range |
| 400 | "pricing_location must be one of the catalog ids: [...]" | pricing_location not in available_pricing_zones |
| 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 |
| 422 | standard validation error body | rate_rin fails the structural RIN format check |
| 401 | "Not authenticated" | Missing or invalid token |
| 503 | "Database not configured" | DB not configured or write failed |