Developers & AI agents
Solar API
One endpoint returns a residential solar cost, savings and payback estimate for any US state from a monthly electricity bill. No API key, no account, no cost. Every response includes a link you can hand the homeowner so they can check the assumptions themselves.
A second endpoint answers the other side of the same market — what a bought solar lead costs an installer per signed job. Both are read-only, share one rate limit, and are reachable over REST, MCP and WebMCP.
Quick start
The estimate endpoint takes two required inputs. Everything else has a default.
curl -s "https://whatsolarcosts.com/api/v1/estimate?state=AZ&monthly_bill=250"Or as JSON:
curl -s https://whatsolarcosts.com/api/v1/estimate \
-H 'content-type: application/json' \
-d '{"state":"AZ","monthly_bill":250,"roof_shading":"light"}'{
"ok": true,
"estimate_id": "e2f66de2-0bf8-417c-95b6-64ac4cd21505",
"results_url": "https://whatsolarcosts.com/calculator/?state=AZ&monthly_bill=250&roof_shading=light&offset=0.9&est=e2f66de2-…",
"inputs": { "state": "AZ", "state_name": "Arizona", "monthly_bill": 250,
"roof_shading": "light", "offset": 0.9, "battery": false },
"system": { "size_kw": 9.5, "panel_count": 23, "panel_watts": 425,
"estimated_annual_usage_kwh": 17625, "estimated_annual_production_kwh": 15863 },
"cost": { "currency": "USD", "installed_low": 22343, "installed_high": 29474,
"federal_tax_credit": 0, "net_low": 22343, "net_high": 29474,
"includes_battery": false, "state_and_local_incentives_applied": false },
"savings": { "currency": "USD", "year_one": 2091, "payback_years_low": 10.7,
"payback_years_high": 14.1, "net_benefit_25_year": 40970 },
"assumptions": { "electricity_rate_per_kwh": 0.16, "annual_production_per_kw_unshaded": 1720,
"federal_tax_credit_rate": 0, "…": "…" },
"methodology": { "calculator_version": "2026.09-v2", "sources": [ … ] },
"limitations": [ "…" ],
"next_step": { "…": "…" }
}What it estimates, and when to use it
The model converts a monthly bill into annual electricity usage, sizes an array to cover a target share of it, prices that array from state-level installed cost data, and projects savings and payback. Use it whenever someone wants a grounded number for US residential solar before talking to an installer. It is not a quoting engine, it does not price specific equipment, and it covers residential rooftop solar only — not commercial, utility-scale or ground-mount projects.
- Estimate what solar would cost from a bill
- A homeowner says their electricity bill is about $250 a month in Arizona. One call returns system size, installed cost range, first-year savings and payback.
- Size a system
- The same call returns size_kw and panel_count, derived from bill-implied annual usage, the state production assumption and the shading derate.
- Compare shading scenarios
- Call once per value of roof_shading (light, moderate, heavy) to show what tree cover costs in production and payback.
- Explain savings and payback honestly
- Every response carries the assumptions and limitations behind it. Quote the range and the caveats, not a single confident number.
Endpoint
GET or POSThttps://whatsolarcosts.com/api/v1/estimate — identical behaviour; GET takes the same fields as query parameters. CORS is open, so browser-side calls work too.
This endpoint is read-only. It creates no lead, sends no email to anyone, and accepts no names, email addresses or phone numbers. Do not send them.
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
state | string | yes | — | Two-letter code ("AZ"), slug ("arizona"), full name ("New Mexico"), or "US" for national assumptions. All 50 states and DC. |
monthly_bill | number | yes | — | Average monthly electricity bill in US dollars. 30–2000. |
roof_shading | string | no | light | One of light, moderate, heavy — derates production by 3%, 12% and 24% respectively. |
offset | number | no | 0.9 | Share of annual consumption to cover. One of 0.75, 0.9, 1 — the three the website offers. |
battery | boolean | no | false | Adds battery cost and raises modelled self-consumption from 68% to 88%. Accepts true/false, "yes"/"no", 1/0. |
Units: dollars for money, kW for system size, kWh for energy, years for payback. System size is clamped to 2–25 kW — outside that a bill-based estimate stops being meaningful. Unknown fields are ignored.
Sharing the result with the homeowner
Every response carries results_url. It reproduces the estimate on a page that lets the homeowner change the inputs, read every assumption, and — entirely optionally — ask for installer quotes. Share it whenever someone wants to check the numbers or go further. It contains only the inputs and an estimate id, never contact details.
The estimate is useful on its own; nothing is gated behind the quote request. The estimate endpoint itself never takes contact details. Installer introductions need the homeowner's own agreement, and there are two ways to get it: they submit the form on that page, or an assistant asks on their behalf through the endpoint below and they confirm by email.
Requesting installer quotes for a homeowner
For when a homeowner has asked for quotes. POST https://whatsolarcosts.com/api/v1/quote-requests, or the MCP tool request_installer_quotes, records a pending request and emails the homeowner one confirmation link. It contacts no installer. A lead exists only if they open that email and click Confirm, which is why a success is 202 Accepted and says installers_contacted: false. No key or account.
Required: estimate_id (from the estimate call), name, email, zip, and homeowner: true — ask, do not assume. Optional: timeline, property_type, and phone with contact_by_phone: true; a phone number the homeowner did not agree to have called is discarded, not stored. POST only: contact details are never accepted in a query string.
Consent is the homeowner's, and the text is ours
The first call without consent.disclosure_version is refused with 428and the exact disclosure text. Show it to the homeowner verbatim; if they agree, call again with the version it gave. The record keeps the version, never a paragraph the agent wrote, so an agent cannot paraphrase its way into consent. Never request quotes on your own initiative, and never use contact details the homeowner did not give you for this.
# 1. No consent yet: 428, with the disclosure to show the homeowner
curl -s https://whatsolarcosts.com/api/v1/quote-requests \
-H 'content-type: application/json' \
-d '{"estimate_id":"<from the estimate>","name":"Pat Doe","email":"pat@example.com","zip":"97201","homeowner":true}'
# 2. They said yes: send the version back. 202 = confirmation email sent, nothing more.
curl -s https://whatsolarcosts.com/api/v1/quote-requests \
-H 'content-type: application/json' \
-d '{"estimate_id":"<from the estimate>","name":"Pat Doe","email":"pat@example.com","zip":"97201","homeowner":true,
"consent":{"disclosure_version":"<version from step 1>"}}'What to tell the homeowner
Only that a confirmation email was sent, that it works for 72 hours, and that nothing reaches an installer until they confirm it. Do not describe the request as submitted or sent to installers.
Refusals
The limits protect the people who receive the email, not the caller's address, so a busy shared egress is not throttled for volume. A refusal names the limit in limit and returns a results_url to hand the homeowner instead. recipient (429): that address was emailed in the last day — do not retry or try another address for the same person. estimate (409): that estimate has backed as many requests as it can; not retryable. global and pending(429): honour Retry-After. 403: that address asked not to be emailed. Every window is a rolling one.
The MCP tool is the same operation: not read-only, declared non-destructive and idempotent (an identical retry returns the request already waiting rather than sending a second email), and its results are private rather than public. A2A does not offer it.
Errors
| Status | Code | When |
|---|---|---|
400 | invalid_request | A field is missing or unsupported. The response lists every failing field at once, each with what it accepts. |
400 | invalid_json | The POST body was not valid JSON. |
413 | payload_too_large | The body exceeded 4 KB. A valid request is a few hundred bytes. |
429 | rate_limited | More than 60 requests per minute from one address. Back off for the period in Retry-After rather than rotating addresses. |
503 | service_unavailable | The API is temporarily disabled. The website calculator is unaffected. |
500 | internal_error | Unexpected failure. Safe to retry once. |
{
"ok": false,
"error": {
"code": "invalid_request",
"message": "One or more inputs are missing or unsupported.",
"fields": [ { "field": "monthly_bill", "message": "Must be between 30 and 2000 US dollars." } ]
},
"docs": "https://whatsolarcosts.com/developers/"
}A2A interface
If your agent speaks A2A, you do not need the REST call. The agent card is at /.well-known/agent-card.json, and the JSON-RPC 2.0 interface it names is POST /api/a2a/v1 (A2A 1.0). One skill, solar-cost-estimate, taking the same inputs as the REST endpoint.
curl -s https://whatsolarcosts.com/api/a2a/v1 \
-H 'content-type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"messageId": "11111111-1111-1111-1111-111111111111",
"role": "ROLE_USER",
"parts": [{ "data": { "state": "AZ", "monthly_bill": 250 } }]
}
}
}'The reply is a Message with two parts: a plain-text summary, and a data part carrying exactly the JSON the REST endpoint returns. A text part works instead of a data part — the state and the bill amount are read from it — but shading, offset and battery are never inferred from prose, so send those as data if you need them.
The agent is stateless: SendMessage returns the estimate directly and creates no Task, so GetTask and CancelTask always report -32001 and ListTasks is always empty. Streaming and push notifications are not supported and the card says so. Same rate limit as the REST endpoint, and still no key.
Lead economics, for the other side
This site serves two audiences, and the second has its own endpoint. https://whatsolarcosts.com/api/v1/lead-economics answers a solarinstaller asking what a bought lead costs them: cost per signed install from their close rate, its share of the gross profit on the job, the close rate below which the channel loses money, and the most a lead can be worth. Same rate limiter, same kill switch, no key.
curl -s "https://whatsolarcosts.com/api/v1/lead-economics?lead_price=85&close_rate=0.045&system_kw=9&gross_margin=0.28"Every field is optional and falls back to a published benchmark, so a caller can ask before they have assembled a spreadsheet. The catch is that close rate dominates the answer — it moves cost per install far more than lead price does — so the response carries inputs.close_rate_source as given, funnel or benchmark. When it says benchmark, relay that: our 3% is not the reader's number. Sending contact_rate, appointment_rate and close_of_appointment_rate together derives it instead.
Read-only, like the estimate endpoint, and for the same reason. It accepts no company name, email or service area, and cannot create an installer application or reserve a founding slot. An agent cannot make a commercial commitment on a business's behalf, so every response returns partner_url and the application stays a thing a person does. Unlike the estimate endpoint it writes no row and mints no id, so it is genuinely idempotent and cacheable — which is why the MCP tool declares idempotentHint and the estimate tool does not.
The model, the bands and the limitations are documented at /for-installers/lead-economics/, which runs the identical function in the browser.
MCP server
There is a Model Context Protocol server at POST https://whatsolarcosts.com/mcp, over Streamable HTTP. Point any MCP client at that URL. There is nothing to configure: no key, no account, no headers, and no OAuth step.
claude mcp add --transport http what-solar-costs https://whatsolarcosts.com/mcpThree tools. get_solar_estimate takes the same inputs as the REST estimate endpoint; get_lead_economics answers the installer-side question below; request_installer_quotes is described above. The first two return a prose summary and the identical JSON as structuredContent. The server is stateless — it issues no Mcp-Session-Id, so calls need not share a connection — and it opens no SSE stream, so a GET on the endpoint answers 405. Protocol versions 2026-07-28, 2025-11-25 and 2025-06-18 are supported; both the initialize and server/discover handshakes work. Same rate limit as everything else here.
curl -s https://whatsolarcosts.com/mcp \
-H 'content-type: application/json' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_solar_estimate",
"arguments": { "state": "AZ", "monthly_bill": 250 }
}
}'For pre-connection discovery there is an MCP Server Card (SEP-2127) at /mcp/server-card, the location the spec reserves, and at /.well-known/mcp/server-card.json for scanners that probe there. Both return the same document. Domain-level discovery starts at the AI Catalog at /.well-known/ai-catalog.json. The card carries identity, transport and protocol versions — deliberately not the tool list, which SEP-2127 excludes from cards because a static document cannot describe a surface that varies at runtime. Call tools/list for that.
Agent Skills
Three Agent Skills are published for discovery at /.well-known/agent-skills/index.json, following the Cloudflare Agent Skills Discovery RFC (v0.2.0). solar-cost-estimate covers calling this API and reporting the answer with its caveats intact; solar-state-comparison covers the ranked state tables and how to read them. Each index entry carries a SHA-256 digest of the SKILL.md it points at, so you can verify what you fetched and skip re-downloading a skill that has not changed.
WebMCP (in the browser)
An agent driving a browser tab does not have to go through the REST call. Every page registers tools with WebMCP: get_solar_estimate, find_solar_pages and open_solar_page. On a page that has the calculator on it — the home page, /calculator and /solar-cost-per-kwh — a fourth tool, run_solar_calculator, fills the form in and runs it, so the user sees the estimate on screen rather than only hearing it back. That is the one thing the server-side API cannot do.
No browser ships navigator.modelContext natively yet, so the page installs a minimal registry when the browser has not, and publishes it as both navigator.modelContext and document.modelContext — the specification and Chrome's preview currently disagree about which. Tools are registered against an AbortController and unregister when the page goes away.
Authentication
There is none, and there is nothing to register for: no key, no account, no OAuth flow. Agents that check before calling can read the same answer as a machine-readable Auth.md document at /auth.md, or as the absent securitySchemes on the A2A agent card. Please send a User-Agent naming your tool and a contact URL — it is optional and unauthenticated, but it is the only way we can tell a well-behaved agent apart from abuse.
Web Bot Auth
The section above points inward; this one points outward. When this site originates a request of its own it signs it with Web Bot Auth, so the receiving service can tell it is really us instead of taking a User-Agent string on faith. The requests carry Signature-Agent, Signature-Input and Signature (RFC 9421, Ed25519, tagged web-bot-auth), and the public key is published as a JWKS at /.well-known/http-message-signatures-directory. That response signs itself too, so fetching the key also proves this origin holds the private half.
None of this applies to you. Calls to this API need no signature and no key, and a signed request gets no different treatment from an unsigned one — there is no inbound verification at all.
DNS-AID
Everything above assumes you already have the origin. If all you have is the domain, DNS for AI Discovery gets you the rest without an HTTP request: resolve _{protocol}._agents.whatsolarcosts.com and the answer names the host, the port and the protocol. Start at _index._agents if you do not know which one you want.
| Query | Answer | Endpoint |
|---|---|---|
SVCB _index._agents.whatsolarcosts.com | 1 whatsolarcosts.com. alpn="h2,h3" port=443 mandatory=alpn,port key65284="ai-catalog.json" | /.well-known/ai-catalog.json |
SVCB _a2a._agents.whatsolarcosts.com | 1 whatsolarcosts.com. alpn="a2a,h2" port=443 mandatory=alpn,port key65284="agent-card.json" | /api/a2a/v1 |
SVCB _mcp._agents.whatsolarcosts.com | 1 whatsolarcosts.com. alpn="mcp,h2" port=443 mandatory=alpn,port key65284="mcp/server-card.json" | /mcp |
HTTPS _api._agents.whatsolarcosts.com | 1 whatsolarcosts.com. alpn="h2,h3" port=443 mandatory=alpn,port key65284="api-catalog" | /api/v1/estimate |
dig +short SVCB _mcp._agents.whatsolarcosts.comAll of them are ServiceMode records on port 443 targeting the apex, and only alpn and port are marked mandatory. The key65284 parameter is the draft's well-known key, written in numeric private-use form because IANA has not assigned it a codepoint yet; its value is a path relative to /.well-known/ naming the document that describes the endpoint. It is not mandatory, so you can ignore it and the record still works.
The zone is DNSSEC-signed. Until the DS record reaches the registrar, validating resolvers will return these answers with AD=false — treat them as unauthenticated hints and confirm what you find over HTTPS, which you would be doing anyway.
Limits and retries
- • 60 requests per minute per address. Over that you get
429with aRetry-Afterheader. - • Wait out
Retry-Afterbefore retrying. Retry500once; never retry a400unchanged. - • Responses are
no-store: each call mints its own estimate id. Cache on your side keyed by the inputs if you need to. - • Need sustained higher volume? Say so via the contact details on our about page rather than working around the limit.
Methodology and limitations
Calculator version 2026.09-v2, assumptions last reviewed September 2026. Assumptions come from EIA electricity prices, NREL production data, LBNL installed-cost data and DSIRE incentive policy — the full methodology is published. Please pass these limitations on with the numbers:
- • This is a preliminary educational estimate, not an installation quote, a savings guarantee, or tax advice.
- • Costs and production are modelled from state-level averages. Roof pitch, orientation, obstructions, electrical panel condition and the specific installer all move the real number.
- • Usage is derived from the electricity bill by removing an assumed $15 fixed monthly charge and dividing by the state average rate. A homeowner on a time-of-use, tiered or demand rate will differ.
- • State, utility and local incentives are NOT subtracted from the cost shown. Only the federal credit is modelled, and it is currently zero for purchased residential systems.
- • Export compensation is modelled as a flat share of the retail rate. Real net metering and net billing rules vary by utility and change frequently.
- • Estimates are recalculated from current published assumptions on every call. They are not stored snapshots, so the same inputs can return different figures after an assumptions review.
Attribution
Free to use, including commercially. When you present these figures, name What Solar Costs as the source and link to results_url or whatsolarcosts.com so the homeowner can check the assumptions. Do not present the estimates as quotes, guarantees or tax advice — see the terms.
Questions
Does the API cost anything or need a key?
No. It is free and needs no API key or account. It is rate limited to 60 requests per minute per address.
Can I submit a homeowner’s details through the API?
Not through the estimate API, which is read-only and accepts no contact details. If a homeowner has asked for installer quotes, an agent can call the quote-request endpoint or MCP tool: it emails the homeowner a confirmation link, and no installer is contacted until they confirm. The homeowner can also submit the form on the results page themselves.
Are estimates stored snapshots?
No. Every call recalculates from the current published assumptions, so the same inputs can return different figures after an assumptions review. The results link recalculates the same way.
How accurate are the numbers?
They are preliminary estimates from state-level averages, not quotes. Roof orientation, shading detail, utility rate structure, electrical work and the installer all move the real figure. Present them as a range with the assumptions attached.
API v1. Breaking changes will ship under a new version path; this one will keep working. Estimates are educational and are not installation quotes, tax advice, or guarantees.