{
  "openapi": "3.1.0",
  "info": {
    "title": "Energy Price Forecast EU Integration API",
    "version": "1.0.0",
    "description": "Compact contract for Home Assistant and ioBroker integrations. Official day-ahead prices are preferred where available; forecast values extend the remaining horizon. Free access currently supports up to 48 hours and an entitled API key up to 120 hours. The effective limit is returned in meta.allowed_horizon_hours."
  },
  "servers": [
    {
      "url": "https://api.energypriceforecast.eu"
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Optional Private Pro API key. Keep the real key local and send it as Authorization: Bearer YOUR_API_KEY."
      }
    },
    "parameters": {
      "country": {
        "name": "country",
        "in": "query",
        "required": true,
        "schema": {
          "type": "string",
          "example": "de"
        },
        "description": "Country or bidding zone, for example de, nl, at, dk1, dk2 or no1."
      },
      "hours": {
        "name": "hours",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 120,
          "default": 48
        },
        "description": "Requested horizon. Access without a key is currently limited to 48 hours; an entitled key may allow up to 120 hours."
      },
      "windowHours": {
        "name": "window_hours",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 24,
          "default": 4
        },
        "description": "Length of the best continuous time window."
      }
    },
    "schemas": {
      "AccessMeta": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "plan": {
            "type": "string"
          },
          "api_key_state": {
            "type": "string"
          },
          "allowed_horizon_hours": {
            "type": "integer"
          },
          "requested_horizon_hours": {
            "type": "integer"
          },
          "used_horizon_hours": {
            "type": "integer"
          },
          "rate_limit_daily": {
            "type": [
              "integer",
              "null"
            ]
          },
          "used_calls_today": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "ForecastQualityPriceWindow": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "window_hours",
          "price_basis",
          "evaluated_days",
          "exact_hit_days",
          "within_one_hour_days",
          "mean_extra_cost",
          "mean_extra_cost_unit",
          "period_start",
          "period_end"
        ],
        "properties": {
          "window_hours": {
            "type": "integer",
            "minimum": 1,
            "maximum": 24,
            "description": "Continuous window length actually used for this evaluation."
          },
          "price_basis": {
            "type": "string",
            "const": "base",
            "description": "The quality metric is based on wholesale/base prices, including when the current summary uses retail pricing."
          },
          "evaluated_days": {
            "type": "integer",
            "minimum": 0,
            "maximum": 30,
            "description": "Number of complete local calendar days included in the evaluation."
          },
          "exact_hit_days": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Days on which the forecast selected the same start hour as the retrospectively cheapest window."
          },
          "within_one_hour_days": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "description": "Days with a start-hour difference of at most one hour. This count includes exact hits."
          },
          "mean_extra_cost": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "description": "Mean daily max(0, selected actual average minus lowest actual average)."
          },
          "mean_extra_cost_unit": {
            "type": "string",
            "description": "Same currency-per-kWh unit as price.unit in this response."
          },
          "period_start": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "period_end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          }
        }
      },
      "ForecastQuality": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "available",
          "error",
          "price_window"
        ],
        "properties": {
          "available": {
            "type": "boolean",
            "description": "True when at least seven complete days are available."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "insufficient_history",
              "market_not_covered",
              "history_unavailable",
              null
            ]
          },
          "price_window": {
            "$ref": "#/components/schemas/ForecastQualityPriceWindow"
          }
        }
      },
      "Summary": {
        "type": "object",
        "additionalProperties": true,
        "required": [
          "api_version",
          "integration",
          "generated_at",
          "country",
          "meta",
          "flat",
          "price",
          "co2"
        ],
        "properties": {
          "api_version": {
            "type": "string"
          },
          "integration": {
            "type": "string"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "country": {
            "type": "string"
          },
          "timezone": {
            "type": "string"
          },
          "meta": {
            "$ref": "#/components/schemas/AccessMeta"
          },
          "flat": {
            "type": "object",
            "additionalProperties": true
          },
          "price": {
            "type": "object",
            "additionalProperties": true
          },
          "co2": {
            "type": "object",
            "additionalProperties": true
          },
          "combined": {
            "type": "object",
            "additionalProperties": true
          },
          "forecast_quality": {
            "$ref": "#/components/schemas/ForecastQuality"
          },
          "hourly_accuracy": {
            "$ref": "#/components/schemas/HourlyAccuracy"
          },
          "cheaper_day_decision": {
            "$ref": "#/components/schemas/CheaperDayDecision"
          },
          "source": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "PriceEntry": {
        "type": "object",
        "additionalProperties": true,
        "required": [
          "start",
          "end",
          "value",
          "unit",
          "source"
        ],
        "properties": {
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "value": {
            "type": "number"
          },
          "unit": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "description": "Identifies official day-ahead or forecast data."
          },
          "slot_minutes": {
            "type": "integer"
          },
          "native_resolution_minutes": {
            "type": "integer"
          },
          "expansion_method": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "PriceSeries": {
        "type": "object",
        "additionalProperties": true,
        "required": [
          "api_version",
          "generated_at",
          "country",
          "unit",
          "entries",
          "meta"
        ],
        "properties": {
          "api_version": {
            "type": "string"
          },
          "generated_at": {
            "type": "string",
            "format": "date-time"
          },
          "country": {
            "type": "string"
          },
          "currency": {
            "type": "string"
          },
          "unit": {
            "type": "string"
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PriceEntry"
            }
          },
          "request": {
            "type": "object",
            "additionalProperties": true
          },
          "meta": {
            "$ref": "#/components/schemas/AccessMeta"
          }
        }
      },
      "Error": {
        "type": "object",
        "additionalProperties": true,
        "properties": {
          "error": {
            "type": [
              "string",
              "object"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "HourlyAccuracy": {
        "type": "object",
        "description": "Hourly deviation of the frozen forecast from the later published day-ahead price over the last 30 days. Computed once on the forecast backend and published, so the app and this API quote the same figures. Deliberately open for additional properties so the block can grow without breaking clients that validate against this schema.",
        "additionalProperties": true,
        "required": [
          "available",
          "error",
          "unit"
        ],
        "properties": {
          "available": {
            "type": "boolean",
            "description": "False when no summary exists for the market or fewer than 24 hourly pairs could be compared."
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reason when unavailable, e.g. no_summary or insufficient_history."
          },
          "unit": {
            "type": "string",
            "description": "Unit of every absolute error in this block; matches price.unit."
          },
          "sample_count": {
            "type": "integer",
            "minimum": 1
          },
          "mean_abs_error": {
            "type": [
              "number",
              "null"
            ],
            "description": "Mean absolute error."
          },
          "median_abs_error": {
            "type": [
              "number",
              "null"
            ]
          },
          "p90_abs_error": {
            "type": [
              "number",
              "null"
            ],
            "description": "90 percent of hourly errors are at or below this value."
          },
          "within_primary_share": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "within_secondary_share": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "primary_threshold": {
            "type": [
              "number",
              "null"
            ]
          },
          "secondary_threshold": {
            "type": [
              "number",
              "null"
            ]
          },
          "pearson_r": {
            "type": [
              "number",
              "null"
            ],
            "minimum": -1,
            "maximum": 1,
            "description": "How well the forecast follows the shape of the day, independent of the level."
          },
          "period_start": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "period_end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "computed_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "detail": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "CheaperDayDecision": {
        "type": "object",
        "description": "Share of day pairs within a 48-hour horizon where following the forecast led to the cheaper of the two days, judged by each day's cheapest window. Open for additional properties for the same reason as HourlyAccuracy.",
        "additionalProperties": true,
        "required": [
          "available",
          "error",
          "unit"
        ],
        "properties": {
          "available": {
            "type": "boolean"
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reason when unavailable, e.g. no_summary or no_decision_pairs."
          },
          "unit": {
            "type": "string",
            "description": "Unit of mean_regret; matches price.unit."
          },
          "evaluated_pairs": {
            "type": "integer",
            "minimum": 1
          },
          "correct_day_count": {
            "type": "integer",
            "minimum": 0
          },
          "correct_day_share": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "ready": {
            "type": "boolean",
            "description": "False while fewer than minimum_ready_pairs have been compared; the share still moves a lot."
          },
          "minimum_ready_pairs": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "mean_regret": {
            "type": [
              "number",
              "null"
            ],
            "description": "Average cost of following the forecast against picking the cheaper day in hindsight."
          },
          "near_optimal_share": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1
          },
          "comparison_horizon_hours": {
            "type": [
              "number",
              "null"
            ]
          },
          "window_hours": {
            "type": [
              "number",
              "null"
            ]
          },
          "period_start": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "period_end": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "computed_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "detail": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid or unsupported request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, invalid or disabled API key when protected access is requested",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/home-assistant/summary": {
      "get": {
        "summary": "Home Assistant automation summary",
        "description": "Returns current price and CO2 values, useful windows, flat helper fields, access metadata and a cached market-specific forecast-quality block. The quality metric is available without an API key and uses the requested window_hours.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/country"
          },
          {
            "$ref": "#/components/parameters/hours"
          },
          {
            "$ref": "#/components/parameters/windowHours"
          },
          {
            "name": "summary_hours",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 120,
              "default": 24
            },
            "description": "Horizon used for compact summary calculations."
          }
        ],
        "responses": {
          "200": {
            "description": "Summary response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Summary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/home-assistant/prices": {
      "get": {
        "summary": "Home Assistant price series",
        "description": "Returns a price series for the native Home Assistant integration (charts, custom cheapest-N-hours automations). In mixed mode official day-ahead data is used first and forecast data extends the open horizon. With resolution=15m, native quarter-hour day-ahead slots remain native while hourly forecast values are repeated across four slots without invented interpolation.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/country"
          },
          {
            "$ref": "#/components/parameters/hours"
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "mixed",
                "forecast_only"
              ],
              "default": "mixed"
            },
            "description": "Use mixed for day-ahead-first data followed by forecast values."
          },
          {
            "name": "resolution",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "native",
                "15m"
              ],
              "default": "15m"
            }
          },
          {
            "name": "price_mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "base",
                "retail"
              ],
              "default": "base"
            },
            "description": "Base/market price or an assumption-based retail price. Retail is not an exact supplier tariff."
          },
          {
            "name": "plz",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{5}$",
              "example": "10115"
            },
            "description": "German postal code; required for German retail pricing."
          }
        ],
        "responses": {
          "200": {
            "description": "Price-series response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PriceSeries"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/iobroker/summary": {
      "get": {
        "summary": "ioBroker automation summary",
        "description": "Returns a compact structure for JavaScript-adapter states and automations.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/country"
          },
          {
            "$ref": "#/components/parameters/hours"
          },
          {
            "$ref": "#/components/parameters/windowHours"
          },
          {
            "name": "summary_hours",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 120,
              "default": 24
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Summary response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Summary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/iobroker/prices": {
      "get": {
        "summary": "ioBroker price series",
        "description": "Returns a price series for custom scripts. In mixed mode official day-ahead data is used first and forecast data extends the open horizon. With resolution=15m, native quarter-hour day-ahead slots remain native while hourly forecast values are repeated across four slots without invented interpolation.",
        "security": [
          {},
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/country"
          },
          {
            "$ref": "#/components/parameters/hours"
          },
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "mixed",
                "forecast_only"
              ],
              "default": "mixed"
            },
            "description": "Use mixed for day-ahead-first data followed by forecast values."
          },
          {
            "name": "resolution",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "native",
                "15m"
              ],
              "default": "15m"
            }
          },
          {
            "name": "price_mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "base",
                "retail"
              ],
              "default": "base"
            },
            "description": "Base/market price or an assumption-based retail price. Retail is not an exact supplier tariff."
          },
          {
            "name": "plz",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{5}$",
              "example": "10115"
            },
            "description": "German postal code; required for German retail pricing."
          }
        ],
        "responses": {
          "200": {
            "description": "Price-series response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PriceSeries"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  }
}
