PV yield API

The solar yield of any roof face in Sweden, by tilt and azimuth, from a precomputed PVGIS dataset. Free, no key, answers in milliseconds.

Contains PVGIS 5.3 data © European Union, 2001–2026 (CC BY 4.0), transposed with Numsolar solar-core poa v2. Source: PVGIS (European Commission, Joint Research Centre), PVGIS 5.3 API; licence CC BY 4.0. Keep this line when you show the numbers.
Endpoint What it does
GET /api/v1/pv-yield The yield of one roof face at one point: annual and monthly kWh per kWp, the local optimum and how close the face gets to it.
GET /api/v1/pv-yield/matrix Annual kWh per kWp for every tilt (0–90°, 5° steps) and azimuth (0–350°, 10° steps) at one point.
POST /api/v1/pv-yield/batch Many roof faces in one call (each item counts against the limit).
GET /api/v1/pv-yield/meta The dataset: version, sources, coverage, angle steps, limits and measured accuracy.
GET /api/v1/pv-yield/openapi.json The OpenAPI 3.1 document of the PV yield and simulation APIs.

Units and conventions

  • Azimuth is the compass bearing the panels face: 0 = north, 90 = east, 180 = south, 270 = west (360 = 0). PVGIS's "aspect" is azimuth − 180.
  • Tilt: 0 = flat, 90 = vertical. The dataset has 5° tilt and 10° azimuth steps; values between are interpolated.
  • Energies are kWh. _kwh_kwp is per kWp of installed DC peak power. Irradiation is kWh/m².
  • 14 % system loss by default (inverter, cables, dirt, mismatch). system_loss rescales the result by (1 − loss) / 0.86.
  • Free horizon: hills and trees are not in the model. Send your own shading factor (0–1).
  • orientation_factor = the annual yield ÷ the best orientation's annual yield at that point (0–1).

Endpoints

GET /api/v1/pv-yield

The yield of one roof face at one point: annual and monthly kWh per kWp, the local optimum and how close the face gets to it.

Parameter Type Description
latrequired number Latitude, −90 to 90. Inside Sweden's bounding box (55.0–69.5 N, 10.5–24.5 E) and not at sea.
lngrequired number Longitude, −180 to 180.
tiltrequired number Degrees, 0 (flat) to 90 (vertical).
azimuthrequired number Compass degrees, 0 to 360.
kwp number The installed kWp, more than 0 and at most 10 000. Adds annual_kwh and monthly_kwh.
system_loss number 0 to 0.6. Default 0.14.
shading number 0 to 1. Default 1.0 (no shade).
curl
curl 'https://numsolar.com/api/v1/pv-yield?lat=59.33&lng=18.07&tilt=35&azimuth=180&kwp=9.24'
JavaScript
const url = new URL("https://numsolar.com/api/v1/pv-yield");
url.search = new URLSearchParams({lat: 59.33, lng: 18.07, tilt: 35, azimuth: 180, kwp: 9.24}).toString();
const res = await fetch(url);
if (!res.ok) throw new Error((await res.json()).error.message);
const data = await res.json();
200 OK
{
  "annual_kwh": 9084.8,
  "annual_kwh_kwp": 983.2,
  "attribution": "Contains PVGIS 5.3 data © European Union, 2001–2026 (CC BY 4.0), transposed with Numsolar solar-core poa v2",
  "cell": {
    "elevation_m": 48.0,
    "era5_corrected": false,
    "grid_deg": [0.25, 0.5],
    "interpolation": "bilinear",
    "lat": 59.25,
    "lng": 18.0,
    "radiation_db": "PVGIS-SARAH3"
  },
  "dataset": {
    "coverage": "SE bbox 55.0–69.5N 10.5–24.5E",
    "generated_at": "2026-10-07T20:47:05Z",
    "grid_deg": [0.25, 0.5],
    "radiation_db": "PVGIS-SARAH3",
    "sources": ["PVGIS 5.3 SARAH3 2005–2023", "PVGIS 5.3 ERA5 2005–2023 (north of 65N, bias-corrected towards SARAH3)"],
    "version": "se-2026.10"
  },
  "ghi_annual_kwh_m2": 995.5,
  "input": {
    "azimuth_deg": 180.0,
    "kwp": 9.24,
    "lat": 59.33,
    "lng": 18.07,
    "shading": 1.0,
    "system_loss": 0.14,
    "tilt_deg": 35.0
  },
  "model": {
    "albedo": 0.2,
    "anchor_scale": 0.978,
    "horizon": "free",
    "poa_model": "v2",
    "representative_days": 12,
    "sky_model": "hay_davies",
    "steps_per_day": 24,
    "system_loss_default": 0.14
  },
  "monthly_kwh": [162.2, 337.4, 824.5, 1127.6, 1276.1, 1330.5, 1262.0, 1109.9, 851.1, 501.4, 193.1, 109.3],
  "monthly_kwh_kwp": [17.5, 36.5, 89.2, 122.0, 138.1, 144.0, 136.6, 120.1, 92.1, 54.3, 20.9, 11.8],
  "optimal": {
    "annual_kwh_kwp": 988.7,
    "azimuth_deg": 180,
    "tilt_deg": 45
  },
  "orientation_factor": 0.994,
  "poa_annual_kwh_m2": 1171.5
}
  • optimal: the best tilt and azimuth at this point, and its yield.
  • cell: the dataset cell the point fell in (≈ 25 km), and how it was interpolated.
  • model and dataset: how the numbers were made, and the dataset version.

GET /api/v1/pv-yield/matrix

Annual kWh per kWp for every tilt (0–90°, 5° steps) and azimuth (0–350°, 10° steps) at one point.

Parameter Type Description
latrequired number As above.
lngrequired number As above.
system_loss number As above.
shading number As above.

annual_kwh_kwp[tilt][azimuth]: one row per tilt in tilts_deg (19 rows), one column per azimuth in azimuths_deg (36 columns). Use it for a heat map, or to find the best roof face at an address. The example shows the first 2 of the 19 rows.

curl
curl 'https://numsolar.com/api/v1/pv-yield/matrix?lat=59.33&lng=18.07'
200 OK (2 of 19 rows)
{
  "annual_kwh_kwp": [
    [798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7, 798.7],
    [753.5, 754.1, 756.0, 759.2, 763.5, 768.8, 775.0, 781.8, 789.2, 796.6, 804.2, 811.4, 818.2, 824.4, 829.7, 834.0, 837.2, 839.1, 839.8, 839.1, 837.2, 834.1, 829.7, 824.4, 818.2, 811.4, 804.1, 796.6, 789.1, 781.8, 775.0, 768.8, 763.5, 759.2, 756.1, 754.1]
  ],
  "attribution": "Contains PVGIS 5.3 data © European Union, 2001–2026 (CC BY 4.0), transposed with Numsolar solar-core poa v2",
  "azimuths_deg": [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280, 290, 300, 310, 320, 330, 340, 350],
  "cell": {
    "elevation_m": 48.0,
    "era5_corrected": false,
    "grid_deg": [0.25, 0.5],
    "interpolation": "bilinear",
    "lat": 59.25,
    "lng": 18.0,
    "radiation_db": "PVGIS-SARAH3"
  },
  "dataset": {
    "coverage": "SE bbox 55.0–69.5N 10.5–24.5E",
    "generated_at": "2026-10-07T20:47:05Z",
    "grid_deg": [0.25, 0.5],
    "radiation_db": "PVGIS-SARAH3",
    "sources": ["PVGIS 5.3 SARAH3 2005–2023", "PVGIS 5.3 ERA5 2005–2023 (north of 65N, bias-corrected towards SARAH3)"],
    "version": "se-2026.10"
  },
  "input": {
    "lat": 59.33,
    "lng": 18.07,
    "shading": 1.0,
    "system_loss": 0.14
  },
  "model": {
    "albedo": 0.2,
    "anchor_scale": 0.978,
    "horizon": "free",
    "poa_model": "v2",
    "representative_days": 12,
    "sky_model": "hay_davies",
    "steps_per_day": 24,
    "system_loss_default": 0.14
  },
  "optimal": {
    "annual_kwh_kwp": 988.7,
    "azimuth_deg": 180,
    "tilt_deg": 45
  },
  "tilts_deg": [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90]
}

POST /api/v1/pv-yield/batch

Many roof faces in one call (each item counts against the limit).

The body has items: 1 to 50 roof faces, each with the parameters of GET /api/v1/pv-yield and an optional id of your own. One bad item does not fail the call: its result has ok: false and an error.

pv_yield_batch.request.json
{
  "items": [
    {"id": "south", "lat": 59.33, "lng": 18.07, "tilt": 27, "azimuth": 170, "kwp": 6.3},
    {"id": "west", "lat": 59.33, "lng": 18.07, "tilt": 27, "azimuth": 260, "kwp": 3.2},
    {"id": "sea", "lat": 40.0, "lng": 0.0, "tilt": 27, "azimuth": 170}
  ]
}
curl
curl -s -X POST 'https://numsolar.com/api/v1/pv-yield/batch' -H 'content-type: application/json' \
  -d @pv_yield_batch.request.json
200 OK
{
  "attribution": "Contains PVGIS 5.3 data © European Union, 2001–2026 (CC BY 4.0), transposed with Numsolar solar-core poa v2",
  "dataset": {
    "coverage": "SE bbox 55.0–69.5N 10.5–24.5E",
    "generated_at": "2026-10-07T20:47:05Z",
    "grid_deg": [0.25, 0.5],
    "sources": ["PVGIS 5.3 SARAH3 2005–2023", "PVGIS 5.3 ERA5 2005–2023 (north of 65N, bias-corrected towards SARAH3)"],
    "version": "se-2026.10"
  },
  "results": [
    {
      "annual_kwh": 6045.9,
      "annual_kwh_kwp": 959.7,
      "cell": {
        "elevation_m": 48.0,
        "era5_corrected": false,
        "grid_deg": [0.25, 0.5],
        "interpolation": "bilinear",
        "lat": 59.25,
        "lng": 18.0,
        "radiation_db": "PVGIS-SARAH3"
      },
      "ghi_annual_kwh_m2": 995.5,
      "id": "south",
      "input": {
        "azimuth_deg": 170.0,
        "kwp": 6.3,
        "lat": 59.33,
        "lng": 18.07,
        "shading": 1.0,
        "system_loss": 0.14,
        "tilt_deg": 27.0
      },
      "model": {
        "albedo": 0.2,
        "anchor_scale": 0.978,
        "horizon": "free",
        "poa_model": "v2",
        "representative_days": 12,
        "sky_model": "hay_davies",
        "steps_per_day": 24,
        "system_loss_default": 0.14
      },
      "monthly_kwh": [96.1, 208.3, 524.0, 749.5, 875.4, 922.4, 872.2, 750.3, 553.7, 313.7, 116.9, 64.0],
      "monthly_kwh_kwp": [15.3, 33.1, 83.2, 119.0, 138.9, 146.4, 138.4, 119.1, 87.9, 49.8, 18.6, 10.2],
      "ok": true,
      "optimal": {
        "annual_kwh_kwp": 988.7,
        "azimuth_deg": 180,
        "tilt_deg": 45
      },
      "orientation_factor": 0.971,
      "poa_annual_kwh_m2": 1143.1
    },
    {
      "annual_kwh": 2566.2,
      "annual_kwh_kwp": 802.0,
      "cell": {
        "elevation_m": 48.0,
        "era5_corrected": false,
        "grid_deg": [0.25, 0.5],
        "interpolation": "bilinear",
        "lat": 59.25,
        "lng": 18.0,
        "radiation_db": "PVGIS-SARAH3"
      },
      "ghi_annual_kwh_m2": 995.5,
      "id": "west",
      "input": {
        "azimuth_deg": 260.0,
        "kwp": 3.2,
        "lat": 59.33,
        "lng": 18.07,
        "shading": 1.0,
        "system_loss": 0.14,
        "tilt_deg": 27.0
      },
      "model": {
        "albedo": 0.2,
        "anchor_scale": 0.978,
        "horizon": "free",
        "poa_model": "v2",
        "representative_days": 12,
        "sky_model": "hay_davies",
        "steps_per_day": 24,
        "system_loss_default": 0.14
      },
      "monthly_kwh": [25.8, 68.8, 192.8, 316.0, 402.4, 443.6, 409.7, 332.7, 218.8, 106.9, 33.2, 15.6],
      "monthly_kwh_kwp": [8.1, 21.5, 60.3, 98.8, 125.8, 138.6, 128.0, 104.0, 68.4, 33.4, 10.4, 4.9],
      "ok": true,
      "optimal": {
        "annual_kwh_kwp": 988.7,
        "azimuth_deg": 180,
        "tilt_deg": 45
      },
      "orientation_factor": 0.811,
      "poa_annual_kwh_m2": 951.8
    },
    {
      "error": {
        "code": "outside_coverage",
        "message": "The point is outside the dataset's coverage (SE bbox 55.0–69.5N 10.5–24.5E; points at sea are outside too)."
      },
      "id": "sea",
      "ok": false
    }
  ]
}

GET /api/v1/pv-yield/meta

The dataset: version, sources, coverage, angle steps, limits and measured accuracy.

200 OK
{
  "accuracy": {
    "interpolation_note": "p95 interpolation error measured on a 50 km sub-grid; an upper bound for the served 25 km grid",
    "interpolation_p95_pct": 3.4,
    "known_deviation": "north-facing planes read high vs PVGIS: 35N ≈ +6 % (SARAH3), ≈ +12 % north of 65N (ERA5-corrected); flat, east and vertical south within a few per cent; see docs",
    "vs_pvgis_35s_max_pct": 4.51,
    "vs_pvgis_35s_rms_pct": 2.32,
    "vs_pvgis_rms_pct_by_orientation": {
      "35E": 2.32,
      "35N": 9.03,
      "35S": 2.32,
      "90S": 2.73,
      "flat": 3.75
    },
    "vs_pvgis_samples": 39
  },
  "angles": {
    "azimuth_step_deg": 10,
    "tilt_step_deg": 5
  },
  "attribution": "Contains PVGIS 5.3 data © European Union, 2001–2026 (CC BY 4.0), transposed with Numsolar solar-core poa v2",
  "coverage": {
    "bbox": [55.0, 10.5, 69.5, 24.5],
    "cells": 1306,
    "cells_era5_corrected": 406,
    "description": "SE bbox 55.0–69.5N 10.5–24.5E",
    "grid_deg": [0.25, 0.5]
  },
  "dataset": {
    "coverage": "SE bbox 55.0–69.5N 10.5–24.5E",
    "generated_at": "2026-10-07T20:47:05Z",
    "grid_deg": [0.25, 0.5],
    "sources": ["PVGIS 5.3 SARAH3 2005–2023", "PVGIS 5.3 ERA5 2005–2023 (north of 65N, bias-corrected towards SARAH3)"],
    "version": "se-2026.10"
  },
  "docs_url": "https://numsolar.com/docs/api/pv-yield",
  "limits": {
    "anonymous_per_min": 60,
    "batch_max_items": 50,
    "keyed_matrix_per_min": 60,
    "keyed_per_min": 600,
    "matrix_per_min": 10
  },
  "model": {
    "albedo": 0.2,
    "anchor_scale": 0.978,
    "horizon": "free",
    "poa_model": "v2",
    "representative_days": 12,
    "sky_model": "hay_davies",
    "steps_per_day": 24,
    "system_loss_default": 0.14
  },
  "openapi_url": "https://numsolar.com/api/v1/pv-yield/openapi.json"
}

GET /api/v1/pv-yield/openapi.json

The OpenAPI 3.1 document of the PV yield and simulation APIs.

Open the document. Import it into Postman, Insomnia or a code generator, or give it to an AI agent as its tool description.

Errors

Code Status Retry-After When
invalid_parameter 422 – A parameter is missing or out of range. field names it (for example lat).
outside_coverage 422 – The point is outside the dataset (the Sweden bounding box, or at sea).
invalid_json 400 – POST /batch only: the body is not valid JSON.
body_too_large 413 – POST /batch only: the body is larger than 2 MB.
invalid_api_key 401 – The x-api-key header has a key we do not know. A wrong key does not fall back to the free limits.
rate_limited 429 seconds until the next clock minute Over the limit of this minute, or 10 failed calls (a wrong key, a body that is not valid JSON or too large) from your IP this minute: then every call of that IP gets 429 until the minute ends.
dataset_unavailable 503 30 The PV yield data is not loaded right now. Try again in a minute.

How the numbers are made

PVGIS 5.3 monthly horizontal irradiation, diffuse fraction and temperature (2005–2023; SARAH3, and ERA5 corrected towards SARAH3 north of 65° N) for each node of a 0.25° × 0.5° grid (≈ 25 km). Numsolar's solar-core engine turns them into every tilt and azimuth (plane-of-array model v2, Hay–Davies sky). A request interpolates in space and angle; nothing is fetched at request time.

Accuracy

  • South 35°, against PVGIS at 39 random points: RMS 2.32 %, worst 4.51 %.
  • Interpolation between cells: p95 3.4 %, measured on a coarser 50 km grid (an upper limit for the 25 km grid we serve).
  • Flat, east-facing and vertical south panels are within a few per cent of PVGIS. North-facing panels read high: about +6 % at 35° in most of Sweden, about +12 % north of 65° N.