Webhooks

Push instead of polling — SpotpriceAPI calls your URL on forecast/plan updates. Saves Community quota (120/day). Complements Hours Demand and Forecast. Open REST for any client; Loxone typically via Miniserver HTTP out.

Webhooks mean SpotpriceAPI calls your server when something changes — instead of you asking every few minutes. Two events exist: new forecast ready, or your charging plan changed.

Everyday example

You run a small server at home. You register its URL in the SpotpriceAPI portal. When tomorrow’s day-ahead prices arrive, your server gets a POST: “forecast updated.” It fetches fresh data once — no timer running all night.

When to use / when not

Use when:

  • You want fewer API polls and fewer rate-limit hits
  • Your EMS must react quickly when charge_hours change

Do not use when:

  • You have no server reachable from the internet (use polling or Loxone Virtual Output instead)
  • You only need prices occasionally — hourly polling is fine

What you need

  • HTTPS URL on your server (SpotpriceAPI POSTs to you)
  • Signing secret from the portal (to verify requests are genuine)
  • Optional: Forecast or Hours Demand to fetch full data after the ping

Step by step

1. In the portal go to /settings/webhook (or Partner site card for multiple sites). 2. Enter your HTTPS URL, e.g. https://your-home.example/webhook/spotforecast. 3. Copy the signing secret — store it safely. 4. Click Test in the portal — confirm your server receives the POST. 5. On every request, verify header X-Spotforecast-Signature (see below). 6. On forecast.updated → optionally GET /v1/forecast. On plant.plan_updated → use charge_hours from the payload or re-call Hours Demand.

How verification works

SpotpriceAPI sends header:


X-Spotforecast-Signature: t=<unix_timestamp>,v1=<hex_hmac>

Build the string {timestamp}.{raw_body}, HMAC-SHA256 with your secret, compare to v1. Reject if timestamp is older than ~5 minutes.


import hashlib, hmac, time

def verify(secret, raw_body, header, tolerance=300):
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts = int(parts["t"])
    if abs(time.time() - ts) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{ts}.{raw_body.decode()}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Events

EventWhen it fires
forecast.updatedAfter a new forecast run (scheduled, day-ahead, manual)
plant.plan_updatedWhen hours_needed, charge_hours[], or the O12 discharge cap change materially (including an internal Decide near min SoC, down and up)

Example plant.plan_updated payload


{
  "event": "plant.plan_updated",
  "issued_at": "2026-08-17T18:40:00+02:00",
  "hours_needed": 3,
  "hours_needed_today": 2,
  "energy_charge_kwh": 4,
  "charge_hours_today": [
    { "start": "2026-08-17T19:00:00+02:00", "end": "2026-08-17T20:00:00+02:00" }
  ],
  "hours_demand_url": "https://api.spotpriceapi.com/v1/decision/hours-demand",
  "prefix_hours_needed_today": 2,
  "prefix_period_hours_today": 10,
  "prefix_energy_charge_kwh": 4,
  "charge_now": 1,
  "charge_stop_soc_pct": 72,
  "discharge_power_kw": 9,
  "discharge_reason": "recharge_planned",
  "adaptive_discharge": true,
  "min_soc_pct": 20,
  "charge_power_kw": 7.9,
  "grid_charge_allowed": true,
  "plan_hash": "a1b2c3d4e5f67890"
}

discharge_power_kw (O12) is the full cap except cheap-hold when cover is unsatisfied, near min SoC with no recharge, or while the wallbox is charging (ev_charge_block / ev_charge_limit). Charge hours stay at full cap. A change of the O12 cap also fires plant.plan_updated.

Full week plan: The event only embeds charge_hours_today (compact). After every plant.plan_updated, pull GET /v1/decision/hours-demand with mode=decide for day_plan / full charge_hours. Optional plan_hash (16 chars, SHA-256 of plan starts) for dedupe. Materiality checks today and full plan starts (tomorrow-only changes trigger a push). Pure SOC correction without slot or O12 change (e.g. cover refine v1.9) does not emit an event.

Example forecast.updated payload fields

FieldWhat it means
eventforecast.updated
issued_atWhen the event was sent
model_versionForecast model version
triggere.g. scheduled, manual
forecast_urlWhere to GET full data

Partner accounts may also get a site object (api_key_id, label, customer_ref).

Common mistakes

  • HTTP not HTTPS — registration requires HTTPS (HTTP and private/loopback hosts are rejected)
  • Not verifying signature — anyone could POST fake events to your URL
  • Returning non-2xx — a single delivery attempt is recorded; repeated failures show as “failing” in the portal (no automatic retry)
  • Expecting full forecast JSON in the webhook — usually you GET it yourself after the ping

Next steps

Start with SpotpriceAPI

Register for Community or code samples with your personal key.

Live API status