# Forecasts

The forecast delivers the expected PV power for a site in 15-minute intervals.
You query it via the `site_id`; site, strings and configuration come from the
stored site.

```bash
GET /v2/forecast/{site_id}
```

Timestamps are in the site's **local timezone** by default (see `timezone` in the response).
Pass `?timezone=utc` to receive UTC timestamps ending in `Z` instead.

:::tip
The full parameter and field list is in the [API Reference](/api). This page explains
the concepts.
:::

## Parameters

| Parameter       | Meaning                                                                                                          |
|-----------------|------------------------------------------------------------------------------------------------------------------|
| `forecast_days` | Forecast horizon in days. Without a value, the plan maximum. Hard limit 7, additionally capped to the plan limit. |
| `past_days`     | Past days before today (archived forecast). Default 0. Hard limit 30, capped to the plan limit.                  |
| `include`       | Which field groups come back (repeatable). Default `default`.                                                   |
| `timezone`      | Timezone of the timestamps: `local` (default) or `utc`. `utc` returns UTC timestamps ending in `Z`.             |

:::note
`past_days` delivers **archived forecast data**, not actual measurements. For real past
yields use the [Historical Data](/en/v2/api/historical).
:::

## `include` groups

`include` is repeatable and selects **field groups** — not individual columns:

```bash
GET /v2/forecast/{site_id}?include=weather&include=irradiance
```

| Group         | Fields                                                                                                  |
|---------------|---------------------------------------------------------------------------------------------------------|
| `default`     | `pv_power` (PV power in W).                                                                             |
| `weather`     | `temp`, `weather_code`, `wind_speed`, `relative_humidity`, `precipitation_mm`, `snow_water_equivalent`. |
| `irradiance`  | `ghi`, `dhi`, `bni` (horizontal irradiance).                                                           |
| `clearsky`    | `pv_power_clearsky` (power under a cloudless sky).                                                      |
| `strings`     | Adds the separate `strings` block (see below). `values` stays unchanged.                                |
| `variability` | Adds the min/max band (see below). **Paid feature**, never part of `all`.                               |
| `all`         | All field groups **and** the `strings` block (but **not** `variability`).                               |

:::note
`default` is **not** added automatically. If you only request `weather`, you get
weather data without `pv_power`. The `included` field in the response reflects the resolved selection.
:::

## Response structure

```json title="Response (abridged)"
{
  "site_id": "site_rv8wm5…",
  "timezone": "Europe/Berlin",
  "computed_at": "2026-06-09T08:00:00",
  "included": [
    "default"
  ],
  "daily": [
    {
      "date": "2026-06-09",
      "pv_energy_kwh": 42.7,
      "temp_min": 11.0,
      "temp_max": 24.3
    }
  ],
  "values": [
    {
      "timestamp": "2026-06-09T12:00:00",
      "pv_power": 8200.0
    }
  ]
}
```

- **`daily`** — daily aggregates (energy in kWh, min/max temperature, dominant weather code).
- **`values`** — time series per 15 minutes. `pv_power` is the **sum over all strings**.

## The `strings` block

With `include=strings` (or `all`) you get a separate top-level array `strings` in
**long/tidy format** — one row per time step and string:

```json
"strings": [
{"timestamp": "2026-06-09T12:00:00", "string_index": 0, "string_id": "str_9f3a1c08", "pv_power": 5100.0, "gti": 820.0, "gti_shaded": 815.0},
{"timestamp": "2026-06-09T12:00:00", "string_index": 1, "string_id": "str_4b7e2d10", "pv_power": 3100.0, "gti": 760.0, "gti_shaded": 752.0}
]
```

- **`string_index`** refers position-based to `site.strings[i]` — same order as in
  `GET /v2/sites/{site_id}`. Always present.
- **`string_id`** is the string's stable id — the join key to its measurements. Present for saved
  sites; on the inline endpoint it appears only if you supplied an `id` on the string.
- The **tilted irradiance** (`gti`, `gti_shaded`) lives here, not in `values`, because it depends
  on the string's orientation.
- Each string delivers a row in **every** time step (0.0 at night), so there are no gaps.
- `values.pv_power` is the sum of the string powers.

In a DataFrame the block can be loaded directly and pivoted on `string_index`:

```python
import pandas as pd

df = pd.DataFrame(response["strings"])
wide = df.pivot(index="timestamp", columns="string_index", values="pv_power")
```

## Variability band (`include=variability`)

The band delivers a lower/upper bound of the PV power, if
available in your plan. A request with `include=variability` on a plan without this feature returns
**403**.

- Fields: `pv_power_min` / `pv_power_max` in every `values` and `strings` row, plus
  `pv_energy_kwh_min` / `pv_energy_kwh_max` in `daily`.
- **Horizon:** covers roughly the **next 48 h**. Outside that (and for `past_days`)
  `min == max == pv_power` (flat band). Sites not covered also receive a flat band — not an error.
- `variability` is **never** included by `all`, only on explicit request.

## Caching & `computed_at`

Forecasts are **not** recomputed on every request, but only as often as your plan allows.
Repeated requests in the same slot return the same cached result; `computed_at` shows the
computation time.

The cache is automatically invalidated when the site changes (coordinates, strings,
config) or the plan changes.

## Limits & errors

| Limit / error                 | Behavior                                                                                              |
|-------------------------------|-------------------------------------------------------------------------------------------------------|
| Forecast access               | Without forecast access in the plan → **403**.                                                       |
| Monthly requests              | Monthly request limit (per user & endpoint), reset on the 1st of the month → **429** when exceeded.  |
| `forecast_days` / `past_days` | Values are capped to the plan limit.                                                                 |
| Site not found                | **404**.                                                                                             |

:::tip
You'll find your specific limits on the [Usage & Limits](https://pvnode.com/usage) page.
:::
