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.
| 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_kwpis per kWp of installed DC peak power. Irradiation is kWh/m². - 14 % system loss by default (inverter, cables, dirt, mismatch).
system_lossrescales the result by (1 − loss) / 0.86. - Free horizon: hills and trees are not in the model. Send your own
shadingfactor (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 'https://numsolar.com/api/v1/pv-yield?lat=59.33&lng=18.07&tilt=35&azimuth=180&kwp=9.24'
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();
{
"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.modelanddataset: 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 'https://numsolar.com/api/v1/pv-yield/matrix?lat=59.33&lng=18.07'
{
"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.
{
"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 -s -X POST 'https://numsolar.com/api/v1/pv-yield/batch' -H 'content-type: application/json' \
-d @pv_yield_batch.request.json
{
"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.
{
"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.