Submit forecasts with the API

Provider software sends day-ahead price curves to one endpoint with a Forecast Score API key. Forecasts stay private until official prices are published and they are scored.

1 · Get an API key

First accept the provider terms to become a provider. Then create, rotate and revoke keys in the API keys panel of your dashboard. Keys never expire unless you choose 1 year or 90 days; a new key is shown once, so store it securely. Never put a key in browser code or share it. Send it on every request in the X-API-Key header.

2 · The endpoint

POST https://ingest.forecastscore.eu/v1/forecasts

This host accepts forecast submissions and cancellations, and the read-only reference data below. Send your key in the X-API-Key header and a unique Idempotency-Key with every submission or cancellation, with a JSON body up to 1 MiB. Current limits: Deadlines, versions and clock changes.

3 · Ask which periods to fill

The same host serves read-only reference data with the same X-API-Key: GET https://ingest.forecastscore.eu/v1/markets, GET https://ingest.forecastscore.eu/v1/products and GET https://ingest.forecastscore.eu/v1/markets/{market}/delivery-periods?date=YYYY-MM-DD&resolution=PT60M. The delivery-periods answer lists every period_start the submission must contain, DST days included; for 2026-10-25 in ES that is 25 hourly periods. Each period also carries its market-local local_time and an occurrence that is 2 only for the repeated hour when clocks go back. The day-level submission_deadline and state (open, closed or frozen) say whether the day still accepts forecasts; an optional product parameter defaults to DAY_AHEAD.

curl -H "X-API-Key: YOUR_API_KEY" \
  "https://ingest.forecastscore.eu/v1/markets/ES/delivery-periods?date=2026-10-25&resolution=PT60M"
{
  "market": "ES",
  "product": "DAY_AHEAD",
  "delivery_date": "<requested day>",
  "resolution": "PT60M",
  "timezone": "Europe/Madrid",
  "submission_deadline": "2026-10-24T10:45:00Z",
  "state": "open",
  "period_count": 25,
  "periods": [
    { "period_start": "2026-10-24T22:00:00Z", "local_time": "00:00", "occurrence": 1 },
    { "period_start": "2026-10-24T23:00:00Z", "local_time": "01:00", "occurrence": 1 },
    { "period_start": "2026-10-25T00:00:00Z", "local_time": "02:00", "occurrence": 1 },
    { "period_start": "2026-10-25T01:00:00Z", "local_time": "02:00", "occurrence": 2 },
    "…"
  ]
}

Periods follow the market's local day, so daylight-saving changes alter the count. Hourly curves have 23, 24 or 25 periods; 15-minute curves have 92, 96 or 100. Missing, duplicated, misaligned or out-of-day periods, and instants without an offset, are rejected. On the autumn change the repeated local hour is two different UTC instants.

4 · Send the curves

The body is {"forecasts": [...]} with 1–100 forecasts, accepted or rejected together: if any forecast is invalid, none is saved and the error names it, for example forecasts[3]: ….

This Python 3.9+ script (standard library only) writes forecast.json for the next delivery day that is still open, with one period per market-local interval, DST days included. Replace the price with your model's values. On Windows, run pip install tzdata first: Python there has no built-in time-zone data.

"""Build forecast.json: a complete curve for the next delivery day still open."""
import json
from datetime import datetime, time, timedelta, timezone
from zoneinfo import ZoneInfo

MODEL_CODE = "LSTM2"                   # your code: letters and digits, up to 32
MARKET = "ES"                          # ES, FR or DE-LU
MARKET_TZ = ZoneInfo("Europe/Madrid")  # FR: Europe/Paris, DE-LU: Europe/Berlin
RESOLUTION, MINUTES = "PT60M", 60      # or "PT15M", 15
CUTOFF_TZ = ZoneInfo("Europe/Brussels")


def next_open_delivery_day(now: datetime):
    """Earliest delivery day whose 12:45 Europe/Brussels cutoff (day before) is ahead."""
    day = now.astimezone(CUTOFF_TZ).date() + timedelta(days=1)
    while datetime.combine(day - timedelta(days=1), time(12, 45), CUTOFF_TZ) <= now:
        day += timedelta(days=1)
    return day


def period_starts(day):
    """UTC start of every period in the market-local day (23/24/25 or 92/96/100)."""
    start = datetime.combine(day, time(0), MARKET_TZ).astimezone(timezone.utc)
    end = datetime.combine(day + timedelta(days=1), time(0), MARKET_TZ).astimezone(timezone.utc)
    while start < end:
        yield start
        start += timedelta(minutes=MINUTES)


day = next_open_delivery_day(datetime.now(timezone.utc))
forecast = {
    "market": MARKET,
    "product": "DAY_AHEAD",
    "delivery_date": day.isoformat(),
    "resolution": RESOLUTION,
    "model_code": MODEL_CODE,
    "values": [
        {"period_start": start.strftime("%Y-%m-%dT%H:%M:%SZ"), "value": 72.5}  # your price
        for start in period_starts(day)
    ],
}
# Up to 100 forecasts per request (other markets, models or days), accepted together.
with open("forecast.json", "w") as file:
    json.dump({"forecasts": [forecast]}, file)
print(f"{MODEL_CODE} {MARKET} {day} {RESOLUTION}: {len(forecast['values'])} periods -> forecast.json")

Send it with your own key in place of YOUR_API_KEY:

KEY=$(uuidgen)   # one key per change; reuse it only to retry this same request
curl -X POST https://ingest.forecastscore.eu/v1/forecasts \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  --data @forecast.json

A successful submission returns 201 Created, one entry per forecast in request order:

{
  "forecasts": [
    {
      "id": "…",
      "market": "ES",
      "product": "DAY_AHEAD",
      "delivery_date": "<delivery day>",
      "resolution": "PT60M",
      "model_code": "LSTM2",
      "model_name": "LSTM2",
      "version": 1,
      "status": "PENDING",
      "periods": <23, 24 or 25 for hourly curves>,
      "submitted_at": "<arrival time, UTC>",
      "submission_deadline": "<12:45 Europe/Brussels the day before, UTC>"
    }
  ]
}

The fields

FieldAccepted values
marketES, FR or DE-LU.
productDAY_AHEAD (default).
delivery_dateThe market-local delivery day, YYYY-MM-DD.
resolutionPT60M (hourly) or PT15M (15 minutes).
model_codeYour code for the model, 1–32 letters and digits (sent in lower case, it is stored upper case), unique among your models and shown publicly next to your display name. Other providers may use the same code for their own models.
valuesOne {"period_start", "value"} item per delivery period. period_start is the period's start as an ISO 8601 instant with an offset, for example 2026-10-25T00:00:00Z; value is the price in EUR/MWh. Order does not matter.

Deadline and revisions

Submissions close at 12:45 Europe/Brussels (CET/CEST) on the day before delivery, for every market, ten minutes before the earliest SDAC results. The time your complete request arrives counts, not a client timestamp. Sending the same market, product, delivery date, resolution and model code again before the deadline creates a new version and supersedes the previous one; history is kept. Before the deadline you can also cancel a pending forecast by its id; it stays in your history as cancelled. Success is 204 No Content:

curl -X DELETE https://ingest.forecastscore.eu/v1/forecasts/FORECAST_ID \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

Retries and idempotency keys

Every submission and cancellation needs an Idempotency-Key header: any unique value up to 255 printable characters, such as a UUID. If a request times out, send it again with the same key: within 24 hours you get the first answer back (marked Idempotent-Replayed: true) instead of a second version. Use a new key for each new change. Reusing a key for a different body is rejected, and a request that failed does not use up its key.

Errors

400
Missing or invalid Idempotency-Key.
401
Missing, invalid, expired or revoked API key.
403
The account has not accepted the provider terms (provider_terms_required), or its provider access is suspended or revoked (provider_access_inactive).
404
Unknown market or product, a forecast that is not yours, or any other path or method on this host.
409
The deadline has passed, or the forecast to cancel is no longer pending.
413
Request body larger than 1 MiB.
422
Invalid body or delivery periods, the same forecast twice in one request, or an idempotency key reused for a different request.
422
A value outside −1000 to 4000 €/MWh (forecast_value_out_of_range) or with more than 2 decimals (forecast_value_precision); values are never rounded.