Charge off-peak.
Discharge on-peak.

Home batteries, fleets, and grid-scale BESS — one job, off the tariff clock. Period names and boundaries. Not ¢/kWh. Not a hardcoded 4–9 PM.

Don’t Hardcode Peak Hours

Inverter apps and EMS crons are full of 16:00–21:00. That’s one California weekday in summer. Wrong in winter, on a holiday, and the week the utility refiles the tariff.

Hardcoded
peak_start = time(16, 0)
peak_end   = time(21, 0)

if peak_start <= now.time() < peak_end
   and now.weekday() < 5:
    discharge()

Looks fine in June. Misses season flips, holidays, DST, and every utility that isn’t 4–9.

From the tariff
period = results[0]["period"]

if period == "on_peak":
    raise_reserve()
    discharge()
elif period in ("off_peak", "super_off_peak"):
    charge()

The period is the clock the utility bills. Dispatch follows it.

ZIP → current period → charge / discharge

One REST call for the live period. A forward timeline when you need the next windows. A webhook when the period actually flips.

1

Pass a ZIP

Any U.S. ZIP, optional street if the ZIP is shared. residential by default; commercial or industrial for behind-the-meter BESS.

2

Read the period

off_peak, on_peak, mid_peak, super_off_peak, or critical_peak — plus next_change_at.

3

Dispatch on state

Charge through off-peak. Raise reserve and discharge at peak start. Hold a backup floor if the site needs one.

GET /api/v1/query/current?zip_code=94103
{
"confidence": "unique",
"results": [{
"period": "off_peak",
"next_change_at": "2026-05-16T16:00:00-07:00"
}]
}

One house, a handful of calls a day

Pass the ZIP. When period is off_peak or super_off_peak, charge — from solar leftover or from the grid. When it becomes on_peak, raise reserve and discharge to the house.

Refresh at next_change_at instead of polling. That’s a few calls a day, one location — inside Free (250 calls, 3 locations). The Home Assistant recipe is that loop as sensors.

python · one site
# GET /api/v1/query/current?zip_code=94103
period = results[0]["period"]
next_at = results[0]["next_change_at"]

# charge cheap, discharge expensive — your rates, our clock
if period == "on_peak":
    raise_reserve()
    discharge()
elif period in ("off_peak", "super_off_peak"):
    charge()

refresh_at(next_at)  # do not poll every 15 minutes

Free is current period only. Need tomorrow’s windows? /query/forward starts on Builder (24 h).

Every site has a different clock

A national pack of home batteries, a C&I portfolio, or a grid-scale BESS fleet — same job as the house, at every ZIP you operate. PG&E is not SCE. A summer weekday is not a winter holiday.

Watch locations with a signed tou.period.changed POST. Plan charge and discharge windows with /query/forward (24 h on Builder, 7 days on Growth, 30 days on Business). Unique location = distinct ZIP this billing cycle, not polls.

We still don’t send ¢/kWh. Bill savings, capacity payments, and wholesale bids stay yours. Enterprise is unlimited locations and calls from $1,200/mo, with a dedicated engineer and a contractual uptime SLA.

GET /query/current
Live period + next_change_at per ZIP. customer_class=commercial or industrial for behind-the-meter BESS.
GET /query/forward
Concrete timeline: period, start_at, end_at. Seasons, weekdays, and holidays already applied. Plan the next cycle, don’t re-encode the tariff.
tou.period.changed
Webhook on paid plans. One POST at peak start — and every other boundary — for each watched ZIP. No 15-minute fleet poll.
locations
Builder 75 · Growth 300 · Business 1,500 · Enterprise unlimited. Hitting the cap is a 429, not an overage bill.

Event dispatch still sits on the TOU clock

Called events usually land in the same hours as on-peak. Use the period so a DR event doesn’t empty the pack during off-peak — and so daily TOU dispatch doesn’t fight a called event.

Daily shift first

Charge off-peak, discharge on-peak, every day the tariff says so. That’s the bill. Most event programs pay on top of hours you would discharge anyway.

Hold reserve for a call

When next_change_at is an on-peak start, you already know to be full. A called event then discharges into the same window instead of a surprise drain at 2 PM.

We don’t send DR signals

Utility and aggregator events stay in your stack. tou.tools is the timing layer: period names and boundaries. Pair them. Don’t conflate them.

Frequently asked

  1. How do I dispatch a battery on time-of-use?

    Call GET /api/v1/query/current with the site ZIP. results[0].period is the live period. Charge on off_peak / super_off_peak, discharge on on_peak, and use next_change_at to pre-position. Paid plans add /query/forward for a concrete timeline and webhooks for the flip itself.

  2. Does this return ¢/kWh?

    No. tou.tools is the timing layer: period names and boundaries. Prices live in riders, baselines, and your offtake. Put your own value model on top — bill savings, capacity, wholesale, or a called event.

  3. Which plan for a home battery vs a fleet?

    A house that refreshes at next_change_at fits Free (250 calls/month, 3 locations). Forward schedule starts on Builder (24 hours). Fleets count unique locations — one per ZIP — and want webhooks on paid plans. Business is about 1,500 sites. Unlimited is Enterprise; contact sales.

  4. Is this for virtual power plants and grid-scale BESS?

    Yes. Aggregated home batteries and grid-scale BESS run the same job as a single home battery, at every ZIP you operate. This page is titled for that job, not “VPP” — Variable Peak Pricing is a different tariff name in our hubs.

  5. How do I avoid polling every 15 minutes?

    Refresh at next_change_at, or watch the ZIP with a tou.period.changed webhook on a paid plan. A 15-minute poll is ~2,880 calls per site per month — it exhausts Free, and at fleet scale it chews through Builder too.

  6. Will this work with my utility?

    If we have a seeded TOU plan for the utility that serves the ZIP, yes. See coverage. When a ZIP is ambiguous (common in Texas / ERCOT), the API returns every candidate plan instead of guessing — pick the one on the bill, or pass street / city / state.

Start Free, Or Talk Enterprise

No credit card for a house. Contact sales when the fleet needs unlimited locations and an SLA.