A Clojure client library for the California Energy Commission's MIDAS API, providing access to electricity rate data, GHG emissions signals, Flex Alerts, utility holidays, and reference lookup tables. Built on a non-official OpenAPI spec derived from the CEC's public documentation.
ZonedDateTime end-to-end — every coerced timestamp is a ZonedDateTime in the client's configured zone (default America/Los_Angeles, MIDAS's native zone); DST-aware by construction:midas/raw metadataflex-alert?, flex-alert-active?, ghg? for quick classificationAdd to your deps.edn:
{:deps {energy.grid-coordination/clj-midas {:mvn/version "0.5.0"}}}
(require '[midas.client :as client]
'[midas.entities :as entities])
;; Create an auto-refreshing client (token renews transparently)
(def c (client/create-auto-client "username" "password"))
;; Or manually: acquire token, then create client
(def token-info (client/get-token "username" "password"))
(def c (client/create-client token-info))
;; Fetch rate values for a RIN
(def resp (client/get-rate-values c "USCA-TSTS-TTOU-TEST" "alldata"))
(client/success? resp)
;=> true
;; Coerce to idiomatic Clojure entities
(def rate (entities/rate-info resp))
The MIDAS API exposes five endpoints. clj-midas wraps all of them:
| Function | Endpoint | Description |
|---|---|---|
get-rin-list | GET /ValueData?SignalType= | List available RINs by signal type |
get-rate-values | GET /ValueData?ID=&QueryType= | Fetch rate/price data for a RIN |
get-lookup-table | GET /ValueData?LookupTable= | Fetch reference data (codes & descriptions) |
get-holidays | GET /Holiday | Fetch all utility holidays |
get-historical-list | GET /HistoricalList | List RINs with archived data |
get-historical-data | GET /HistoricalData | Fetch archived rate data by date range |
register | POST /Registration | Register a new API account |
The library provides two views of the API data:
Direct from the JSON — PascalCase keys, string values. Useful for debugging or when you need the exact API representation.
(:body resp)
;=> {:RateID "USCA-TSTS-TTOU-TEST"
; :RateName "CEC TEST24HTOU"
; :RateType "Time of use"
; :ValueInformation [{:ValueName "winter off peak"
; :DateStart "2023-05-01"
; :TimeStart "07:00:00"
; :value 0.1006
; :Unit "$/kWh"} ...]}
Idiomatic Clojure — namespaced keywords, native types. Tick boundaries
(:tick/beginning, :tick/end) and parsed datetimes (:midas.rate/system-time,
:midas.rin/last-updated, …) are ZonedDateTime in the client's configured
zone — see Time and timezones.
(entities/rate-info resp)
;=> #:midas.rate{:id "USCA-TSTS-TTOU-TEST"
; :name "CEC TEST24HTOU"
; :type :midas.rate-type/tou
; :system-time #zoned-date-time "2026-03-19T03:03:46.379-07:00[America/Los_Angeles]"
; :values [#:midas.value{:name "winter off peak"
; :date-start #local-date "2023-05-01"
; :time-start #local-time "07:00"
; :price 0.1006M
; :unit :midas.unit/dollar-per-kwh
; :day-start :midas.day/monday
; :tick/beginning #zoned-date-time "2023-05-01T07:00-07:00[America/Los_Angeles]"
; :tick/end #zoned-date-time "2023-05-01T07:59:59-07:00[America/Los_Angeles]"} ...]}
| Key | Type | Description |
|---|---|---|
:midas.rate/id | String | Rate Identification Number (RIN) |
:midas.rate/name | String | Rate name |
:midas.rate/type | Keyword or String | Rate type (:midas.rate-type/tou, /cpp, /rtp, /ghg, /flex-alert, or passthrough string) |
:midas.rate/system-time | ZonedDateTime | Server timestamp (parsed from SystemTime_UTC, displayed in the client's configured zone) |
:midas.rate/sector | String or nil | Customer sector |
:midas.rate/end-use | String or nil | End use category |
:midas.rate/api-url | String or nil | API URL (literal "None" becomes nil) |
:midas.rate/rate-plan-url | String or nil | Rate schedule URL |
:midas.rate/signup-close | ZonedDateTime or nil | Signup deadline (parsed from a Z-suffixed wire field) |
:midas.rate/values | vector | Vector of ValueData maps |
| Key | Type | Description |
|---|---|---|
:midas.value/name | String | Interval description (e.g. "winter off peak") |
:midas.value/date-start | LocalDate | Interval start date (zone-naive boundary descriptor) |
:midas.value/date-end | LocalDate | Interval end date (zone-naive boundary descriptor) |
:midas.value/day-start | Keyword or nil | Day type (:midas.day/monday ... :midas.day/holiday) |
:midas.value/day-end | Keyword or nil | Day type |
:midas.value/time-start | LocalTime | Interval start time (zone-naive boundary descriptor) |
:midas.value/time-end | LocalTime | Interval end time (zone-naive boundary descriptor) |
:midas.value/price | BigDecimal | Price or emissions value |
:midas.value/unit | Keyword or String | Unit (:midas.unit/dollar-per-kwh, /kg-co2-per-kwh, /event, etc.) |
:tick/beginning | ZonedDateTime | Interval start as a moment-in-time (composed from date-start + time-start in the configured zone). Present when both date-start and time-start are non-nil. |
:tick/end | ZonedDateTime | Interval end as a moment-in-time. Present when both date-end and time-end are non-nil. |
| Key | Type | Description |
|---|---|---|
:midas.rin/id | String | Rate Identification Number |
:midas.rin/signal-type | Keyword or nil | Signal type (:midas.signal-type/rates, /ghg, /flex-alert) |
:midas.rin/description | String | Human-readable description |
:midas.rin/last-updated | ZonedDateTime or nil | Last data update (bare wire field — interpreted as wall-clock in the configured zone) |
| Key | Type | Description |
|---|---|---|
:midas.holiday/energy-code | String | Two-character provider code |
:midas.holiday/energy-name | String | Provider name |
:midas.holiday/date | LocalDate | Holiday date |
:midas.holiday/description | String | Holiday name |
| Key | Type | Description |
|---|---|---|
:midas.lookup/code | String | Upload code |
:midas.lookup/description | String | Human-readable description |
| Key | Type | Description |
|---|---|---|
:midas.rin/country | String | Country code (e.g. "US") |
:midas.rin/state | String | State code (e.g. "CA") |
:midas.rin/distribution | String | Distribution utility code (e.g. "PG") |
:midas.rin/energy | String | Energy provider code (e.g. "PG") |
:midas.rin/rate | String | Rate schedule identifier (e.g. "TOU4") |
:midas.rin/location | String | Location identifier (e.g. "0000") |
:midas.rin/distribution-name | String or absent | Human-readable distribution utility name (added by annotate-rin) |
:midas.rin/energy-name | String or absent | Human-readable energy provider name (added by annotate-rin) |
| Raw (API) | Coerced (Clojure) | Example |
|---|---|---|
| ISO date string | java.time.LocalDate | "2023-05-01" → #local-date "2023-05-01" |
| Time string | java.time.LocalTime | "07:00:00" → #local-time "07:00" |
| ISO datetime with Z | java.time.ZonedDateTime (instant preserved, displayed in client's zone) | "2026-03-19T10:03:46.379Z" → #zoned-date-time "2026-03-19T03:03:46.379-07:00[America/Los_Angeles]" |
| Datetime without TZ | java.time.ZonedDateTime (wall-clock attached to client's zone) | "2023-06-07T15:57:48.023" → #zoned-date-time "2023-06-07T15:57:48.023-07:00[America/Los_Angeles]" |
| date + time → tick | java.time.ZonedDateTime | ("2023-05-01", "07:00:00") → #zoned-date-time "2023-05-01T07:00-07:00[America/Los_Angeles]" |
| Number | BigDecimal | 0.1006 → 0.1006M |
| Rate type string | Namespaced keyword | "Time of use" → :midas.rate-type/tou |
| Unit string | Namespaced keyword | "$/kWh" → :midas.unit/dollar-per-kwh |
| Day string | Namespaced keyword | "Monday" → :midas.day/monday |
MIDAS is the California Energy Commission's API and uses America/Los_Angeles as its native zone. The wire format mixes UTC fields (those whose names end in _UTC or whose ISO 8601 strings carry a Z suffix) with bare wall-clock fields (everything else). The CEC does not document this; it has been verified empirically — see midas-api-specs/doc/datetime-and-timezone.md.
clj-midas normalises both styles to a single in-memory representation:
Every coerced datetime is a
java.time.ZonedDateTimein the client's configured zone.
The default zone is America/Los_Angeles; override with :zone at client construction:
;; default — bare datetimes interpreted as PT, Z fields preserved instant-wise
(def c (client/create-auto-client "user" "pass"))
;; override — accepts a ZoneId or a zone-id string
(def c-utc (client/create-auto-client "user" "pass" {:zone "UTC"}))
(def c-pt (client/create-auto-client "user" "pass" {:zone (java.time.ZoneId/of "America/Los_Angeles")}))
| Wire field | Wire form | Coerced as |
|---|---|---|
SystemTime_UTC, SignupCloseDate | ISO 8601 with Z (UTC) | Parsed as OffsetDateTime, re-expressed in the configured zone via .atZoneSameInstant (instant preserved). |
LastUpdated, DateOfHoliday, DateStart+TimeStart, DateEnd+TimeEnd | Bare ISO 8601 / time / date (no zone) | Parsed as LocalDateTime/LocalTime/LocalDate and attached to the configured zone (treated as wall-clock in that zone). |
ZonedDateTime (and not Instant or OffsetDateTime)ZonedDateTime carries an IANA zone (America/Los_Angeles) that knows DST rules. .plusDays(1) on a PT timestamp lands on the next PT midnight regardless of spring-forward/fall-back. OffsetDateTime carries a fixed offset and gets DST math wrong; Instant discards wall-clock semantics entirely. For operating-day arithmetic — TOU period boundaries, billing days, "next 24 hours" — ZonedDateTime is the only correct type.
This library is part of an ecosystem-wide ZonedDateTime end-to-end discipline.
:midas.value/date-start, :date-end, :time-start, :time-end are deliberately kept as LocalDate / LocalTime — they describe boundary clock-times of a tariff schedule (e.g. "this rate is in effect from PT-clock-time 07:00 to 21:59:59"), not moments in time. The moment-in-time view is provided as :tick/beginning and :tick/end (ZonedDateTime), which compose date+time+zone.
:midas/zone on responsesEach client/get-* fn annotates its response with :midas/zone — the coercion helpers (entities/rate-info, entities/rin-list, entities/historical-list, entities/historical-data) read it during coercion. If you call those helpers on a synthetic response that lacks :midas/zone, they throw a clear error; for direct coercion you can call the lower-level (entities/->rate-info raw zone) etc. with an explicit ZoneId.
MIDAS uses a two-step auth flow:
GET /Token returns a bearer token (valid for 10 minutes);; Manual token management
(def token-info (client/get-token "user" "pass"))
;=> {:token "eyJ..." :acquired-at #inst "..." :expires-at #inst "..."}
(client/token-expired? token-info)
;=> false
;; Auto-refreshing client (recommended)
(def c (client/create-auto-client "user" "pass"))
;; Token refreshes transparently — no manual management needed
Parse a Rate Identification Number into its component fields:
(entities/parse-rin "USCA-PGPG-TOU4-0000")
;=> {:midas.rin/country "US"
; :midas.rin/state "CA"
; :midas.rin/distribution "PG"
; :midas.rin/energy "PG"
; :midas.rin/rate "TOU4"
; :midas.rin/location "0000"}
;; Returns nil for invalid RINs
(entities/parse-rin "not-a-rin")
;=> nil
Add human-readable labels from MIDAS lookup tables:
;; Fetch lookup tables once
(def dist-table (entities/lookup-table (client/get-lookup-table c "Distribution")))
(def energy-table (entities/lookup-table (client/get-lookup-table c "Energy")))
(def lookups {"Distribution" dist-table, "Energy" energy-table})
(entities/annotate-rin (entities/parse-rin "USCA-SDEA-TTOU-0000") lookups)
;=> {:midas.rin/country "US"
; :midas.rin/state "CA"
; :midas.rin/distribution "SD"
; :midas.rin/distribution-name "San Diego Gas and Electric"
; :midas.rin/energy "EA"
; :midas.rin/energy-name "Clean Energy Alliance"
; :midas.rin/rate "TTOU"
; :midas.rin/location "0000"}
;; Detect signal type from rate data
(entities/ghg? rate) ;=> true/false (checks rate-type + unit)
(entities/flex-alert? rate) ;=> true/false
(entities/flex-alert-active? rate) ;=> true if any value > 0
Every coerced entity preserves the original API data as metadata:
(def rate (entities/rate-info resp))
;; Access the original API response
(-> rate meta :midas/raw)
;=> {:RateID "USCA-TSTS-TTOU-TEST" :RateName "CEC TEST24HTOU" ...}
;; Works at every level
(-> rate :midas.rate/values first meta :midas/raw)
;=> {:ValueName "winter off peak" :DateStart "2023-05-01" ...}
Malli schemas are published in dedicated namespaces.
midas.entities.schema — Coerced entities (the public contract)(require '[midas.entities.schema :as schema]
'[malli.core :as m])
(m/validate schema/RateInfo rate) ;=> true
(m/validate schema/ValueData value) ;=> true
;; Available: RateInfo, ValueData, RinListEntry, ParsedRin, Holiday,
;; LookupEntry, RateType, SignalType, DayType, UnitType
midas.entities.schema.raw — Raw API shapes(require '[midas.entities.schema.raw :as raw])
(raw/validate-raw-rate-info (:body resp)) ;=> nil (valid)
(raw/validate-raw-value-data value) ;=> nil or Malli explanation
;; Available: RateInfo, ValueData, RinListEntry, HolidayEntry, LookupEntry
midas.client| Function | Description |
|---|---|
get-token | Authenticate with HTTP Basic, returns token-info map |
token-expired? | Check if a token-info is expired (with 30s buffer) |
create-client | Create client with a token string or token-info map |
create-auto-client | Create client with auto-refreshing token |
token-info | Get current token-info from a client |
success? | True if HTTP response is 2xx |
body | Extract parsed body from response |
get-rin-list | List RINs by signal type (0=All, 1=Rates, 2=GHG, 3=Flex Alert) |
get-rate-values | Fetch rate data for a RIN ("alldata" or "realtime") |
get-lookup-table | Fetch a reference table (Distribution, Energy, Unit, etc.) |
get-holidays | Fetch all utility holidays |
get-historical-list | List RINs with archived data for a provider pair |
get-historical-data | Fetch archived rate data for a RIN and date range |
register | Register a new MIDAS account (auto base64 encodes fields) |
routes | List available Martian route names |
midas.entities| Function | Description |
|---|---|
->rate-info | Coerce a raw RateInfo map (takes raw, zone) |
->value-data | Coerce a raw ValueData map (takes raw, zone) |
->rin-list-entry | Coerce a raw RIN list entry (takes raw, zone) |
->holiday | Coerce a raw HolidayEntry |
->lookup-entry | Coerce a raw LookupEntry |
rate-info | Extract + coerce rate info from HTTP response |
rin-list | Extract + coerce RIN list from HTTP response |
holidays | Extract + coerce holidays from HTTP response |
historical-list | Extract + coerce + deduplicate historical RIN list |
historical-data | Extract + coerce historical rate data |
lookup-table | Extract + coerce lookup table entries |
ghg? | True if rate-info is a GHG signal |
parse-rin | Parse a RIN string into its component fields |
annotate-rin | Add human-readable labels from lookup tables to a parsed RIN |
flex-alert? | True if rate-info is a Flex Alert |
flex-alert-active? | True if a Flex Alert is currently active |
(require '[midas.client :as client]
'[midas.entities :as entities]
'[midas.entities.schema :as schema]
'[malli.core :as m])
;; Create auto-refreshing client
(def c (client/create-auto-client
(System/getenv "MIDAS_USERNAME")
(System/getenv "MIDAS_PASSWORD")))
;; List all rate RINs
(def rin-resp (client/get-rin-list c 1))
(def rins (entities/rin-list rin-resp))
(count rins)
;=> 67266
;; Fetch the test TOU rate
(def rate-resp (client/get-rate-values c "USCA-TSTS-TTOU-TEST" "alldata"))
(def rate (entities/rate-info rate-resp))
(:midas.rate/type rate)
;=> :midas.rate-type/tou
(count (:midas.rate/values rate))
;=> 768
;; Inspect a value interval
(first (:midas.rate/values rate))
;=> #:midas.value{:name "winter off peak"
; :date-start #local-date "2023-05-01"
; :time-start #local-time "07:00"
; :time-end #local-time "07:59:59"
; :price 0.1006M
; :unit :midas.unit/dollar-per-kwh
; :day-start :midas.day/monday
; :day-end :midas.day/monday}
;; Validate against schema
(m/validate schema/RateInfo rate)
;=> true
;; Check Flex Alert status
(def flex-resp (client/get-rate-values c "USCA-FLEX-FXRT-0000" "realtime"))
(def flex (entities/rate-info flex-resp))
(entities/flex-alert? flex)
;=> true
(entities/flex-alert-active? flex)
;=> false (no active alert)
;; Lookup tables
(def dists (entities/lookup-table (client/get-lookup-table c "Distribution")))
(first dists)
;=> #:midas.lookup{:code "BN" :description "Banning"}
;; Holidays
(first (entities/holidays (client/get-holidays c)))
;=> #:midas.holiday{:energy-code "SD"
; :energy-name "San Diego Gas and Electric"
; :date #local-date "2023-02-20"
; :description "President's Day"}
;; Access raw API data via metadata
(-> rate meta :midas/raw :RateType)
;=> "Time of use"
clojure -M:nrepl
# nREPL port written to .nrepl-port
Requires MIDAS_USERNAME and MIDAS_PASSWORD environment variables.
The dev/user.clj namespace provides REPL convenience functions:
(start!) ; create auto-refreshing client from env vars
# Unit tests only (fast, no network)
clojure -M:test -m kaocha.runner --focus :unit
# Integration tests (hits live API, needs credentials)
clojure -M:test -m kaocha.runner --focus :integration
# All tests
clojure -M:test -m kaocha.runner
MIT License -- Copyright (c) 2026 Clark Communications Corporation
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |