Introduction
tou.tools resolves a U.S. address or ZIP code to its electric utility, then reports the active Time-of-Use (TOU) period — on-peak, off-peak, and everything between — along with when the next period begins.
The scope is deliberately narrow: period names, boundaries, and schedules only. tou.tools does not return ¢/kWh prices — those vary by tariff rider, customer class, and baseline allowance, and are out of scope. What you get is the timing layer: which period is active, and exactly when it changes.
Two ways to integrate
- REST API — plain HTTP
GETrequests returning JSON. Best for apps, dashboards, and backend services. - MCP server — a Model Context Protocol endpoint that AI agents can call as a native tool, with no integration code. See MCP integration.
Base URL
All REST endpoints are served under a single versioned base path:
https://tou.tools/api/v1
Authentication
Requests authenticate with an API key passed as a bearer token in the
Authorization header. Keys are issued from your dashboard
under API keys, and are prefixed tou_.
Authorization: Bearer tou_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
The raw key is shown once, at creation — store it
somewhere safe. Every /query/* request and every MCP
call must carry a valid key; requests without one are rejected
with 401 Unauthorized.
Quickstart
Ask whether power is peak or off-peak at a ZIP code right now. This is the single most common call:
# current period for ZIP 94103 (San Francisco) curl https://tou.tools/api/v1/query/current?zip_code=94103 \ -H "Authorization: Bearer tou_YOUR_API_KEY"
The response lists every active TOU rate plan for that location, each with its current period and the timestamp of the next change. See Current period for the full shape.
Current period
Returns the live TOU period for every active rate plan serving a location. If the ZIP maps to more than one utility, every candidate is returned — tou.tools never silently guesses.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| zip_code | string · required | 5-digit U.S. ZIP code. |
| customer_class | string | One of residential, commercial, industrial, agricultural, ev. Defaults to residential. |
| at | string | ISO 8601 timestamp, must include a timezone offset. Defaults to the current time (UTC). Use it to ask "what period applies at this moment?" Past timestamps are unrestricted; future ones may reach up to your plan's forward schedule horizon (plus a one-hour grace) — beyond that the call returns 403. |
| street | string | Street address. When supplied, triggers address-level disambiguation if the ZIP alone is ambiguous (see Location resolution). |
| city | string | Optional address component, used alongside street. |
| state | string | Two-letter U.S. state code, used alongside street. |
Example response
{
"zip_code": "94103",
"confidence": "unique",
"resolved_by": "zip",
"customer_class": "residential",
"as_of": "2026-05-16T11:30:00-07:00",
"results": [
{
"rate_plan": {
"id": "8f3a2c10-4b6d-4e9a-9f12-7c5e1d8a0b34",
"utility_number": 14328,
"utility_name": "Pacific Gas & Electric Co",
"code": "E-TOU-C",
"name": "Residential Time-of-Use",
"customer_class": "residential",
"timezone": "America/Los_Angeles",
"is_closed": false,
"effective_from": "2024-01-01",
"effective_to": null,
"last_verified_at": "2026-04-30T18:22:00Z",
"provider": null
},
"period": "off_peak",
"next_change_at": "2026-05-16T16:00:00-07:00"
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
| confidence | string | unique, ambiguous, or no_coverage — see Location resolution. |
| resolved_by | string | zip or address. address only when a point lookup actually ran. |
| as_of | string | The tz-aware moment the query was evaluated at — your at value, or the current time if omitted. Shared by every result. |
| results[] | array | One entry per active rate plan. An empty array is normal when no candidate utility has a seeded plan yet. |
| period | string | The active period name — see TOU periods. |
| next_change_at | string | Timestamp when the period next changes, or null if the schedule has no further boundary. |
| last_verified_at | string | When the schedule was last cross-checked against the live tariff — a freshness signal. |
| rate_plan.provider | object | null for a regulated utility tariff (the common case). On a competitive Texas (ERCOT) retail plan it carries the retailer that authors the plan — { rep_id, name, puct_number, url } — and the sibling utility_number/utility_name then identify the delivering wires company (TDU), not the seller. See Retail providers. |
Full schedule
Returns the complete rule set for a single rate plan — every period window, season bound, and weekday mask. Use it to render a calendar or precompute upcoming periods client-side.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| rate_plan_id | uuid · required | The rate plan's id, as returned in any rate_plan object from /query/current. |
| hours | integer | Optional. When supplied, the response also carries a forward block — the plan's schedule resolved into a concrete period timeline covering the next hours hours. Gated by your plan's forward schedule horizon; see Forward schedule. Omit for the rule set only (available on every plan). |
Example response
{
"rate_plan": { /* same shape as in /query/current */ },
"rules": [
{
"period_name": "on_peak",
"season_start_md": "06-01",
"season_end_md": "09-30",
"season_start_anchor": null,
"season_end_anchor": null,
"days_of_week": 31,
"time_start": "16:00:00",
"time_end": "21:00:00",
"excluded_holidays": ["Independence Day"],
"holidays_as_dow": null,
"priority": 0
}
]
}
Rule fields
| Field | Type | Description |
|---|---|---|
| period_name | string | The period this rule defines. Any time not covered by a rule is off_peak. |
| season_start_md season_end_md | string | MM-DD season bounds, or null when that side of the season is anchored (see below). If the end falls before the start, the season wraps the year-end. |
| season_start_anchor season_end_anchor | object | null for a fixed-date bound. Otherwise a floating "Nth weekday of a month" boundary, resolved per year — { month, dow, n, offset_days } where dow is 0 = Monday … 6 = Sunday, n is the occurrence (1–5, or -1 for the last in the month), and offset_days shifts the resolved date. Exactly one of season_*_md / season_*_anchor is set per side. Used by tariffs that track DST or holiday weeks. |
| days_of_week | integer | Weekday bitmask — bit 0 = Monday … bit 6 = Sunday. 31 = Mon–Fri. |
| time_start time_end | string | Local time window (utility timezone). If the end is ≤ the start, the window wraps midnight. |
| excluded_holidays | array | Holiday names treated as off-peak; resolved to observed dates at query time. |
| holidays_as_dow | integer | null in the common case. When set, the dates in excluded_holidays are not skipped — instead, on those dates the rule's weekday match uses this bitmask in place of the actual weekday (e.g. treat holidays as a weekend). A common California IOU pattern. |
| priority | integer | When two rules both match a moment, the one with the higher priority wins. Defaults to 0. |
Forward schedule
Resolves the schedule into a concrete period timeline
for every active rate plan at a location — contiguous segments, each
with a period name and exact start/end timestamps, covering the next
hours hours. Seasons, weekday masks, holidays, and
priorities are already applied, so a battery dispatcher or EV charger
can plan against it directly instead of re-implementing the rules from
/query/schedule.
Query parameters
Takes the same location parameters as
/query/current —
zip_code (required), customer_class,
street, city, state — plus:
| Parameter | Type | Description |
|---|---|---|
| hours | integer | Forward window in hours, starting now. Defaults to your plan's full horizon. Requests beyond the horizon return 403 — the window is never silently clamped. |
Plan horizons
How far ahead you can resolve is set by your plan — the forward schedule horizon:
| Plan | Horizon |
|---|---|
| Free | Current period only — no forward access. |
| Builder | 24 hours |
| Growth | 7 days (168 h) |
| Business | 30 days (720 h) |
| Enterprise | Custom |
Example response
{
"zip_code": "94103",
"confidence": "unique",
"resolved_by": "zip",
"customer_class": "residential",
"as_of": "2026-06-11T09:00:00Z",
"until": "2026-06-12T09:00:00Z",
"hours": 24,
"results": [
{
"rate_plan": { /* same shape as in /query/current */ },
"timeline": [
{ "period": "off_peak", "start_at": "2026-06-11T02:00:00-07:00", "end_at": "2026-06-11T16:00:00-07:00" },
{ "period": "on_peak", "start_at": "2026-06-11T16:00:00-07:00", "end_at": "2026-06-11T21:00:00-07:00" },
{ "period": "off_peak", "start_at": "2026-06-11T21:00:00-07:00", "end_at": "2026-06-12T02:00:00-07:00" }
]
}
]
}
Segments tile the window exactly: the first starts at
as_of, the last ends at until, and each
segment begins where the previous one ended (timestamps are returned
in the rate plan's local timezone). The same timeline is available
for a single known plan via
/query/schedule's
hours parameter.
An over-horizon request returns 403 with
code: "horizon_exceeded" and a next_tier
hint naming the lowest plan whose horizon covers it. Like all
rejected calls, a 403 never counts against your
call quota.
Coverage
Lists every utility with at least one active rate plan, the states it serves, and which customer classes are covered. Public — no key required.
{
"summary": {
"total_utilities": 1000,
"total_states": 51,
"total_rate_plans": 0,
"last_verified_at": "2026-04-30T18:22:00Z"
},
"utilities": [ /* one object per covered utility */ ]
}
Health
A liveness probe — no auth, no parameters.
{ "status": "ok" }
TOU periods
Every period in a response is one of the names below.
off_peak is the implicit default — it is returned whenever
no schedule rule matches the queried moment.
| Period | Meaning |
|---|---|
| off_peak | Lowest-demand window, and the implicit default returned whenever no schedule rule matches the queried moment. |
| mid_peak | Shoulder period between off-peak and on-peak, used by tariffs with a three-tier schedule. |
| on_peak | Highest-demand window, typically late afternoon into evening. |
| super_off_peak | Deepest discount window — often overnight or midday solar hours. |
| critical_peak | Event-based critical-peak window declared on a limited number of days. |
Customer classes
A single ZIP may have different TOU schedules for a home, a storefront,
and a factory. The customer_class parameter selects which
set of rate plans to evaluate.
residential— homes and apartments. The default.commercial— offices, retail, and other small-to-medium businesses.industrial— large-load industrial sites.agricultural— farms and irrigation accounts.ev— dedicated electric-vehicle charging rate plans.
Retail providers
In most of the U.S., one regulated utility both delivers power and
sets the TOU schedule, so every rate_plan.provider is
null. In deregulated Texas (ERCOT), the wires company
(the "TDU" — e.g. Oncor, CenterPoint) only delivers, while a
competitive Retail Electric Provider (REP) authors the plan and its
TOU schedule. For those plans provider is populated and
the utility_number/utility_name identify
the delivering TDU, not the seller.
{
"rep_id": "38a88ef1-4d17-40d0-bdb2-b0f929a51838",
"name": "Reliant Energy Retail Services",
"puct_number": 10007,
"url": "https://www.reliant.com"
}
| Field | Type | Description |
|---|---|---|
| rep_id | uuid | Stable identifier for the retail provider. |
| name | string | The retail provider's name. |
| puct_number | integer | The provider's Public Utility Commission of Texas certificate number — its natural key. |
| url | string | The provider's website, or null. |
Location resolution
U.S. electric service territories do not follow ZIP boundaries. A ZIP
can fall entirely inside one utility, straddle two, or land in an area
with no mapped retail utility. tou.tools reports this honestly with the
confidence field:
unique— the ZIP maps to exactly one utility.ambiguous— the ZIP overlaps multiple utilities; every candidate is returned.no_coverage— no mapped retail utility for that ZIP.
When a ZIP is ambiguous, pass street (plus
optionally city and state) and tou.tools will
geocode the address to narrow candidates to the territory that actually
contains the point. The result's resolved_by becomes
address when that lookup ran. If geocoding fails or the
point lands outside every candidate territory, the full ambiguous
result is returned unchanged — coverage is never silently dropped.
Errors
Errors use standard HTTP status codes with a JSON body carrying a
detail message.
| Status | Meaning |
|---|---|
| 400 | Bad request — e.g. an unknown customer_class, or an at value missing its timezone offset. |
| 401 | Unauthorized — the Authorization header is missing, or the API key is invalid or revoked. |
| 403 | Plan gate — the request asks for a feature beyond your plan, e.g. a forward window past your forward schedule horizon. The detail carries code: "horizon_exceeded" and the lowest tier that covers the request. |
| 404 | Not found — e.g. a rate_plan_id that does not exist. |
| 422 | Validation error — a malformed parameter, such as a non-5-digit zip_code. |
| 429 | Quota exceeded — you've reached your plan's monthly call cap, or a query to a new location would exceed your location cap. The detail names the limit and the date your billing cycle resets. |
{ "detail": "`at` must include a timezone offset" }
Plan limits are counted per billing cycle and reset on your
signup anniversary — see plans & limits.
Over MCP there is no HTTP status: a quota-blocked tool call
returns the same message in an { "error": … } object.
Period-change events
Instead of polling /query/current
on a timer, register a webhook endpoint and tou.tools
will POST a signed event to your server the moment a TOU
period changes for a location you watch — the off-peak → on-peak flip,
and every other boundary, pushed as it happens.
Webhooks are a paid feature. Each plan caps the
number of endpoints (destination URLs) you can register:
3 on Builder, 10 on Growth,
25 on Business, unlimited on Enterprise. Each endpoint
can watch any number of locations within your plan. Set them up on
the Webhooks dashboard.
How it works
- Create an endpoint with your destination URL. You'll get a signing secret (
tousec_…), shown once — store it. - Add the
(ZIP, customer_class)locations that endpoint should watch. - tou.tools evaluates each watch on a schedule. When a plan's period changes versus the last value it saw, it sends an event. (The first observation of each plan is recorded silently — you only get events on real transitions.)
- Your server responds
2xxto acknowledge. Non-2xx or a timeout is retried with backoff: immediately, then 1 min, 5 min, 30 min, 2 h, after which the delivery is marked failed.
Event payload
Each delivery is a single tou.period.changed event for one
rate plan at one watched location:
{
"id": "evt_8f3a2c104b6d4e9a9f12",
"type": "tou.period.changed",
"created_at": "2026-05-16T16:00:00-07:00",
"zip_code": "94103",
"customer_class": "residential",
"previous_period": "off_peak",
"period": "on_peak",
"next_change_at": "2026-05-16T21:00:00-07:00",
"rate_plan": {
"id": "8f3a2c10-4b6d-4e9a-9f12-7c5e1d8a0b34",
"utility_number": 14328,
"utility_name": "Pacific Gas & Electric Co",
"code": "E-TOU-C",
"name": "Residential Time-of-Use"
}
}
A watched ZIP that resolves to several rate plans sends one event per
plan that changed. previous_period / period
use the same vocabulary as the REST API — see
TOU periods.
Request headers
| Header | Description |
|---|---|
| Tou-Signature | HMAC-SHA256 hex digest of "{timestamp}.{body}", keyed with your endpoint's signing secret. |
| Tou-Timestamp | Unix seconds when the request was signed. Reject deliveries whose timestamp is too old to defend against replay. |
| Tou-Webhook-Id | The event id, echoed for idempotency — deliveries can be retried, so dedupe on this. |
Verifying a delivery
Recompute the signature with your stored secret and compare it to the
Tou-Signature header (use a constant-time comparison):
# Flask-style handler import hashlib, hmac, time SECRET = "tousec_…" def verify(request): ts = request.headers["Tou-Timestamp"] sig = request.headers["Tou-Signature"] body = request.get_data(as_text=True) # raw bytes/string, unmodified # reject anything older than 5 minutes if abs(time.time() - int(ts)) > 300: return False expected = hmac.new( SECRET.encode(), f"{ts}.{body}".encode(), hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, sig)
Sign over the raw request body, before any JSON re-serialization — re-encoding can reorder keys or change spacing and break the signature. Every delivery attempt is logged on the Webhooks dashboard with its status, HTTP code, and attempt count.
MCP integration
tou.tools ships as a Model Context Protocol server, so any MCP-capable AI client — Claude Code, Cursor, Windsurf, Cline, Continue, Zed, Claude Desktop, ChatGPT, and others — can query TOU data as a native tool with no SDK or glue code. The server speaks streamable HTTP at:
https://tou.tools/mcp
Available tools
| Tool | Arguments | Returns |
|---|---|---|
| get_current_period | zip_code | Current period and next-change time for each rate plan serving the ZIP. |
| get_schedule | rate_plan_id | Full schedule — all periods, ranges, and seasons. |
| get_forward_schedule | zip_code, hours | Resolved period timeline for the coming hours — gated by your plan's forward schedule horizon. |
| list_utilities | state | Utilities with TOU rate plans in that state. |
All four tools are live, backed by the same resolver as the REST API. Two ways to authenticate — pick whichever your client supports.
Option A — API key (desktop and CLI clients)
For local clients — Claude Code, Cursor, Windsurf, Cline,
Continue, Zed, Claude Desktop, and similar — connect with an
Authorization: Bearer tou_… header (the same API
key you use for REST). Mint one on the
API Keys page,
then add tou.tools to your client's MCP server config.
Most clients accept the standard mcpServers JSON
shape below:
{
"mcpServers": {
"tou": {
"url": "https://tou.tools/mcp",
"headers": { "Authorization": "Bearer tou_…" }
}
}
}
Where this config lives differs by client — check your client's
MCP docs for the right file or UI (e.g.
~/.cursor/mcp.json for Cursor,
claude_desktop_config.json for Claude Desktop, the
in-app Settings panel for Windsurf, an
.mcp.json in your project root for Claude Code).
The block above is portable across all of them.
Option B — OAuth 2.1 (browser-hosted clients)
Browser-hosted clients like Claude Web and ChatGPT only accept MCP servers behind OAuth 2.1 with discovery. Add tou.tools as a custom connector and the client handles the full flow — discovery, dynamic client registration, PKCE, the consent screen, token refresh. You just point and sign in.
- In your client (Claude Web, ChatGPT, etc.), open the connector / custom integration settings and choose Add custom connector (or the equivalent).
- Server URL:
https://tou.tools/mcp - The client redirects you to tou.tools to sign in (use your dashboard email + password). You'll see a consent screen showing the four tools — click Allow.
- Done. The four tools appear in your client's tool list and bill against your account just like REST calls do.
Access tokens are 1-hour JWTs with audience bound to
https://tou.tools/mcp; refresh tokens are 30 days and
rotate on every use. The discovery documents live at
/.well-known/oauth-protected-resource and
/.well-known/oauth-authorization-server.
Ready to build? Get a free API key or review plans & limits.