solarwhen.com

Free hourly solar forecast for any UK postcode. No API key. No signup.

made by Tim Jones

Quick start

curl 'https://uk.solarwhen.com/forecast/SW1A?dir=s&tilt=35&kwh=4.5'

Replace SW1A with your postcode outcode (the part before the space).

Try it

Embeddable stats card

Append &format=svg to get an 800×460 SVG of the chart + headline numbers. Clickable example:

uk.solarwhen.com/forecast/SW1A?dir=s&tilt=35&kwh=4.5&format=svg

Drop the URL into an <img> tag, a markdown image, a Notion / Discord / Slack message, or a README badge — anywhere an image URL works:

<img src="https://uk.solarwhen.com/forecast/SW1A?kwh=4.5&format=svg"
     alt="UK solar forecast">

![SW1A solar forecast](https://uk.solarwhen.com/forecast/SW1A?kwh=4.5&format=svg)

Each unique URL is cached at the edge for 1 hour, so embedding it on a popular page costs nothing extra.

GET /forecast/{outcode}

{outcode} is the first part of a UK postcode — e.g. SW1A, EC1Y, N1.

Param Type Default Description
dir enum s Panel facing: n / ne / e / se / s / sw / w / nw
tilt 0 – 90 35 Panel tilt in degrees from horizontal
kwh number System size in kWp. Presence flips default mode to kwh.
mode percent | kwh percent (kwh if kwh given) Output units: percent of nameplate or absolute kWh.
when today | now | tomorrow today Window: today (00–23 local), now (rolling 24 h from this hour), tomorrow.
electricity_region A–P auto-derived from outcode DNO region letter for the Agile price lookup. Override only if you're in a postcode-area-split edge case.
format json | svg json svg returns an 800×460 embeddable stats card (chart + headline numbers). Drop it into a <img> tag or a README.

Response

{
  "date":               "2026-05-08",       // local date the window starts on
  "electricity_region": "C",                // DNO region used for the Agile price lookup
  "solar_production":   "medium",           // low | medium | high (peak as % of nameplate)
  "sunrise":            "05:24",            // local time HH:MM (null at polar latitudes)
  "sunset":             "20:52",
  "peak_pv":            { "hour": "14",     // best solar-generation hour
                          "kwh":  2.0 },    // or "percent" in percent mode; null if no daylight
  "total_produced_kwh": 15.8,               // only present in kwh mode
  "average_price":      22.4,               // avg Agile p/kWh across the 24 h window
  "hourly_pv": {                            // PV production: kWh (1 dp) or % (integer)
    "00": 0.0, "01": 0.0, ...
    "14": 2.0, "15": 2.0, ...
    "23": 0.0
  },
  "hourly_price": {                         // Agile p/kWh per hour (1 dp)
    "00": 12.3, "01": 8.5, ...              // missing days fall back to the previous day
    "23": 18.7
  },
  "hourly_charge": {                        // recommended battery-charge schedule
    "00": 0, "01": 1, ...                   // 1 = charge from grid this hour, 0 = stop
    "23": 0
  }
}

Charge plan

hourly_charge is a per-hour battery-from-grid schedule (1 = charge, 0 = stop), computed from the same price + solar curve. Drop the array straight into a Home Assistant automation to drive a Fox ESS / GivEnergy / similar battery's force-charge slots without writing any rule logic yourself.

Rules, in priority order:

  1. Never charge during peak times — the whole point of the charge plan.
  2. Charge in the run-up to peak times — ensures there's enough charge in the battery so you're not forced to pay peak prices.
  3. Charge during cheap hours when solar can't help.
  4. Otherwise, don't charge — it's more efficient to let solar charge the battery than to pre-emptively grab from the grid.

Convenience endpoints

If you only need the charge bit and not the rest of the forecast object, two thin endpoints are exposed alongside /forecast:

GET /charge/{outcode}/now Returns 0 or 1 — should the battery charge from the grid this hour.
GET /charge/{outcode}/schedule Returns a 24-element array of 0/1, one per local hour 00..23 today.

Both accept the same dir, tilt, and electricity_region query params as /forecast. Drop the schedule array straight into a Home Assistant REST sensor and an automation can write your battery's force-charge slots without any rule logic in HA itself.

GET /weather/forecast/{outcode}

The raw hourly weather we already fetch from MET Norway to drive the solar forecast — exposed directly for callers who just want a simple per-postcode weather feed (no panel direction / tilt / battery rules needed).

Example: uk.solarwhen.com/weather/forecast/SW1A

Embeddable card: uk.solarwhen.com/weather/forecast/SW1A?format=svg

UK weather forecast card

Param Type Default Description
when today | now | tomorrow today Window: today (00–23 local), now (rolling 24 h from this hour), tomorrow.
format json | svg json svg returns an 800×460 embeddable weather card (temp line + precip bars + condition strip + highs/lows). Drop into an <img> tag or README.
{
  "date":            "2026-05-11",       // local date the window starts on
  "outcode":         "SW1A",
  "sunrise":         "05:18",            // local HH:MM (null at polar latitudes)
  "sunset":          "20:38",
  "temp_high_c":     11.9,               // max / min across the 24 h window
  "temp_low_c":      5.2,
  "total_precip_mm": 1.0,                // sum of hourly precip in the window
  "hourly_temp_c": {                     // air temperature, °C (1 dp)
    "00": 8.1, "01": 7.6, ...
    "23": 5.4
  },
  "hourly_cloud_pct": {                  // total cloud cover, % (integer)
    "00": 12, "01": 25, ...
    "23": 100
  },
  "hourly_precip_mm": {                  // liquid-equivalent precip per hour (2 dp)
    "00": 0.0, "13": 0.1, "14": 0.8, ...
  },
  "hourly_condition": {                  // derived label, see below
    "00": "clear", "13": "rain", ...
    "23": "overcast"
  }
}

hourly_condition is a coarse label derived from the same row:

Precipitation is liquid-equivalent (lumps drizzle, rain, sleet, snow into a single number). Same edge-cache TTL as /forecast: 1 hour.

Rate limits

100 requests / minute and 5 000 requests / day per IP. Every response carries:

RateLimit:        limit=100, remaining=99, reset=59
RateLimit-Policy: 100;w=60, 5000;w=86400

On a hit, the API responds 429 with a Retry-After seconds header.

Status codes

200Success
400Bad input (invalid outcode format, tilt out of range, unknown dir / mode / when).
404Outcode not in the UK postcode list.
429Rate limit hit. Wait the Retry-After seconds.
503Forecast batch not yet refreshed for this hour. Try again shortly.

How it works