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 GET requests 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:

base url
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_.

header
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:

curl
# 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.

GET /api/v1/query/current

Query parameters

ParameterTypeDescription
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

200 · application/json
{
  "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

FieldTypeDescription
confidencestringunique, ambiguous, or no_coverage — see Location resolution.
resolved_bystringzip or address. address only when a point lookup actually ran.
as_ofstringThe tz-aware moment the query was evaluated at — your at value, or the current time if omitted. Shared by every result.
results[]arrayOne entry per active rate plan. An empty array is normal when no candidate utility has a seeded plan yet.
periodstringThe active period name — see TOU periods.
next_change_atstringTimestamp when the period next changes, or null if the schedule has no further boundary.
last_verified_atstringWhen the schedule was last cross-checked against the live tariff — a freshness signal.
rate_plan.providerobjectnull 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.

GET /api/v1/query/schedule

Query parameters

ParameterTypeDescription
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

200 · application/json
{
  "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

FieldTypeDescription
period_namestringThe period this rule defines. Any time not covered by a rule is off_peak.
season_start_md
season_end_md
stringMM-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
objectnull 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_weekintegerWeekday bitmask — bit 0 = Monday … bit 6 = Sunday. 31 = Mon–Fri.
time_start
time_end
stringLocal time window (utility timezone). If the end is ≤ the start, the window wraps midnight.
excluded_holidaysarrayHoliday names treated as off-peak; resolved to observed dates at query time.
holidays_as_dowintegernull 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.
priorityintegerWhen 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.

GET /api/v1/query/forward

Query parameters

Takes the same location parameters as /query/currentzip_code (required), customer_class, street, city, state — plus:

ParameterTypeDescription
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:

PlanHorizon
FreeCurrent period only — no forward access.
Builder24 hours
Growth7 days (168 h)
Business30 days (720 h)
EnterpriseCustom

Example response

200 · application/json
{
  "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.

GET /api/v1/coverage
200 · application/json
{
  "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.

GET /api/v1/health
200 · application/json
{ "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.

PeriodMeaning
off_peakLowest-demand window, and the implicit default returned whenever no schedule rule matches the queried moment.
mid_peakShoulder period between off-peak and on-peak, used by tariffs with a three-tier schedule.
on_peakHighest-demand window, typically late afternoon into evening.
super_off_peakDeepest discount window — often overnight or midday solar hours.
critical_peakEvent-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.

provider object
{
  "rep_id": "38a88ef1-4d17-40d0-bdb2-b0f929a51838",
  "name": "Reliant Energy Retail Services",
  "puct_number": 10007,
  "url": "https://www.reliant.com"
}
FieldTypeDescription
rep_iduuidStable identifier for the retail provider.
namestringThe retail provider's name.
puct_numberintegerThe provider's Public Utility Commission of Texas certificate number — its natural key.
urlstringThe 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.

StatusMeaning
400Bad request — e.g. an unknown customer_class, or an at value missing its timezone offset.
401Unauthorized — the Authorization header is missing, or the API key is invalid or revoked.
403Plan 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.
404Not found — e.g. a rate_plan_id that does not exist.
422Validation error — a malformed parameter, such as a non-5-digit zip_code.
429Quota 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.
400 · application/json
{ "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

  1. Create an endpoint with your destination URL. You'll get a signing secret (tousec_…), shown once — store it.
  2. Add the (ZIP, customer_class) locations that endpoint should watch.
  3. 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.)
  4. Your server responds 2xx to 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:

POST body · application/json
{
  "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

HeaderDescription
Tou-SignatureHMAC-SHA256 hex digest of "{timestamp}.{body}", keyed with your endpoint's signing secret.
Tou-TimestampUnix seconds when the request was signed. Reject deliveries whose timestamp is too old to defend against replay.
Tou-Webhook-IdThe 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):

python
# 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:

mcp endpoint
https://tou.tools/mcp

Available tools

ToolArgumentsReturns
get_current_periodzip_codeCurrent period and next-change time for each rate plan serving the ZIP.
get_schedulerate_plan_idFull schedule — all periods, ranges, and seasons.
get_forward_schedulezip_code, hoursResolved period timeline for the coming hours — gated by your plan's forward schedule horizon.
list_utilitiesstateUtilities 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:

mcp server config
{
  "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.

  1. In your client (Claude Web, ChatGPT, etc.), open the connector / custom integration settings and choose Add custom connector (or the equivalent).
  2. Server URL: https://tou.tools/mcp
  3. 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.
  4. 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.