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_hourschange
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
| Event | When it fires |
|---|---|
forecast.updated | After a new forecast run (scheduled, day-ahead, manual) |
plant.plan_updated | When 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
| Field | What it means |
|---|---|
event | forecast.updated |
issued_at | When the event was sent |
model_version | Forecast model version |
trigger | e.g. scheduled, manual |
forecast_url | Where 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
- Hours Demand — source of
plant.plan_updated - Forecast — refresh after
forecast.updated - Rate limits — why webhooks save quota
- Code Samples — personalized examples
Register for Community or code samples with your personal key.