{"openapi":"3.1.0","info":{"title":"Wolta API","description":"Electricity price data, optimisation grades and profitability calculations for solar and battery systems.\n\nFree for private use. Raw electricity price and ancillary service data redistributes material from Svenska kraftnät under CC BY 4.0 and may be used freely, including commercially, with attribution. Wolta's own calculations (optimisation grade, profitability calculation, sizing) require an agreement for commercial use: kontakt@wolta.se. Terms: https://wolta.se/villkor","termsOfService":"https://wolta.se/villkor","contact":{"name":"Wolta","url":"https://wolta.se/api","email":"kontakt@wolta.se"},"version":"0.80.0"},"servers":[{"url":"https://wolta.se","description":"Production"}],"paths":{"/api/v1/health":{"get":{"tags":["operations"],"summary":"Health check","description":"Returns `{\"status\": \"ok\"}` when the API is up, plus a configuration bit showing that the salt for plant identities is set. For monitoring.","operationId":"health_api_v1_health_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Health Api V1 Health Get"}}}}}}},"/api/v1/prices/now":{"get":{"tags":["prices"],"summary":"Price right now","description":"The spot price for the current hour plus the full series for today and tomorrow for one zone. Prices are given in öre/kWh excluding VAT; the response carries `vat_factor` for anyone who wants to show them including VAT. Tomorrow's series only exists once the day-ahead prices have been published (Wolta fetches them from 13:15 Swedish time).","operationId":"prices_now_api_v1_prices_now_get","parameters":[{"name":"zone","in":"query","required":true,"schema":{"type":"string","title":"Zone"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Prices Now Api V1 Prices Now Get"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/prices":{"get":{"tags":["prices"],"summary":"Historical prices","description":"Price history for one zone between two dates.\n\n- `resolution=15m` gives native rows: hourly values before 2025-10-09, quarter-hourly after.\n- `resolution=1h` gives hourly means, `1d`/`1mo` aggregates per calendar day and month respectively.\n- `unit=ore` converts to öre/kWh using the Riksbank exchange rate (`v` is null when no rate exists for the day); `1d`/`1mo` with `unit=ore` return the mean only.\n- `vat=true` (×1.25) requires `unit=ore`.\n\n`15m` and `1h` accept at most 400 days per call.","operationId":"prices_api_v1_prices_get","parameters":[{"name":"zone","in":"query","required":true,"schema":{"type":"string","title":"Zone"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date","title":"From"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","format":"date","title":"To"}},{"name":"resolution","in":"query","required":false,"schema":{"enum":["15m","1h","1d","1mo"],"type":"string","default":"1h","title":"Resolution"}},{"name":"unit","in":"query","required":false,"schema":{"enum":["eur","ore"],"type":"string","default":"eur","title":"Unit"}},{"name":"vat","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Vat"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Prices Api V1 Prices Get"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stats/compare":{"get":{"tags":["prices"],"summary":"Compare two periods","description":"Compares two arbitrary periods for a zone, for example June this year against June three years ago. Both periods take `from` before `to`, otherwise 422.","operationId":"compare_api_v1_stats_compare_get","parameters":[{"name":"zone","in":"query","required":true,"schema":{"type":"string","title":"Zone"}},{"name":"a_from","in":"query","required":true,"schema":{"type":"string","format":"date","title":"A From"}},{"name":"a_to","in":"query","required":true,"schema":{"type":"string","format":"date","title":"A To"}},{"name":"b_from","in":"query","required":true,"schema":{"type":"string","format":"date","title":"B From"}},{"name":"b_to","in":"query","required":true,"schema":{"type":"string","format":"date","title":"B To"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Compare Api V1 Stats Compare Get"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/stats/records":{"get":{"tags":["prices"],"summary":"Price peaks and troughs","description":"The five most and five least expensive hours and days for a zone. 404 if the zone has no price data.","operationId":"records_api_v1_stats_records_get","parameters":[{"name":"zone","in":"query","required":true,"schema":{"type":"string","title":"Zone"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Records Api V1 Stats Records Get"}}}},"404":{"description":"No data for the requested zone or market."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ancillary/{market}":{"get":{"tags":["prices"],"summary":"Ancillary service markets","description":"Capacity prices (EUR/MW) and activated energy (EUR/MWh) per zone and direction for `afrr`, `mfrr` or `fcr`. At most 400 days per call. An unknown market returns 404.","operationId":"ancillary_api_v1_ancillary__market__get","parameters":[{"name":"market","in":"path","required":true,"schema":{"type":"string","title":"Market"}},{"name":"zone","in":"query","required":true,"schema":{"type":"string","title":"Zone"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date","title":"From"}},{"name":"to","in":"query","required":true,"schema":{"type":"string","format":"date","title":"To"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Ancillary Api V1 Ancillary  Market  Get"}}}},"404":{"description":"No data for the requested zone or market."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/quality":{"get":{"tags":["prices"],"summary":"Data quality and sources","description":"How the price data is composed: source distribution per zone (Svenska kraftnät, ENTSO-E or interpolated) and the most recent ingest runs. Use it to judge how complete the underlying data is before building on it.","operationId":"quality_api_v1_quality_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Quality Api V1 Quality Get"}}}}}}},"/api/v1/simulate/battery":{"get":{"tags":["grades"],"summary":"Arbitrage simulation against historical prices","description":"Simulates what a battery would have earned on actual spot prices from 2020 onwards, with a linear optimisation per day. Gives the theoretical upper bound a real grade is measured against.","operationId":"simulate_battery_api_v1_simulate_battery_get","parameters":[{"name":"zone","in":"query","required":true,"schema":{"type":"string","title":"Zone"}},{"name":"kwh","in":"query","required":true,"schema":{"type":"number","maximum":10000,"exclusiveMinimum":0,"title":"Kwh"}},{"name":"kw","in":"query","required":true,"schema":{"type":"number","maximum":10000,"exclusiveMinimum":0,"title":"Kw"}},{"name":"eff","in":"query","required":false,"schema":{"type":"number","maximum":1,"exclusiveMinimum":0.3,"default":0.9,"title":"Eff"}},{"name":"factor","in":"query","required":false,"schema":{"type":"number","maximum":1,"exclusiveMinimum":0,"default":0.36,"title":"Factor"}},{"name":"cost","in":"query","required":false,"schema":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Cost"}},{"name":"cycles","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":10,"exclusiveMinimum":0},{"type":"null"}],"title":"Cycles"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Simulate Battery Api V1 Simulate Battery Get"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/simulate/household":{"get":{"tags":["plant"],"summary":"Household economics for solar and battery","description":"Computes IRR, payback and a value breakdown for solar and battery. `mode=decision` answers whether an investment pays off, `mode=history` what an investment already made has returned. `future_price_factor` scales the future price level in the projection (the Low, Medium and High scenarios).\n\nA warm cache returns 200 directly; a cold path returns 202 with `warm.config_key`, which is polled via `GET /warm/by-config`, after which the call is repeated. The plant's profile token is sent in `Authorization: Bearer`; the `profile_token` query parameter remains as a deprecated fallback for already-shipped web clients.","operationId":"simulate_household_api_v1_simulate_household_get","parameters":[{"name":"zone","in":"query","required":true,"schema":{"type":"string","title":"Zone"}},{"name":"mode","in":"query","required":true,"schema":{"type":"string","title":"Mode"}},{"name":"pv_kwp","in":"query","required":false,"schema":{"type":"number","maximum":500,"exclusiveMinimum":0,"default":10,"title":"Pv Kwp"}},{"name":"load_annual","in":"query","required":false,"schema":{"type":"number","maximum":200000,"exclusiveMinimum":0,"default":20000,"title":"Load Annual"}},{"name":"has_battery","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Has Battery"}},{"name":"kwh","in":"query","required":false,"schema":{"type":"number","maximum":10000,"exclusiveMinimum":0,"default":10,"title":"Kwh"}},{"name":"kw","in":"query","required":false,"schema":{"type":"number","maximum":10000,"exclusiveMinimum":0,"default":5,"title":"Kw"}},{"name":"eff","in":"query","required":false,"schema":{"type":"number","maximum":1,"exclusiveMinimum":0.3,"default":0.9,"title":"Eff"}},{"name":"realism_factor","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"exclusiveMinimum":0},{"type":"null"}],"title":"Realism Factor"}},{"name":"selfuse_capture","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"exclusiveMinimum":0},{"type":"null"}],"title":"Selfuse Capture"}},{"name":"solar_export_capture","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"exclusiveMinimum":0},{"type":"null"}],"title":"Solar Export Capture"}},{"name":"capex_battery","in":"query","required":false,"schema":{"type":"number","minimum":0,"default":0,"title":"Capex Battery"}},{"name":"capex_solar","in":"query","required":false,"schema":{"type":"number","minimum":0,"default":0,"title":"Capex Solar"}},{"name":"persons","in":"query","required":false,"schema":{"type":"integer","maximum":10,"minimum":1,"default":1,"title":"Persons"}},{"name":"green_mode","in":"query","required":false,"schema":{"type":"string","default":"off","title":"Green Mode"}},{"name":"green_manual","in":"query","required":false,"schema":{"type":"number","minimum":0,"default":0,"title":"Green Manual"}},{"name":"lifetime_years","in":"query","required":false,"schema":{"type":"integer","maximum":40,"minimum":5,"default":30,"title":"Lifetime Years"}},{"name":"battery_life_years","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":40,"minimum":1},{"type":"null"}],"title":"Battery Life Years"}},{"name":"max_cycles","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":15000,"minimum":2000},{"type":"null"}],"title":"Max Cycles"}},{"name":"house_type","in":"query","required":false,"schema":{"type":"string","default":"heated","title":"House Type"}},{"name":"battery_degradation","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":0.2,"minimum":0},{"type":"null"}],"title":"Battery Degradation"}},{"name":"solar_degradation","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":0.2,"minimum":0},{"type":"null"}],"title":"Solar Degradation"}},{"name":"inverter_replace","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Inverter Replace"}},{"name":"inverter_replace_year","in":"query","required":false,"schema":{"type":"integer","maximum":40,"minimum":1,"default":15,"title":"Inverter Replace Year"}},{"name":"inverter_replace_cost","in":"query","required":false,"schema":{"type":"number","minimum":0,"default":20000,"title":"Inverter Replace Cost"}},{"name":"future_price_factor","in":"query","required":false,"schema":{"type":"number","maximum":3,"exclusiveMinimum":0,"default":1,"title":"Future Price Factor"}},{"name":"battery_only_economics","in":"query","required":false,"schema":{"type":"boolean","description":"Compute IRR and payback on the battery's value stream alone. Use this when the solar panels are already paid for and `capex_solar` is therefore 0.","default":false,"title":"Battery Only Economics"},"description":"Compute IRR and payback on the battery's value stream alone. Use this when the solar panels are already paid for and `capex_solar` is therefore 0."},{"name":"purchase_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"}},{"name":"paid","in":"query","required":false,"schema":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Paid"}},{"name":"purchase_date_solar","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date Solar"}},{"name":"purchase_date_battery","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date Battery"}},{"name":"paid_solar","in":"query","required":false,"schema":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Paid Solar"}},{"name":"paid_battery","in":"query","required":false,"schema":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Paid Battery"}},{"name":"profile_token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Profile Token"}},{"name":"pv_orient","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Pv Orient"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Simulate Household Api V1 Simulate Household Get"}}}},"202":{"description":"The calculation was queued (cold cache). The response carries `warm.config_key`; poll `GET /warm/by-config`, then repeat the original call to fetch the result."},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/link":{"post":{"tags":["plant"],"summary":"Get or create the plant's read link","description":"As `POST /profile/{token}/link`, with the token in `Authorization: Bearer`.","operationId":"mint_link_bearer_api_v1_profile_link_post","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Mint Link Bearer Api V1 Profile Link Post"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."},"503":{"description":"The link feature is not enabled on the server (encryption key missing)."}},"security":[{"ProfileToken":[]}]},"delete":{"tags":["plant"],"summary":"Remove the plant's read link","description":"As `DELETE /profile/{token}/link`, with the token in `Authorization: Bearer`.","operationId":"revoke_link_bearer_api_v1_profile_link_delete","responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."}},"security":[{"ProfileToken":[]}]}},"/api/v1/profile/link/rotate":{"post":{"tags":["plant"],"summary":"Replace the plant's read link","description":"As `POST /profile/{token}/link/rotate`, with the token in `Authorization: Bearer`.","operationId":"rotate_link_bearer_api_v1_profile_link_rotate_post","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Rotate Link Bearer Api V1 Profile Link Rotate Post"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."},"503":{"description":"The link feature is not enabled on the server (encryption key missing)."}},"security":[{"ProfileToken":[]}]}},"/api/v1/profile/claim-code":{"post":{"tags":["plant"],"summary":"Create linking code","description":"As `POST /profile/{token}/claim-code`, with the token in `Authorization: Bearer`.","operationId":"mint_claim_code_bearer_api_v1_profile_claim_code_post","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Mint Claim Code Bearer Api V1 Profile Claim Code Post"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."}},"security":[{"ProfileToken":[]}]}},"/api/v1/profile":{"get":{"tags":["plant"],"summary":"Plant parameters","description":"The plant's saved parameters: zone, battery, solar, control system, purchase (including any split between solar and battery), custom tariff and reserve floor, plus the optimisation grade. No hourly data and no personal data beyond what was entered.","operationId":"get_profile_api_v1_profile_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Get Profile Api V1 Profile Get"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."}},"security":[{"ProfileToken":[]}]},"post":{"tags":["plant"],"summary":"Create plant","description":"Creates a plant for continuous uploads and returns **201** with `profile_token` (plus `account_linked`). The token is the key to the plant and is sent as `Authorization: Bearer` on the calls below. It is shown here only: Wolta stores it hashed.\n\nRequired: `zone`, `battery_kwh` and `battery_kw` (both > 0, this path requires a battery). Optional: `eff` (default 0.9), `has_solar`, control system (`control_system`, strictly validated), purchase price and date, custom tariff and reserve floor. The same path the Home Assistant integration uses.","operationId":"create_profile_api_v1_profile_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileCreate"}}},"required":true},"responses":{"201":{"description":"The resource was created.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Create Profile Api V1 Profile Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["plant"],"summary":"Update the plant","description":"Updates a saved plant without a new upload. Economics, custom tariff (`null` restores the country default) and reserve floor are cheap changes; a changed battery or solar size recalculates the grade on the stored operating data.\n\nThe price zone may be corrected if it was set wrong at creation, but only within the same currency - the stored amounts are expressed in the zone's currency and are not converted. A change that crosses the currency boundary returns 422; delete and recreate the plant instead. A correction recalculates the grade and the economics against the new price series, so the figures will change. The same value as already stored is a no-op.","operationId":"patch_profile_api_v1_profile_patch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilePatch"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Patch Profile Api V1 Profile Patch"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ProfileToken":[]}]}},"/api/v1/profile/adopt":{"post":{"tags":["plant"],"summary":"Convert the plant to an integration","description":"Converts a plant created through the website into an integration plant, so that it can receive continuous data. The Home Assistant integration calls it when connecting to an existing plant. Idempotent: already converted returns `{\"adopted\": false}`. Plants without a battery return 422.","operationId":"adopt_profile_api_v1_profile_adopt_post","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/AdoptRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Adopt Profile Api V1 Profile Adopt Post"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ProfileToken":[]}]}},"/api/v1/profile/data":{"put":{"tags":["plant"],"summary":"Append operating data","description":"Appends operating data continuously: `{rows: [...]}`. Each row requires `ts` **with a time zone** (a naive timestamp returns 422 for the whole batch), `batt_charged_kwh` and `batt_discharged_kwh`. `solar_kwh`, `grid_import_kwh` and `grid_export_kwh` are optional but **default to 0**: omit the grid flows and zeros are stored without an error, and the grade is computed on a house that never imports electricity.\n\nAll energy values must lie between 0 and 500 per row. Battery flows are additionally sanity-checked per clock hour against already stored state: if the hour's charging or discharging exceeds the battery's power rating with margin (×1.5), the hour's battery fields are zeroed instead of the batch being rejected, and `repaired_battery_rows` in the response counts them. A corrected resend writes real values again.\n\nUpsert is on the exact timestamp, so resending the same rows is safe. If you change resolution (hourly to quarter-hourly) or shift the timestamps, the old rows remain and the day is double-counted: delete the plant and start over instead. At most 40,000 rows per call (more returns **422**) and 80,000 rows per plant in total (**413**).","operationId":"put_data_api_v1_profile_data_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataPut"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Put Data Api V1 Profile Data Put"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."},"413":{"description":"The plant's total storage cap (80,000 rows) has been reached. Delete the plant and create a new one."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"ProfileToken":[]}]}},"/api/v1/profile/recompute":{"post":{"tags":["plant"],"summary":"Request a recalculation","description":"Requests a recalculation of grade and economics after new data. Returns **202**; a background calculation takes over and is followed via `GET /warm`. Cooldown 24 hours (429 with `Retry-After` if it already ran). A parameter change via PATCH clears the cooldown.","operationId":"recompute_api_v1_profile_recompute_post","responses":{"202":{"description":"The request was accepted and queued. Poll for the result.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Recompute Api V1 Profile Recompute Post"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."},"429":{"description":"The recalculation already ran within the cooldown window. `Retry-After` gives when the next one is allowed."}},"security":[{"ProfileToken":[]}]}},"/api/v1/profile/results":{"get":{"tags":["plant"],"summary":"Latest result","description":"The latest completed calculation: optimisation grade with component breakdown and applied tariff, economics and job status. Only reads what has already been calculated.\n\n`decision`/`history` being null does not always mean the calculation is still running: economics only exist for Swedish bidding zones (other zones get a grade but never economics) and require at least 30 days of data. So do not poll waiting for economics outside Sweden or on a shorter window - `status` may well be `done`.","operationId":"results_api_v1_profile_results_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Results Api V1 Profile Results Get"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."}},"security":[{"ProfileToken":[]}]}},"/api/v1/calibration":{"delete":{"tags":["plant"],"summary":"Delete the plant","description":"Deletes the plant and all its data: profile, hourly data, grades and cache. Cannot be undone. The token is sent in `Authorization: Bearer`.","operationId":"delete_submission_api_v1_calibration_delete","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Delete Submission Api V1 Calibration Delete"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."}},"security":[{"ProfileToken":[]}]}},"/api/v1/calibration/ping":{"get":{"tags":["operations"],"summary":"Simple reachability check","description":"Returns `{\"ok\": true}`. Used to check that the API is reachable over the network.","operationId":"ping_api_v1_calibration_ping_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Ping Api V1 Calibration Ping Get"}}}}}}},"/api/v1/calibration/export-script":{"get":{"tags":["grades"],"summary":"Export script for Home Assistant","description":"Downloads `export_energy_csv.py`, the helper script that extracts operating data from Home Assistant in the format the upload paths expect.","operationId":"export_script_api_v1_calibration_export_script_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{}}}}}}},"/api/v1/calibration/compute":{"post":{"tags":["grades"],"summary":"Price-responsiveness factor from your own data","description":"Uploads your own operating data (CSV) and computes a personal price-responsiveness factor, that is, how well consumption follows price. `store=true` saves the profile and returns a profile token. The zone must be SE1-SE4.","operationId":"compute_api_v1_calibration_compute_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_compute_api_v1_calibration_compute_post"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Compute Api V1 Calibration Compute Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/calibration/summary":{"get":{"tags":["grades"],"summary":"Comparison data behind the grade","description":"The aggregate individual grades are measured against: price-responsiveness factor (median, p25/p75) and a grade block (median and max) across the plants in the comparison data, broken down per zone and control system.\n\n`total_plants` is the number of plants with accepted data. The other `n` fields are **effective** sample size (Kish) on the weighted set, not a row count: a fleet from a single partner is weighted down as a whole, so `n` can be lower than the number of plants behind the figure. Segments whose effective `n` falls below 5 are omitted entirely.","operationId":"summary_api_v1_calibration_summary_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Summary Api V1 Calibration Summary Get"}}}}}}},"/api/v1/calibration/{token}":{"delete":{"tags":["plant"],"summary":"Delete the plant (token in path)","description":"The same deletion as `DELETE /calibration`, with the token in the path. 404 if the token is unknown.","operationId":"delete_submission_api_v1_calibration__token__delete","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Delete Submission Api V1 Calibration  Token  Delete"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}":{"get":{"tags":["plant"],"summary":"Plant parameters (token in path)","description":"The same response as `GET /profile`, with the token in the path instead of the `Authorization` header. New clients should use the header variant: a token in a URL ends up in logs and browser history.","operationId":"get_profile_api_v1_profile__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Profile Api V1 Profile  Token  Get"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["plant"],"summary":"Update the plant (token in path)","description":"The same semantics as `PATCH /profile`, with the token in the path. New clients should use the header variant.","operationId":"patch_profile_api_v1_profile__token__patch","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilePatch"}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Patch Profile Api V1 Profile  Token  Patch"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}/link":{"post":{"tags":["plant"],"summary":"Get or create the plant's read link","description":"Returns the plant's read-link token (`wpl_…`), creating it if it does not exist. **Idempotent**: repeated calls return the same token and have no side effects. The link grants read access to the whole plant view but writes nothing — never economy, deletion, a new token, sharing or technical fields. Replace the token with `/link/rotate`.","operationId":"mint_link_api_v1_profile__token__link_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mint Link Api V1 Profile  Token  Link Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The link feature is not enabled on the server (encryption key missing)."}}},"delete":{"tags":["plant"],"summary":"Remove the plant's read link","description":"Revokes the read link. Idempotent: returns 204 even when no link exists. Works even when the link feature is otherwise disabled – being able to revoke a leaked link must never depend on the server's encryption key.","operationId":"revoke_link_api_v1_profile__token__link_delete","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"204":{"description":"Successful response, empty body."},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}/link/rotate":{"post":{"tags":["plant"],"summary":"Replace the plant's read link","description":"Creates a new read link. The old one stops working immediately. Rotation never happens as a side effect of any other call.","operationId":"rotate_link_api_v1_profile__token__link_rotate_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Rotate Link Api V1 Profile  Token  Link Rotate Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The link feature is not enabled on the server (encryption key missing)."}}}},"/api/v1/profile/{token}/claim-code":{"post":{"tags":["plant"],"summary":"Create linking code","description":"Creates a one-time code that links the plant to a wolta.se account. The code is valid for 10 minutes and can only be used once; a new code replaces an earlier unused one. Redeem it under Account → Link plant while signed in.","operationId":"mint_claim_code_api_v1_profile__token__claim_code_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mint Claim Code Api V1 Profile  Token  Claim Code Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}/adopt":{"post":{"tags":["plant"],"summary":"Convert the plant to an integration (token in path)","description":"The same semantics as `POST /profile/adopt`, with the token in the path. An optional `client_plant_id` in the body stamps the plant's stable identity, so it is recognised if the integration is reinstalled.","operationId":"adopt_profile_api_v1_profile__token__adopt_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/AdoptRequest"},{"type":"null"}],"title":"Body"}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Adopt Profile Api V1 Profile  Token  Adopt Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}/data":{"put":{"tags":["plant"],"summary":"Append operating data (token in path)","description":"The same semantics and limits as `PUT /profile/data`, with the token in the path. New clients should use the header variant.","operationId":"put_data_api_v1_profile__token__data_put","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataPut"}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Put Data Api V1 Profile  Token  Data Put"}}}},"404":{"description":"Unknown token."},"413":{"description":"The plant's total storage cap (80,000 rows) has been reached. Delete the plant and create a new one."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}/recompute":{"post":{"tags":["plant"],"summary":"Request a recalculation (token in path)","description":"The same semantics as `POST /profile/recompute`, with the token in the path.","operationId":"recompute_api_v1_profile__token__recompute_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"202":{"description":"The request was accepted and queued. Poll for the result.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Recompute Api V1 Profile  Token  Recompute Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"429":{"description":"The recalculation already ran within the cooldown window. `Retry-After` gives when the next one is allowed."}}}},"/api/v1/profile/{token}/results":{"get":{"tags":["plant"],"summary":"Latest result (token in path)","description":"The same response as `GET /profile/results`, with the token in the path. Never runs a calculation in the request path; everything is read from cache.","operationId":"results_api_v1_profile__token__results_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Results Api V1 Profile  Token  Results Get"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/grade/check-plant":{"post":{"tags":["grades"],"summary":"Does the plant already exist?","description":"A cheap check of whether the plant is already saved, matched on a salted fingerprint of sensor names and zone. Returns `{match, can_update}` and computes no grade.\n\n`can_update` says whether **the caller** may replace the matched row, that is, whether they prove possession (the row's own profile token, or a signed-in session that owns it). A match alone grants no write access, and a plant streaming through an integration is never replaced this way. POST precisely so that the proof never ends up in a path.","operationId":"check_plant_api_v1_grade_check_plant_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckPlantIn"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Check Plant Api V1 Grade Check Plant Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/grade":{"post":{"tags":["grades"],"summary":"Upload operating data for a grade","description":"Starts a grade calculation. Validates synchronously (zone, battery and CSV return 422 immediately), stores the data and queues the calculation: the response is **202** with `profile_token` and `status: pending`. The grade is then fetched by polling `GET /warm` with the token in `Authorization: Bearer`.\n\nRequires at least 7 days of data. 7-29 days give a preliminary grade (`preliminary: true`) that can swing and that neither enters the comparison data nor unlocks economics; a full grade requires 30 days.\n\nAn anonymous grade (number and bidding zone) is always saved for the comparison. `share_profile` is opt-in and controls whether the raw hourly data is kept. If `profile_token` is sent instead of a file, the grade is recomputed on already stored data.","operationId":"grade_api_v1_grade_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_grade_api_v1_grade_post"}}},"required":true},"responses":{"202":{"description":"The upload was accepted and the calculation queued. The response carries `profile_token` and `status: pending`; poll `GET /warm`.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Grade Api V1 Grade Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sizing":{"post":{"tags":["plant"],"summary":"Optimal battery size","description":"Sweeps battery sizes against an operating profile and reports, per size, annual value, cycle life and economics (NPV, IRR and payback), both for a new battery from scratch and as a marginal expansion from the current size. The value is scaled by the measured skill of the actual control, not by a theoretical optimum.\n\nTakes the same CSV format as `POST /grade`, or `profile_token` for an already saved plant. A cold path returns 202 with `warm.config_key`: poll `GET /warm/by-config` and repeat the call once the calculation is done.","operationId":"sizing_endpoint_api_v1_sizing_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_sizing_endpoint_api_v1_sizing_post"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Sizing Endpoint Api V1 Sizing Post"}}}},"202":{"description":"The calculation was queued (cold cache). The response carries `warm.config_key`; poll `GET /warm/by-config`, then repeat the original call to fetch the result."},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/warm/by-config/{config_key}":{"get":{"tags":["grades"],"summary":"Status of a standalone calculation","description":"Status of a queued calculation without a plant (household simulation or sizing). `config_key` comes from the 202 response of the calculation that was queued. Poll until `done`, then repeat the original call, which then returns 200 with the result. 404 if the key is unknown.","operationId":"warm_by_config_api_v1_warm_by_config__config_key__get","parameters":[{"name":"config_key","in":"path","required":true,"schema":{"type":"string","title":"Config Key"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Warm By Config Api V1 Warm By Config  Config Key  Get"}}}},"404":{"description":"Unknown job."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/warm":{"get":{"tags":["grades"],"summary":"Status of a queued calculation","description":"Polled after `POST /grade` until the calculation is done: `status` is `pending`, `running`, `done` or `error`, with `step` and `steps_done`/`steps_total` for progress. Once the first step is complete, the grade is included in the response.\n\nThe plant's profile token is sent in `Authorization: Bearer`.","operationId":"warm_status_bearer_api_v1_warm_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Warm Status Bearer Api V1 Warm Get"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."}},"security":[{"ProfileToken":[]}]}},"/api/v1/warm/{token}":{"get":{"tags":["grades"],"summary":"Status of a queued calculation (token in path)","description":"Deprecated variant of `GET /warm` with the token in the path. A token in a URL ends up in logs, browser history and `Referer` (CWE-598), so new clients should send it in `Authorization: Bearer`. The endpoint remains until already-shipped web clients have aged out, and is then removed.","operationId":"warm_status_api_v1_warm__token__get","deprecated":true,"parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Warm Status Api V1 Warm  Token  Get"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/share":{"post":{"tags":["sharing"],"summary":"Create a share link","description":"Creates a shareable, public link to a plant's grade from its profile token. `wear_on` controls whether wear is included in the shared view. Returns 201 with the link id. An unknown token returns 404.","operationId":"share_create_api_v1_share_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareCreate"}}},"required":true},"responses":{"201":{"description":"The resource was created.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Share Create Api V1 Share Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/import/solaredge":{"post":{"tags":["integrations"],"summary":"Import from SolarEdge","description":"Fetches historical production and battery data from SolarEdge's monitoring platform using an API key the owner obtained themselves, and stores it as operating data. Runs as a job: the response carries `job_id`, polled via the status endpoint below. Price zone is required when the plant is created (step 2, with `site_id` set) – if it is missing, the call returns 422 instead of assuming a value.","operationId":"import_solaredge_api_v1_import_solaredge_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportRequest"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Import Solaredge Api V1 Import Solaredge Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/import/{job_id}/status":{"get":{"tags":["integrations"],"summary":"Status of an import job","description":"Follows a running SolarEdge job: how far it has come and whether it failed. Requires the job's token. An unknown job returns 404.","operationId":"import_status_api_v1_import__job_id__status_get","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"integer","title":"Job Id"}},{"name":"token","in":"query","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Import Status Api V1 Import  Job Id  Status Get"}}}},"404":{"description":"Unknown job."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/interest":{"post":{"tags":["integrations"],"summary":"Register interest in a control system","description":"Registers that someone wants support for a control system Wolta does not yet connect to. The email address is optional and is used only for a one-time message when support exists. The same address and system twice counts once. An unknown system returns 422.","operationId":"register_interest_api_v1_interest_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InterestIn"}}},"required":true},"responses":{"201":{"description":"The resource was created.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Register Interest Api V1 Interest Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reduxi/onboard":{"post":{"tags":["integrations"],"summary":"Connect a Reduxi control system","description":"Provisions an account on Wolta's MQTT bridge for a Reduxi control system and creates the plant. The response carries the broker address, fresh credentials and the plant's `profile_token`. The details are shown here only.","operationId":"onboard_api_v1_reduxi_onboard_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/elpris_api__api__reduxi_onboard__OnboardRequest"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Onboard Api V1 Reduxi Onboard Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reduxi/{token}/status":{"get":{"tags":["integrations"],"summary":"Status of the Reduxi connection","description":"Shows whether the bridge receives data for the plant, and when it last did.","operationId":"status_api_v1_reduxi__token__status_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Status Api V1 Reduxi  Token  Status Get"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reduxi/{token}/rotate":{"post":{"tags":["integrations"],"summary":"Rotate the Reduxi password","description":"Re-provisions the same unit with a new broker password. The old one stops working, so the control system's settings must be updated.","operationId":"rotate_api_v1_reduxi__token__rotate_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Rotate Api V1 Reduxi  Token  Rotate Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reduxi/{token}":{"delete":{"tags":["integrations"],"summary":"Disconnect Reduxi","description":"Removes the broker account and deletes the plant along with its profile, binding and cache.","operationId":"offboard_api_v1_reduxi__token__delete","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Offboard Api V1 Reduxi  Token  Delete"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/sonnen/onboard":{"post":{"tags":["integrations"],"summary":"Connect a sonnenBatterie","description":"Self-service for sonnenBatterie owners: creates a plant and a webhook address the battery posts its operating data to. The ingest token is shown **once** and stored hashed, so it must be saved immediately.","operationId":"onboard_api_v1_sonnen_onboard_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/elpris_api__api__sonnen__OnboardRequest"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Onboard Api V1 Sonnen Onboard Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ingest/sonnen/{ingest_token}":{"post":{"tags":["integrations"],"summary":"Webhook receiver for Sonnen","description":"Receives the battery's own webhook calls. Called by the battery, not by an integration: the address comes from the connection flow above.\n\nThe timestamp is set on receipt, because the battery's own timestamp is local time without a time zone and therefore cannot be trusted. The battery does not read the response; the status codes exist for troubleshooting.","operationId":"receive_webhook_api_v1_ingest_sonnen__ingest_token__post","parameters":[{"name":"ingest_token","in":"path","required":true,"schema":{"type":"string","title":"Ingest Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Receive Webhook Api V1 Ingest Sonnen  Ingest Token  Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}/sonnen":{"get":{"tags":["integrations"],"summary":"Status of the Sonnen connection (token in path)","description":"The same status as the account path, for whoever holds the plant's profile token.","operationId":"sonnen_status_token_api_v1_profile__token__sonnen_get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sonnen Status Token Api V1 Profile  Token  Sonnen Get"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["integrations"],"summary":"Disconnect the sonnenBatterie (token in path)","description":"Disconnects the battery from the plant behind the profile token.","operationId":"sonnen_detach_token_api_v1_profile__token__sonnen_delete","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sonnen Detach Token Api V1 Profile  Token  Sonnen Delete"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}/sonnen/attach":{"post":{"tags":["integrations"],"summary":"Connect a sonnenBatterie (token in path)","description":"Creates the webhook address for the plant behind the profile token. The ingest token is shown once.","operationId":"sonnen_attach_token_api_v1_profile__token__sonnen_attach_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sonnen Attach Token Api V1 Profile  Token  Sonnen Attach Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/profile/{token}/sonnen/rotate":{"post":{"tags":["integrations"],"summary":"Rotate the Sonnen token (token in path)","description":"Mints a new ingest token. The old one stops working immediately.","operationId":"sonnen_rotate_token_api_v1_profile__token__sonnen_rotate_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sonnen Rotate Token Api V1 Profile  Token  Sonnen Rotate Post"}}}},"404":{"description":"Unknown token."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/partner/me":{"get":{"tags":["partner"],"summary":"Your partner status and limits","description":"Your partnership's current status: name, tier, number of plants and the limits that apply to you (`plant_limit` and `rate_max_per_min`).\n\nRead the limits from here instead of hard-coding them: they change when the partnership is upgraded. A good first call to verify that the key works.","operationId":"me_api_v1_partner_me_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartnerMeOut"}}}},"401":{"description":"Missing or invalid key. Check that the header reads exactly `Authorization: Bearer wpk_...`.","content":{"application/json":{"example":{"detail":"Authorization: Bearer wpk_... krävs"}}}},"403":{"description":"The partner account is suspended. Contact partner@wolta.se.","content":{"application/json":{"example":{"detail":"partnern är avstängd"}}}},"429":{"description":"The rate budget is exhausted. `Retry-After` gives the number of seconds to wait.","content":{"application/json":{"example":{"detail":"partner-budgeten är slut, försök strax"}}}}},"security":[{"PartnerKey":[]}]}},"/api/v1/partner/plants":{"post":{"tags":["partner"],"summary":"Create plant","description":"Creates a plant corresponding to one of your battery installations. Returns **201** with `link_token`, the end-customer link's key, which is shown in this response only.\n\nThe call is idempotent on your `client_plant_id`: the same id again returns **200** with `link_token: null` and leaves the existing row untouched, even if the body differs. Use `PATCH` to change a plant that already exists. The idempotency check runs before the tier limit, so a repeated call succeeds even when the quota is full.\n\n`client_plant_id` is used as a path segment in the other calls and must therefore be URL-safe: spaces, `/`, `?`, `#`, `%` and backslashes return 422. Other characters are fine, at most 128.","operationId":"create_plant_api_v1_partner_plants_post","security":[{"PartnerKey":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlantCreate"}}}},"responses":{"200":{"description":"The plant already existed (idempotent repeat). `link_token` is null.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlantCreated"}}}},"201":{"description":"The plant was created. `link_token` is shown here and nowhere else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlantCreated"}}}},"401":{"description":"Missing or invalid key. Check that the header reads exactly `Authorization: Bearer wpk_...`.","content":{"application/json":{"example":{"detail":"Authorization: Bearer wpk_... krävs"}}}},"403":{"description":"The partner account is suspended. Contact partner@wolta.se.","content":{"application/json":{"example":{"detail":"partnern är avstängd"}}}},"409":{"description":"The plant limit for your tier has been reached. Delete an unused plant or request an upgrade.","content":{"application/json":{"example":{"detail":{"error":"plant_limit_reached","plant_limit":10}}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"429":{"description":"The rate budget is exhausted. `Retry-After` gives the number of seconds to wait.","content":{"application/json":{"example":{"detail":"partner-budgeten är slut, försök strax"}}}}}},"get":{"tags":["partner"],"summary":"List the fleet","description":"Your fleet with data coverage and grade status per plant: `first_hour`, `last_hour` and `n_days` (number of distinct local days with data, counted time-zone aware). Use them to spot gaps in your data feed.\n\n`total` is the entire fleet, not the number of rows on this page. `limit` must be greater than 0 and is silently capped at 500.","operationId":"list_plants_api_v1_partner_plants_get","security":[{"PartnerKey":[]}],"parameters":[{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","exclusiveMinimum":0,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlantsPage"}}}},"401":{"description":"Missing or invalid key. Check that the header reads exactly `Authorization: Bearer wpk_...`.","content":{"application/json":{"example":{"detail":"Authorization: Bearer wpk_... krävs"}}}},"403":{"description":"The partner account is suspended. Contact partner@wolta.se.","content":{"application/json":{"example":{"detail":"partnern är avstängd"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"429":{"description":"The rate budget is exhausted. `Retry-After` gives the number of seconds to wait.","content":{"application/json":{"example":{"detail":"partner-budgeten är slut, försök strax"}}}}}}},"/api/v1/partner/plants/{client_plant_id}":{"patch":{"tags":["partner"],"summary":"Update plant","description":"Updates the technical fields on a plant. Only the fields you send are changed.\n\nThe partner owns `control_system`, `battery_kwh`, `battery_kw`, `eff`, `nameplate_kwh` and `reserve_pct`. The end customer's fields (economics, electricity contract, grading window) cannot be set from here: such a field in the body returns **403** and nothing is written.\n\nNever send `null` to clear a field; that returns **422**. Omit the field instead. `zone` is accepted in the body but only with the same value already stored (a no-op), because the zone drives currency, tax, tariff and time zone for the entire grade calculation. A changed value returns 422.\n\nThe response contains `client_plant_id` plus the partner-owned fields after the update, never the end customer's values.","operationId":"patch_plant_api_v1_partner_plants__client_plant_id__patch","security":[{"PartnerKey":[]}],"parameters":[{"name":"client_plant_id","in":"path","required":true,"schema":{"type":"string","title":"Client Plant Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlantPatched"}}}},"401":{"description":"Missing or invalid key. Check that the header reads exactly `Authorization: Bearer wpk_...`.","content":{"application/json":{"example":{"detail":"Authorization: Bearer wpk_... krävs"}}}},"403":{"description":"The body contained a field owned by the end customer (economics, electricity contract or grading window). Nothing was written. Alternatively, the partner account is suspended.","content":{"application/json":{"example":{"detail":{"error":"field_not_partner_owned","field":"cost_sek"}}}}},"404":{"description":"Unknown `client_plant_id` for your partner account. You never see other partners' plants, so a plant belonging to someone else also returns 404.","content":{"application/json":{"example":{"detail":"okänd anläggning"}}}},"422":{"description":"Validation error: invalid zone or control system, a value outside the allowed range, or an explicit `null` on a partner-owned field (omit the field instead of clearing it).","content":{"application/json":{"example":{"detail":{"error":"field_must_not_be_null","field":"battery_kwh","message":"battery_kwh får inte vara null - utelämna fältet i stället"}}}}},"429":{"description":"The rate budget is exhausted. `Retry-After` gives the number of seconds to wait.","content":{"application/json":{"example":{"detail":"partner-budgeten är slut, försök strax"}}}}}},"delete":{"tags":["partner"],"summary":"Delete plant","description":"Decommissions a plant: the binding, hourly data and grades are deleted, and a slot in the tier limit is freed. Returns **204** with an empty body.\n\nDeletes one plant at a time. There is deliberately no bulk deletion of an entire fleet, as protection against a bug in an integration wiping all data.","operationId":"delete_plant_api_v1_partner_plants__client_plant_id__delete","security":[{"PartnerKey":[]}],"parameters":[{"name":"client_plant_id","in":"path","required":true,"schema":{"type":"string","title":"Client Plant Id"}}],"responses":{"204":{"description":"The plant was deleted. Empty body."},"401":{"description":"Missing or invalid key. Check that the header reads exactly `Authorization: Bearer wpk_...`.","content":{"application/json":{"example":{"detail":"Authorization: Bearer wpk_... krävs"}}}},"403":{"description":"The partner account is suspended. Contact partner@wolta.se.","content":{"application/json":{"example":{"detail":"partnern är avstängd"}}}},"404":{"description":"Unknown `client_plant_id` for your partner account. You never see other partners' plants, so a plant belonging to someone else also returns 404.","content":{"application/json":{"example":{"detail":"okänd anläggning"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"429":{"description":"The rate budget is exhausted. `Retry-After` gives the number of seconds to wait.","content":{"application/json":{"example":{"detail":"partner-budgeten är slut, försök strax"}}}}}}},"/api/v1/partner/plants/{client_plant_id}/link/rotate":{"post":{"tags":["partner"],"summary":"Rotate the end-customer link","description":"Mints a new `link_token` for the plant and returns it once. The old link stops working immediately, and whoever opens it meets a page saying the link is no longer valid.\n\nThere is only one valid link token per plant at a time. Use rotation when a link has leaked or when a plant has changed owner.","operationId":"rotate_link_api_v1_partner_plants__client_plant_id__link_rotate_post","security":[{"PartnerKey":[]}],"parameters":[{"name":"client_plant_id","in":"path","required":true,"schema":{"type":"string","title":"Client Plant Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkRotated"}}}},"401":{"description":"Missing or invalid key. Check that the header reads exactly `Authorization: Bearer wpk_...`.","content":{"application/json":{"example":{"detail":"Authorization: Bearer wpk_... krävs"}}}},"403":{"description":"The partner account is suspended. Contact partner@wolta.se.","content":{"application/json":{"example":{"detail":"partnern är avstängd"}}}},"404":{"description":"Unknown `client_plant_id` for your partner account. You never see other partners' plants, so a plant belonging to someone else also returns 404.","content":{"application/json":{"example":{"detail":"okänd anläggning"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"429":{"description":"The rate budget is exhausted. `Retry-After` gives the number of seconds to wait.","content":{"application/json":{"example":{"detail":"partner-budgeten är slut, försök strax"}}}}}}},"/api/v1/partner/plants/{client_plant_id}/data":{"post":{"tags":["partner"],"summary":"Send hourly data","description":"Sends in hourly data, both for historical backfill and continuous sync. The body is a JSON object with `rows`, which must contain at least one row.\n\n`ts_utc` **must carry a time zone**, either `Z` or an explicit offset. A naive timestamp returns 422: Wolta never guesses local time. The energy fields are optional with a default of 0 and allowed values 0-500 kWh per hour. If you hit the cap, the most common cause is a value sent in Wh instead of kWh.\n\nInsertion is an upsert per plant and hour, so resending the same window is safe. At most 5,000 rows and 2 MiB per call (**413** above the cap), and 80,000 rows per plant in total.\n\nThe response's `inserted_hours`, `first` and `last` describe **this batch only**, not the plant's entire history. `repaired_battery_rows` counts hours whose battery flows were zeroed for breaking physical limits; a value above 0 indicates a faulty sensor configuration or a jump in the data.\n\nGrades are not computed per push: they are recalculated in batches when new prices land.","operationId":"append_partner_data_api_v1_partner_plants__client_plant_id__data_post","security":[{"PartnerKey":[]}],"parameters":[{"name":"client_plant_id","in":"path","required":true,"schema":{"type":"string","title":"Client Plant Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DataAppendResult"}}}},"401":{"description":"Missing or invalid key. Check that the header reads exactly `Authorization: Bearer wpk_...`.","content":{"application/json":{"example":{"detail":"Authorization: Bearer wpk_... krävs"}}}},"403":{"description":"The partner account is suspended. Contact partner@wolta.se.","content":{"application/json":{"example":{"detail":"partnern är avstängd"}}}},"404":{"description":"Unknown `client_plant_id` for your partner account. You never see other partners' plants, so a plant belonging to someone else also returns 404.","content":{"application/json":{"example":{"detail":"okänd anläggning"}}}},"413":{"description":"The batch is too large: more than 5,000 rows, a body above 2 MiB, or the plant's total storage cap (80,000 rows) reached.","content":{"application/json":{"example":{"detail":"max 5000 rader per anrop - dela upp i flera batchar"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"429":{"description":"The rate budget is exhausted. `Retry-After` gives the number of seconds to wait.","content":{"application/json":{"example":{"detail":"partner-budgeten är slut, försök strax"}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"rows":{"description":"Hourly rows, at least one and at most 5000 per call. There is no separate load column: consumption is derived from import, solar and battery flows.","items":{"properties":{"ts_utc":{"description":"The start of the hour as ISO 8601 WITH a time zone, either `Z` or an explicit offset. A naive timestamp returns 422 - Wolta never guesses local time.","examples":["2026-08-01T00:00:00Z"],"title":"Ts Utc","type":"string"},"solar_kwh":{"default":0,"description":"Solar production, kWh during the hour. Stored as 0 when omitted.","maximum":500,"minimum":0,"title":"Solar Kwh","type":"number"},"grid_import_kwh":{"default":0,"description":"Grid import, kWh during the hour. Stored as 0 when omitted.","maximum":500,"minimum":0,"title":"Grid Import Kwh","type":"number"},"grid_export_kwh":{"default":0,"description":"Grid export, kWh during the hour. Stored as 0 when omitted.","maximum":500,"minimum":0,"title":"Grid Export Kwh","type":"number"},"batt_charged_kwh":{"default":0,"description":"Charged into the battery, kWh during the hour. Stored as 0 when omitted.","maximum":500,"minimum":0,"title":"Batt Charged Kwh","type":"number"},"batt_discharged_kwh":{"default":0,"description":"Discharged from the battery, kWh during the hour. Stored as 0 when omitted.","maximum":500,"minimum":0,"title":"Batt Discharged Kwh","type":"number"}},"required":["ts_utc"],"title":"PartnerDataRow","type":"object"},"minItems":1,"title":"Rows","type":"array"}},"required":["rows"],"title":"PartnerDataPut","type":"object"},"example":{"rows":[{"ts_utc":"2026-08-01T00:00:00Z","solar_kwh":0.5,"grid_import_kwh":1,"grid_export_kwh":0.1,"batt_charged_kwh":0.2,"batt_discharged_kwh":0.2},{"ts_utc":"2026-08-01T01:00:00Z","solar_kwh":0.4,"grid_import_kwh":1.2,"grid_export_kwh":0,"batt_charged_kwh":0.1,"batt_discharged_kwh":0.3}]}}}}}},"/api/v1/partner/grades":{"get":{"tags":["partner"],"summary":"Read grades for the fleet","description":"The latest grade for your plants in a paginated batch. The same paging rules as `GET /partner/plants`: `limit` must be greater than 0 and is silently capped at 500.\n\n`score` is set only when `status` is `ok`. Every other status gives `null`, including `below_baseline` (the control performed worse than a passive battery after wear). Treat `null` as \"nothing to show yet\", not as an error: Wolta would rather answer null than with a number that can be misread.\n\n`period_start` and `period_end` describe all data Wolta has stored for the plant, not the exact window the latest calculation used. `grade_version` shows which version of the grading logic lies behind the number: do not compare grades across a version change at face value.","operationId":"list_grades_api_v1_partner_grades_get","security":[{"PartnerKey":[]}],"parameters":[{"name":"offset","in":"query","required":false,"schema":{"type":"integer","minimum":0,"default":0,"title":"Offset"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","exclusiveMinimum":0,"default":100,"title":"Limit"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GradesPage"}}}},"401":{"description":"Missing or invalid key. Check that the header reads exactly `Authorization: Bearer wpk_...`.","content":{"application/json":{"example":{"detail":"Authorization: Bearer wpk_... krävs"}}}},"403":{"description":"The partner account is suspended. Contact partner@wolta.se.","content":{"application/json":{"example":{"detail":"partnern är avstängd"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"429":{"description":"The rate budget is exhausted. `Retry-After` gives the number of seconds to wait.","content":{"application/json":{"example":{"detail":"partner-budgeten är slut, försök strax"}}}}}}},"/api/v1/account/link":{"post":{"tags":["account"],"summary":"Link an email address to a plant","description":"Sends a confirmation link that ties an email address to the plant behind a profile token. Always returns **202** regardless of whether the address already exists, so the response never reveals which addresses have an account.\n\nRequires a valid Turnstile token. This path is built for the browser, not for machine use.","operationId":"link_api_v1_account_link_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkIn"}}},"required":true},"responses":{"202":{"description":"The request was accepted and queued. Poll for the result.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Link Api V1 Account Link Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/login":{"post":{"tags":["account"],"summary":"Request a sign-in link","description":"Sends a one-time sign-in link to an email address. Always returns **202**, even for unknown addresses: any other response would reveal which addresses have an account. An account without any plant therefore cannot be created this way.\n\nRequires a valid Turnstile token. This path is built for the browser, not for machine use.","operationId":"login_api_v1_account_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginIn"}}},"required":true},"responses":{"202":{"description":"The request was accepted and queued. Poll for the result.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Login Api V1 Account Login Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/confirm":{"post":{"tags":["account"],"summary":"Redeem a sign-in link","description":"Redeems the one-time token from the email link and sets the session cookie. The token is short-lived and can only be used once.","operationId":"confirm_api_v1_account_confirm_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConfirmIn"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Confirm Api V1 Account Confirm Post"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/me":{"get":{"tags":["account"],"summary":"Account overview","description":"The account's email address, linked plants and settings. Requires a valid session cookie; 401 otherwise.","operationId":"me_api_v1_account_me_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Me Api V1 Account Me Get"}}}},"401":{"description":"No valid session. Sign in again."}},"security":[{"Session":[]}]},"delete":{"tags":["account"],"summary":"Delete the account","description":"Deletes the account and its links to plants. The plants' own data is deleted separately through the plant endpoints.","operationId":"delete_me_api_v1_account_me_delete","responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"No valid session. Sign in again."}},"security":[{"Session":[]}]}},"/api/v1/account/profiles":{"post":{"tags":["account"],"summary":"Link a plant to the account","description":"Links an existing plant to the signed-in account, using the plant's profile token as proof of possession. An invalid token returns 400.","operationId":"add_profile_api_v1_account_profiles_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileIn"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Add Profile Api V1 Account Profiles Post"}}}},"400":{"description":"Invalid profile token - the plant could not be linked."},"401":{"description":"No valid session. Sign in again."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"Session":[]}]}},"/api/v1/account/profiles/claim":{"post":{"tags":["account"],"summary":"Link plant with a code","description":"Links the plant to the account using a linking code created from the Home Assistant integration. The code is valid for 10 minutes and for a single use. Unknown, expired and already-used codes all return the same response.","operationId":"claim_profile_api_v1_account_profiles_claim_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimIn"}}},"required":true},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Claim Profile Api V1 Account Profiles Claim Post"}}}},"401":{"description":"No valid session. Sign in again."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"Session":[]}]}},"/api/v1/account/profiles/{submission_id}":{"delete":{"tags":["account"],"summary":"Unlink a plant","description":"Removes the link between the account and the plant. The plant and its data remain and are still reachable with the profile token.","operationId":"delete_profile_api_v1_account_profiles__submission_id__delete","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"No valid session. Sign in again."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/prefs":{"put":{"tags":["account"],"summary":"Save notification preferences","description":"Sets the account's notification preferences. An unknown preference type returns 422.","operationId":"put_prefs_api_v1_account_prefs_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrefIn"}}},"required":true},"responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"No valid session. Sign in again."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"Session":[]}]}},"/api/v1/account/unsubscribe":{"get":{"tags":["account"],"summary":"Unsubscribe page","description":"Redirects to the human-facing unsubscribe page with the token preserved. It exists so that email clients and link previewers that GET the unsubscribe link meet a page instead of a 405.","operationId":"unsubscribe_landing_api_v1_account_unsubscribe_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{}}}},"307":{"description":"Redirect to the human-facing unsubscribe page."}}},"post":{"tags":["account"],"summary":"Unsubscribe from a mailing","description":"Turns off one mailing type. The entire authorisation lies in the signed token from the email; no sign-in is required. Supports both RFC 8058 one-click unsubscribe (which email clients call automatically) and the human-facing unsubscribe page. It is always the signed mailing type in the token that decides what is turned off, never a field in the request.","operationId":"unsubscribe_api_v1_account_unsubscribe_post","responses":{"204":{"description":"Successful response, empty body."}}}},"/api/v1/account/logout":{"post":{"tags":["account"],"summary":"Sign out","description":"Revokes the current session on the server. An already copied session cookie therefore stops working too.","operationId":"logout_api_v1_account_logout_post","responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"No valid session. Sign in again."}},"security":[{"Session":[]}]}},"/api/v1/account/logout-all":{"post":{"tags":["account"],"summary":"Sign out everywhere","description":"Revokes all of the account's sessions. Does not touch plants that stream data; they authenticate with tokens and are unaffected.","operationId":"logout_all_api_v1_account_logout_all_post","responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"No valid session. Sign in again."}},"security":[{"Session":[]}]}},"/api/v1/account/sessions":{"get":{"tags":["account"],"summary":"Signed-in devices","description":"The account's active sessions with device and last use, for the device list on the account page.","operationId":"list_sessions_api_v1_account_sessions_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Sessions Api V1 Account Sessions Get"}}}},"401":{"description":"No valid session. Sign in again."}},"security":[{"Session":[]}]}},"/api/v1/account/sessions/{session_id}":{"delete":{"tags":["account"],"summary":"Revoke a session","description":"Revokes a single session. An unknown session and someone else's session both return 404, so the response does not reveal which sessions exist. Your own current session may be revoked: the next call then returns 401.","operationId":"revoke_session_api_v1_account_sessions__session_id__delete","security":[{"Session":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"integer","title":"Session Id"}}],"responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"No valid session. Sign in again."},"404":{"description":"Unknown session, or someone else's. Both cases return the same response."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}":{"get":{"tags":["account"],"summary":"Plant parameters (signed in)","description":"The same response as `GET /profile`, for a plant the account owns. Authenticated with the session cookie instead of a token. An unknown plant and someone else's plant both return 404.","operationId":"get_plant_api_v1_account_plant__submission_id__get","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Get Plant Api V1 Account Plant  Submission Id  Get"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["account"],"summary":"Update the plant (signed in)","description":"The same semantics as `PATCH /profile`, for a plant the account owns.","operationId":"patch_plant_api_v1_account_plant__submission_id__patch","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfilePatch"}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Patch Plant Api V1 Account Plant  Submission Id  Patch"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["account"],"summary":"Delete the plant (signed in)","description":"Deletes the plant and all its data. Cannot be undone.","operationId":"delete_plant_api_v1_account_plant__submission_id__delete","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/recompute":{"post":{"tags":["account"],"summary":"Request a recalculation (signed in)","description":"The same semantics as `POST /profile/recompute`, for a plant the account owns. Returns 202.","operationId":"recompute_plant_api_v1_account_plant__submission_id__recompute_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"202":{"description":"The request was accepted and queued. Poll for the result.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Recompute Plant Api V1 Account Plant  Submission Id  Recompute Post"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"429":{"description":"The recalculation already ran within the cooldown window. `Retry-After` gives when the next one is allowed."}}}},"/api/v1/account/plant/{submission_id}/warm":{"get":{"tags":["account"],"summary":"Status of a queued calculation (signed in)","description":"The same status as `GET /warm`, for a plant the account owns.","operationId":"warm_plant_api_v1_account_plant__submission_id__warm_get","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Warm Plant Api V1 Account Plant  Submission Id  Warm Get"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/token":{"post":{"tags":["account"],"summary":"New profile token","description":"Mints a fresh profile token for a plant the account owns. It is shown **once**: Wolta stores only a hashed value and can never show a lost token again.\n\nNeeded to connect the Home Assistant integration, which authenticates with a token even when the owner is signed in. The old token stops working.","operationId":"issue_plant_token_api_v1_account_plant__submission_id__token_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Issue Plant Token Api V1 Account Plant  Submission Id  Token Post"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/share":{"post":{"tags":["account"],"summary":"Create a share link (signed in)","description":"Creates a shareable link to the plant's grade. `wear_on` controls whether wear is included in the shared view. Returns 201.","operationId":"share_plant_api_v1_account_plant__submission_id__share_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShareIn"}}}},"responses":{"201":{"description":"The resource was created.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Share Plant Api V1 Account Plant  Submission Id  Share Post"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/sizing":{"post":{"tags":["account"],"summary":"Optimal battery size (signed in)","description":"The same calculation as `POST /sizing`, for a plant the account owns.","operationId":"sizing_plant_api_v1_account_plant__submission_id__sizing_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SizingIn"}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sizing Plant Api V1 Account Plant  Submission Id  Sizing Post"}}}},"202":{"description":"The calculation was queued (cold cache). The response carries `warm.config_key`; poll `GET /warm/by-config`, then repeat the original call to fetch the result."},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/simulate":{"post":{"tags":["account"],"summary":"Household economics (signed in)","description":"The same calculation as `GET /simulate/household`, for a plant the account owns.","operationId":"simulate_plant_api_v1_account_plant__submission_id__simulate_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimulateIn"}}}},"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Simulate Plant Api V1 Account Plant  Submission Id  Simulate Post"}}}},"202":{"description":"The calculation was queued (cold cache). The response carries `warm.config_key`; poll `GET /warm/by-config`, then repeat the original call to fetch the result."},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/sonnen":{"get":{"tags":["account"],"summary":"Status of the Sonnen connection","description":"Shows whether the plant receives data from a sonnenBatterie and when the most recent webhook arrived.","operationId":"sonnen_status_api_v1_account_plant__submission_id__sonnen_get","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sonnen Status Api V1 Account Plant  Submission Id  Sonnen Get"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["account"],"summary":"Disconnect the sonnenBatterie","description":"Disconnects the battery. Data already stored remains, but no new data is accepted.","operationId":"sonnen_detach_api_v1_account_plant__submission_id__sonnen_delete","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sonnen Detach Api V1 Account Plant  Submission Id  Sonnen Delete"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/sonnen/attach":{"post":{"tags":["account"],"summary":"Connect a sonnenBatterie","description":"Creates the webhook address the battery should post to. The ingest token is shown **once** and stored hashed.","operationId":"sonnen_attach_api_v1_account_plant__submission_id__sonnen_attach_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sonnen Attach Api V1 Account Plant  Submission Id  Sonnen Attach Post"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/sonnen/rotate":{"post":{"tags":["account"],"summary":"Rotate the Sonnen token","description":"Mints a new ingest token for the webhook. The old one stops working immediately, so the battery's settings must be updated.","operationId":"sonnen_rotate_api_v1_account_plant__submission_id__sonnen_rotate_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Sonnen Rotate Api V1 Account Plant  Submission Id  Sonnen Rotate Post"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/link":{"post":{"tags":["account"],"summary":"Get or create read link (signed in)","description":"As `POST /profile/{token}/link`, for a plant the account owns. Authenticated with the session cookie.","operationId":"mint_link_session_api_v1_account_plant__submission_id__link_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Mint Link Session Api V1 Account Plant  Submission Id  Link Post"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The link feature is not enabled on the server (encryption key missing)."}}},"delete":{"tags":["account"],"summary":"Remove read link (signed in)","description":"As `DELETE /profile/{token}/link`, for a plant the account owns.","operationId":"revoke_link_session_api_v1_account_plant__submission_id__link_delete","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/account/plant/{submission_id}/link/rotate":{"post":{"tags":["account"],"summary":"Replace read link (signed in)","description":"As `POST /profile/{token}/link/rotate`, for a plant the account owns.","operationId":"rotate_link_session_api_v1_account_plant__submission_id__link_rotate_post","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Rotate Link Session Api V1 Account Plant  Submission Id  Link Rotate Post"}}}},"401":{"description":"No valid session. Sign in again."},"404":{"description":"The plant does not exist, or belongs to someone else. Both cases return the same response - it never reveals what exists."},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The link feature is not enabled on the server (encryption key missing)."}}}},"/b/{share_id}":{"get":{"tags":["sharing"],"summary":"Shared grade page","description":"The public page behind a share link. Renders HTML, not JSON, and requires no authentication: whoever has the link can see the view.","operationId":"share_html_b__share_id__get","parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful response.","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/b/{share_id}/og.png":{"get":{"tags":["sharing"],"summary":"Preview image for a share link","description":"The PNG image shown when a share link is pasted into social media or chat.","operationId":"share_og_png_b__share_id__og_png_get","parameters":[{"name":"share_id","in":"path","required":true,"schema":{"type":"string","title":"Share Id"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation error: a field is missing, has the wrong type, or falls outside the allowed range. The response body identifies the offending field.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AdoptRequest":{"properties":{"client_plant_id":{"anyOf":[{"type":"string","maxLength":200,"minLength":16},{"type":"null"}],"title":"Client Plant Id"}},"type":"object","title":"AdoptRequest"},"Body_compute_api_v1_calibration_compute_post":{"properties":{"file":{"anyOf":[{"type":"string","contentMediaType":"application/octet-stream"},{"type":"null"}],"title":"File"},"zone":{"type":"string","title":"Zone"},"has_battery":{"type":"boolean","title":"Has Battery","default":false},"has_solar":{"type":"boolean","title":"Has Solar","default":false},"battery_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kwh"},"battery_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kw"},"eff":{"type":"number","title":"Eff","default":0.9},"house_type":{"type":"string","title":"House Type","default":"heated"},"control_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Control System"},"control_system_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Control System Name"},"pv_kwp":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pv Kwp"},"cost_sek":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cost Sek"},"purchase_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"},"profile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Profile Token"},"store":{"type":"boolean","title":"Store","default":false}},"type":"object","required":["zone"],"title":"Body_compute_api_v1_calibration_compute_post"},"Body_grade_api_v1_grade_post":{"properties":{"file":{"anyOf":[{"type":"string","contentMediaType":"application/octet-stream"},{"type":"null"}],"title":"File"},"zone":{"type":"string","title":"Zone"},"battery_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kwh"},"battery_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kw"},"nameplate_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Nameplate Kwh"},"nameplate_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Nameplate Kw"},"eff":{"type":"number","title":"Eff","default":0.9},"contribute_score":{"type":"boolean","title":"Contribute Score","default":true},"share_profile":{"type":"boolean","title":"Share Profile","default":false},"control_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Control System"},"control_system_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Control System Name"},"profile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Profile Token"},"dedup_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dedup Token"},"cost_sek":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cost Sek"},"purchase_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"},"purchase_date_solar":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date Solar"},"purchase_date_battery":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date Battery"},"paid_solar":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Paid Solar"},"paid_battery":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Paid Battery"},"pv_kwp":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pv Kwp"},"sensor_ids":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sensor Ids"},"dedup":{"type":"boolean","title":"Dedup","default":false},"purchase_motivation":{"items":{"type":"string"},"type":"array","title":"Purchase Motivation","default":[]},"purchase_motivation_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Motivation Note"},"grid_var_ore":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Grid Var Ore"},"surcharge_ore":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Surcharge Ore"},"export_extra_ore":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Export Extra Ore"},"grid_var_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Grid Var Pct"},"export_extra_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Export Extra Pct"}},"type":"object","required":["zone"],"title":"Body_grade_api_v1_grade_post"},"Body_sizing_endpoint_api_v1_sizing_post":{"properties":{"file":{"anyOf":[{"type":"string","contentMediaType":"application/octet-stream"},{"type":"null"}],"title":"File"},"zone":{"type":"string","title":"Zone"},"battery_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kwh"},"battery_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kw"},"eff":{"type":"number","title":"Eff","default":0.9},"kr_per_kwh":{"type":"number","title":"Kr Per Kwh","default":7000},"expansion_kr_per_kwh":{"type":"number","title":"Expansion Kr Per Kwh","default":3500},"max_cycles":{"anyOf":[{"type":"number","maximum":15000,"minimum":2000},{"type":"null"}],"title":"Max Cycles"},"calendar_years":{"anyOf":[{"type":"integer","maximum":40,"minimum":1},{"type":"null"}],"title":"Calendar Years"},"battery_degradation":{"anyOf":[{"type":"number","maximum":0.2,"minimum":0},{"type":"null"}],"title":"Battery Degradation"},"kw_per_kwh":{"type":"number","title":"Kw Per Kwh","default":0.5},"green_mode":{"type":"string","title":"Green Mode","default":"auto"},"realistic":{"type":"boolean","title":"Realistic","default":true},"capture":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Capture"},"profile_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Profile Token"},"cabinet_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cabinet Kwh"},"cabinet_cost":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cabinet Cost"},"inverter_cost":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Inverter Cost"},"mode":{"type":"string","title":"Mode","default":"capacity"}},"type":"object","required":["zone"],"title":"Body_sizing_endpoint_api_v1_sizing_post"},"CheckPlantIn":{"properties":{"sensor_ids":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sensor Ids"},"zone":{"type":"string","title":"Zone","default":""},"dedup_token":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Dedup Token"}},"type":"object","title":"CheckPlantIn"},"ClaimIn":{"properties":{"code":{"type":"string","maxLength":64,"minLength":1,"title":"Code","description":"The linking code, with or without the hyphen."}},"type":"object","required":["code"],"title":"ClaimIn"},"ConfirmIn":{"properties":{"jti":{"type":"string","title":"Jti"}},"type":"object","required":["jti"],"title":"ConfirmIn"},"DataAppendResult":{"properties":{"inserted_hours":{"type":"integer","title":"Inserted Hours","description":"Number of rows in THIS batch, not the plant's entire history.","examples":[2]},"first":{"type":"string","title":"First","description":"The first timestamp in this batch."},"last":{"type":"string","title":"Last","description":"The last timestamp in this batch."},"repaired_battery_rows":{"type":"integer","title":"Repaired Battery Rows","description":"Number of stored hourly rows whose battery flows were zeroed because they broke physical limits. Above 0 indicates a faulty sensor configuration or a jump in your data.","examples":[0]}},"type":"object","required":["inserted_hours","first","last","repaired_battery_rows"],"title":"DataAppendResult"},"DataPut":{"properties":{"rows":{"items":{"$ref":"#/components/schemas/DataRow"},"type":"array","maxItems":40000,"minItems":1,"title":"Rows"}},"type":"object","required":["rows"],"title":"DataPut"},"DataRow":{"properties":{"ts":{"type":"string","format":"date-time","title":"Ts"},"batt_charged_kwh":{"type":"number","maximum":500,"minimum":0,"title":"Batt Charged Kwh"},"batt_discharged_kwh":{"type":"number","maximum":500,"minimum":0,"title":"Batt Discharged Kwh"},"solar_kwh":{"type":"number","maximum":500,"minimum":0,"title":"Solar Kwh","default":0},"grid_import_kwh":{"type":"number","maximum":500,"minimum":0,"title":"Grid Import Kwh","default":0},"grid_export_kwh":{"type":"number","maximum":500,"minimum":0,"title":"Grid Export Kwh","default":0}},"type":"object","required":["ts","batt_charged_kwh","batt_discharged_kwh"],"title":"DataRow"},"GradeRow":{"properties":{"client_plant_id":{"type":"string","title":"Client Plant Id"},"score":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Score","description":"The optimisation grade. Set ONLY when `status` is `ok`; every other status gives `null`. Treat null as \"nothing to show yet\", not as an error.","examples":[62]},"status":{"type":"string","enum":["none","collecting","check_data","below_baseline","ok"],"title":"Status","description":"The same vocabulary as `grade_status` in `GET /partner/plants`. The two endpoints can never show different states for the same plant."},"period_start":{"type":"string","title":"Period Start","description":"The first day Wolta has data for the plant. Describes coverage, not the exact window the latest calculation used."},"period_end":{"type":"string","title":"Period End","description":"The last day Wolta has data for the plant."},"grade_version":{"type":"string","title":"Grade Version","description":"The version of the grading logic behind the number. Do not compare grades across a version change at face value.","examples":["3"]}},"type":"object","required":["client_plant_id","status","period_start","period_end","grade_version"],"title":"GradeRow"},"GradesPage":{"properties":{"grades":{"items":{"$ref":"#/components/schemas/GradeRow"},"type":"array","title":"Grades"},"total":{"type":"integer","title":"Total","description":"The size of your entire fleet, not the number of rows on this page."}},"type":"object","required":["grades","total"],"title":"GradesPage"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ImportRequest":{"properties":{"zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zone"},"battery_kwh":{"type":"number","exclusiveMinimum":0,"title":"Battery Kwh","default":1},"battery_kw":{"type":"number","exclusiveMinimum":0,"title":"Battery Kw","default":1},"eff":{"type":"number","maximum":1,"exclusiveMinimum":0,"title":"Eff","default":0.9},"has_solar":{"type":"boolean","title":"Has Solar","default":false},"share_profile":{"type":"boolean","title":"Share Profile","default":false},"pv_kwp":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Pv Kwp"},"cost_sek":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Cost Sek"},"purchase_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"},"grid_var_ore":{"anyOf":[{"type":"number","maximum":500,"minimum":0},{"type":"null"}],"title":"Grid Var Ore"},"surcharge_ore":{"anyOf":[{"type":"number","maximum":500,"minimum":0},{"type":"null"}],"title":"Surcharge Ore"},"export_extra_ore":{"anyOf":[{"type":"number","maximum":500,"minimum":-200},{"type":"null"}],"title":"Export Extra Ore"},"grid_var_pct":{"anyOf":[{"type":"number","maximum":100,"minimum":0},{"type":"null"}],"title":"Grid Var Pct"},"export_extra_pct":{"anyOf":[{"type":"number","maximum":100,"minimum":-100},{"type":"null"}],"title":"Export Extra Pct"},"reserve_pct":{"anyOf":[{"type":"number","maximum":100,"minimum":0},{"type":"null"}],"title":"Reserve Pct"},"nameplate_kwh":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Nameplate Kwh"},"nameplate_kw":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Nameplate Kw"},"battery_events":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Battery Events"},"client_plant_id":{"anyOf":[{"type":"string","maxLength":200,"minLength":16},{"type":"null"}],"title":"Client Plant Id"},"control_system":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Control System","description":"The control system operating the battery. Strictly validated – use `other` if none of the values fit and give the name in `control_system_name`. Omitted on re-onboarding, the stored value is kept."},"control_system_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Control System Name","description":"Free-text name of the control system when `control_system` is `other`."},"api_key":{"type":"string","maxLength":200,"minLength":1,"title":"Api Key"},"site_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Site Id"}},"type":"object","required":["api_key"],"title":"ImportRequest"},"InterestIn":{"properties":{"system":{"type":"string","maxLength":40,"title":"System"},"email":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Email"}},"type":"object","required":["system"],"title":"InterestIn"},"LinkIn":{"properties":{"email":{"type":"string","maxLength":200,"title":"Email"},"profile_token":{"type":"string","title":"Profile Token"},"turnstile_token":{"type":"string","title":"Turnstile Token"}},"type":"object","required":["email","profile_token","turnstile_token"],"title":"LinkIn"},"LinkRotated":{"properties":{"link_token":{"type":"string","title":"Link Token","description":"The new end-customer link key. The old one stops working immediately.","examples":["wpl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]}},"type":"object","required":["link_token"],"title":"LinkRotated"},"LoginIn":{"properties":{"email":{"type":"string","maxLength":200,"title":"Email"},"turnstile_token":{"type":"string","title":"Turnstile Token"}},"type":"object","required":["email","turnstile_token"],"title":"LoginIn"},"PartnerMeOut":{"properties":{"name":{"type":"string","title":"Name","description":"The partner's name.","examples":["Exempelbolaget AB"]},"slug":{"type":"string","title":"Slug","description":"The partner's identifier at Wolta.","examples":["exempelbolaget-ab"]},"tier":{"type":"string","title":"Tier","description":"The tier your partnership is on.","examples":["dev-free"]},"plant_count":{"type":"integer","title":"Plant Count","description":"Number of plants you currently have.","examples":[3]},"plant_limit":{"type":"integer","title":"Plant Limit","description":"The maximum number of plants your tier allows.","examples":[10]},"rate_max_per_min":{"type":"integer","title":"Rate Max Per Min","description":"Your rate budget in calls per minute.","examples":[120]}},"type":"object","required":["name","slug","tier","plant_count","plant_limit","rate_max_per_min"],"title":"PartnerMeOut"},"PlantCreate":{"properties":{"client_plant_id":{"type":"string","maxLength":128,"minLength":1,"pattern":"^(?!\\.{1,2}$)[^\\s/?#%\\\\]+$","title":"Client Plant Id","description":"Your own stable id for the plant. It is used as a path segment in every later call and must therefore be URL-safe: no spaces, slashes, ?, #, % or backslashes. At most 128 characters.","examples":["exb-001"]},"zone":{"type":"string","title":"Zone","description":"Bidding zone, for example `SE3`. Trimmed and upper-cased before validation. The zone is immutable after creation.","examples":["SE3"]},"control_system":{"type":"string","enum":["aikion","checkwatt","emaldo","emhass","ferroamp","greenely","huawei","manual","other","pixii","reduxi","self_consumption","sigenergy","sonnen","tibber"],"title":"Control System","description":"The control system operating the battery. Strictly validated - use `other` if none of the values fit. The field is mandatory because grades are grouped and weighted per control system.","examples":["other"]},"battery_kwh":{"type":"number","exclusiveMinimum":0,"title":"Battery Kwh","description":"The battery's capacity in kWh.","examples":[10]},"battery_kw":{"type":"number","exclusiveMinimum":0,"title":"Battery Kw","description":"The battery's power rating in kW.","examples":[5]},"eff":{"anyOf":[{"type":"number","maximum":1,"exclusiveMinimum":0},{"type":"null"}],"title":"Eff","description":"Round-trip efficiency. Defaults to 0.9 when omitted.","examples":[0.9]},"nameplate_kwh":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Nameplate Kwh","description":"Nameplate capacity, if it differs from the deliverable capacity (for example on a degraded battery).","examples":[15]},"reserve_pct":{"anyOf":[{"type":"number","maximum":100,"minimum":0},{"type":"null"}],"title":"Reserve Pct","description":"The share of the battery, in percent, reserved and not used for optimisation.","examples":[10]}},"type":"object","required":["client_plant_id","zone","control_system","battery_kwh","battery_kw"],"title":"PlantCreate"},"PlantCreated":{"properties":{"client_plant_id":{"type":"string","title":"Client Plant Id","description":"Your own id for the plant.","examples":["exb-001"]},"link_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Link Token","description":"The end-customer link's key. Set only in the 201 response on the FIRST creation - a repeated (idempotent) call gives `null`. Save it immediately; it is never shown again.","examples":["wpl_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"]}},"type":"object","required":["client_plant_id","link_token"],"title":"PlantCreated"},"PlantPatched":{"properties":{"client_plant_id":{"type":"string","title":"Client Plant Id"},"zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zone"},"control_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Control System"},"battery_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kwh"},"battery_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kw"},"eff":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Eff"},"nameplate_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Nameplate Kwh"},"reserve_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Reserve Pct"}},"type":"object","required":["client_plant_id"],"title":"PlantPatched","description":"The PATCH response: partner-owned fields, never the end customer's economics or tariff."},"PlantRow":{"properties":{"client_plant_id":{"type":"string","title":"Client Plant Id"},"zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zone"},"control_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Control System"},"first_hour":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Hour","description":"Timestamp of the first hour Wolta has data for. `null` before the plant has streamed anything."},"last_hour":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Hour","description":"Timestamp of the most recent hour Wolta has data for."},"n_days":{"type":"integer","title":"N Days","description":"Number of distinct local days with data, counted time-zone aware - so not hours divided by 24."},"grade_status":{"type":"string","enum":["none","collecting","check_data","below_baseline","ok"],"title":"Grade Status","description":"Grade status: `none` (fewer than 7 days of data), `collecting` (7-29 days), `check_data` (30+ days but no trustworthy grade yet), `below_baseline` (valid but negative net) or `ok`."}},"type":"object","required":["client_plant_id","n_days","grade_status"],"title":"PlantRow"},"PlantsPage":{"properties":{"plants":{"items":{"$ref":"#/components/schemas/PlantRow"},"type":"array","title":"Plants"},"total":{"type":"integer","title":"Total","description":"The size of your entire fleet, not the number of rows on this page."}},"type":"object","required":["plants","total"],"title":"PlantsPage"},"PrefIn":{"properties":{"kind":{"type":"string","title":"Kind"},"enabled":{"type":"boolean","title":"Enabled"},"submission_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Submission Id"}},"type":"object","required":["kind","enabled"],"title":"PrefIn"},"ProfileCreate":{"properties":{"zone":{"type":"string","title":"Zone"},"battery_kwh":{"type":"number","exclusiveMinimum":0,"title":"Battery Kwh"},"battery_kw":{"type":"number","exclusiveMinimum":0,"title":"Battery Kw"},"eff":{"type":"number","maximum":1,"exclusiveMinimum":0,"title":"Eff","default":0.9},"has_solar":{"type":"boolean","title":"Has Solar","default":false},"share_profile":{"type":"boolean","title":"Share Profile","default":false},"pv_kwp":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Pv Kwp"},"cost_sek":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Cost Sek"},"purchase_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"},"grid_var_ore":{"anyOf":[{"type":"number","maximum":500,"minimum":0},{"type":"null"}],"title":"Grid Var Ore"},"surcharge_ore":{"anyOf":[{"type":"number","maximum":500,"minimum":0},{"type":"null"}],"title":"Surcharge Ore"},"export_extra_ore":{"anyOf":[{"type":"number","maximum":500,"minimum":-200},{"type":"null"}],"title":"Export Extra Ore"},"grid_var_pct":{"anyOf":[{"type":"number","maximum":100,"minimum":0},{"type":"null"}],"title":"Grid Var Pct"},"export_extra_pct":{"anyOf":[{"type":"number","maximum":100,"minimum":-100},{"type":"null"}],"title":"Export Extra Pct"},"reserve_pct":{"anyOf":[{"type":"number","maximum":100,"minimum":0},{"type":"null"}],"title":"Reserve Pct"},"nameplate_kwh":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Nameplate Kwh"},"nameplate_kw":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Nameplate Kw"},"battery_events":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Battery Events"},"client_plant_id":{"anyOf":[{"type":"string","maxLength":200,"minLength":16},{"type":"null"}],"title":"Client Plant Id"},"control_system":{"anyOf":[{"type":"string","maxLength":50},{"type":"null"}],"title":"Control System","description":"The control system operating the battery. Strictly validated – use `other` if none of the values fit and give the name in `control_system_name`. Omitted on re-onboarding, the stored value is kept."},"control_system_name":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}],"title":"Control System Name","description":"Free-text name of the control system when `control_system` is `other`."}},"type":"object","required":["zone","battery_kwh","battery_kw"],"title":"ProfileCreate"},"ProfileIn":{"properties":{"profile_token":{"type":"string","title":"Profile Token"}},"type":"object","required":["profile_token"],"title":"ProfileIn"},"ProfilePatch":{"properties":{"cost_sek":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cost Sek"},"purchase_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"},"paid_solar_sek":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Paid Solar Sek"},"purchase_date_solar":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date Solar"},"battery_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kwh"},"battery_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kw"},"eff":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Eff"},"pv_kwp":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pv Kwp"},"zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zone"},"grid_var_ore":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Grid Var Ore"},"surcharge_ore":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Surcharge Ore"},"export_extra_ore":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Export Extra Ore"},"grid_var_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Grid Var Pct"},"export_extra_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Export Extra Pct"},"reserve_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Reserve Pct"},"nameplate_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Nameplate Kwh"},"nameplate_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Nameplate Kw"},"battery_events":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Battery Events"},"grade_start_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade Start Date"},"grade_window_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Grade Window Days"},"control_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Control System"},"control_system_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Control System Name"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"battery_life_years":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery Life Years"},"max_cycles":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Cycles"},"solar_degradation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Solar Degradation"},"battery_degradation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Degradation"}},"type":"object","title":"ProfilePatch","description":"All fields optional - only those supplied are updated. cost_sek/purchase_date = economics (cheap UPDATE); battery/solar changes trigger a recalculation on the stored operating data."},"ShareCreate":{"properties":{"profile_token":{"type":"string","title":"Profile Token"},"wear_on":{"type":"boolean","title":"Wear On","default":true}},"type":"object","required":["profile_token"],"title":"ShareCreate"},"ShareIn":{"properties":{"wear_on":{"type":"boolean","title":"Wear On","default":true}},"type":"object","title":"ShareIn"},"SimulateIn":{"properties":{"mode":{"type":"string","title":"Mode","default":"decision"},"pv_kwp":{"type":"number","maximum":500,"exclusiveMinimum":0,"title":"Pv Kwp","default":10},"load_annual":{"type":"number","maximum":200000,"exclusiveMinimum":0,"title":"Load Annual","default":20000},"has_battery":{"type":"boolean","title":"Has Battery","default":true},"kwh":{"type":"number","maximum":10000,"exclusiveMinimum":0,"title":"Kwh","default":10},"kw":{"type":"number","maximum":10000,"exclusiveMinimum":0,"title":"Kw","default":5},"eff":{"type":"number","maximum":1,"exclusiveMinimum":0.3,"title":"Eff","default":0.9},"realism_factor":{"anyOf":[{"type":"number","maximum":1,"exclusiveMinimum":0},{"type":"null"}],"title":"Realism Factor"},"selfuse_capture":{"anyOf":[{"type":"number","maximum":1,"exclusiveMinimum":0},{"type":"null"}],"title":"Selfuse Capture"},"solar_export_capture":{"anyOf":[{"type":"number","maximum":1,"exclusiveMinimum":0},{"type":"null"}],"title":"Solar Export Capture"},"capex_battery":{"type":"number","minimum":0,"title":"Capex Battery","default":0},"capex_solar":{"type":"number","minimum":0,"title":"Capex Solar","default":0},"persons":{"type":"integer","maximum":10,"minimum":1,"title":"Persons","default":1},"green_mode":{"type":"string","title":"Green Mode","default":"off"},"green_manual":{"type":"number","minimum":0,"title":"Green Manual","default":0},"lifetime_years":{"type":"integer","maximum":40,"minimum":5,"title":"Lifetime Years","default":30},"battery_life_years":{"anyOf":[{"type":"integer","maximum":40,"minimum":1},{"type":"null"}],"title":"Battery Life Years"},"max_cycles":{"anyOf":[{"type":"integer","maximum":15000,"minimum":2000},{"type":"null"}],"title":"Max Cycles"},"house_type":{"type":"string","title":"House Type","default":"heated"},"battery_degradation":{"anyOf":[{"type":"number","maximum":0.2,"minimum":0},{"type":"null"}],"title":"Battery Degradation"},"solar_degradation":{"anyOf":[{"type":"number","maximum":0.2,"minimum":0},{"type":"null"}],"title":"Solar Degradation"},"inverter_replace":{"type":"boolean","title":"Inverter Replace","default":false},"inverter_replace_year":{"type":"integer","maximum":40,"minimum":1,"title":"Inverter Replace Year","default":15},"inverter_replace_cost":{"type":"number","minimum":0,"title":"Inverter Replace Cost","default":20000},"future_price_factor":{"type":"number","maximum":3,"exclusiveMinimum":0,"title":"Future Price Factor","default":1},"battery_only_economics":{"type":"boolean","title":"Battery Only Economics","default":false},"purchase_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"},"paid":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Paid"},"purchase_date_solar":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date Solar"},"purchase_date_battery":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date Battery"},"paid_solar":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Paid Solar"},"paid_battery":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Paid Battery"},"pv_orient":{"anyOf":[{"type":"string","maxLength":40},{"type":"null"}],"title":"Pv Orient"}},"type":"object","title":"SimulateIn","description":"Parameters for the household economics. The zone always comes from the plant, not from the body."},"SizingIn":{"properties":{"mode":{"type":"string","title":"Mode","default":"capacity"},"kr_per_kwh":{"type":"number","exclusiveMinimum":0,"title":"Kr Per Kwh","default":7000},"expansion_kr_per_kwh":{"type":"number","exclusiveMinimum":0,"title":"Expansion Kr Per Kwh","default":3500},"max_cycles":{"anyOf":[{"type":"number","maximum":15000,"minimum":2000},{"type":"null"}],"title":"Max Cycles"},"calendar_years":{"anyOf":[{"type":"integer","maximum":40,"minimum":1},{"type":"null"}],"title":"Calendar Years"},"battery_degradation":{"anyOf":[{"type":"number","maximum":0.2,"minimum":0},{"type":"null"}],"title":"Battery Degradation"},"kw_per_kwh":{"type":"number","exclusiveMinimum":0,"title":"Kw Per Kwh","default":0.5},"green_mode":{"type":"string","title":"Green Mode","default":"auto"},"realistic":{"type":"boolean","title":"Realistic","default":true},"cabinet_kwh":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Cabinet Kwh"},"cabinet_cost":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Cabinet Cost"},"inverter_cost":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Inverter Cost"},"capture":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Capture"}},"type":"object","title":"SizingIn","description":"Parameters for the sizing calculation. Zone and battery always come from the plant, not from the body."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"elpris_api__api__reduxi_onboard__OnboardRequest":{"properties":{"unit_id":{"type":"string","maxLength":64,"minLength":3,"title":"Unit Id"},"zone":{"type":"string","title":"Zone"},"battery_kwh":{"type":"number","exclusiveMinimum":0,"title":"Battery Kwh"},"battery_kw":{"type":"number","exclusiveMinimum":0,"title":"Battery Kw"},"has_solar":{"type":"boolean","title":"Has Solar","default":false},"pv_kwp":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Pv Kwp"},"cost_sek":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Cost Sek"},"purchase_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"}},"type":"object","required":["unit_id","zone","battery_kwh","battery_kw"],"title":"OnboardRequest"},"elpris_api__api__sonnen__OnboardRequest":{"properties":{"zone":{"type":"string","title":"Zone"},"battery_kwh":{"type":"number","exclusiveMinimum":0,"title":"Battery Kwh"},"battery_kw":{"type":"number","exclusiveMinimum":0,"title":"Battery Kw"},"has_solar":{"type":"boolean","title":"Has Solar","default":false},"pv_kwp":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Pv Kwp"},"cost_sek":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"title":"Cost Sek"},"purchase_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Purchase Date"}},"type":"object","required":["zone","battery_kwh","battery_kw"],"title":"OnboardRequest"}},"securitySchemes":{"ProfileToken":{"type":"http","description":"The plant's profile token: `Authorization: Bearer <profile_token>`","scheme":"bearer"},"PartnerKey":{"type":"http","description":"Partner API key: `Authorization: Bearer wpk_...`","scheme":"bearer"},"Session":{"type":"apiKey","description":"Session cookie set at sign-in (POST /account/confirm). The website's own transport - set and sent by the browser.","in":"cookie","name":"__Host-wolta_session"}}},"tags":[{"name":"prices","description":"Spot prices, history, comparisons and ancillary service markets. Open GET endpoints, no key required. Price responses carry a `source` field with attribution: the raw data comes from Svenska kraftnät and ENTSO-E."},{"name":"grades","description":"The optimisation grade: upload battery operation data, follow the queued calculation and read the comparison data behind the grade. Heavy calculations are queued and polled; they never respond synchronously."},{"name":"plant","description":"A saved plant: parameters, continuous operating data, recalculation, results and sizing. Authenticated with the plant's profile token as `Authorization: Bearer`.","externalDocs":{"description":"How the grade works","url":"https://wolta.se/om"}},{"name":"account","description":"Sign-in by email link, sessions and linked plants. The website's own transport: authenticated with a session cookie, not a token."},{"name":"integrations","description":"Connections that stream operating data automatically: Sonnen, Reduxi and import from SolarEdge."},{"name":"partner","description":"Partner API for connecting an entire fleet of battery plants: create plants, feed in hourly data continuously and read back optimisation grades. Its own key (`wpk_`), its own rate budget and a hard limit on the number of plants. Register and get a key at wolta.se/partner/portal.","externalDocs":{"description":"Getting started with the partner API","url":"https://wolta.se/partner/docs"}},{"name":"sharing","description":"Shareable grade links and their preview images."},{"name":"operations","description":"Health check for monitoring."}],"externalDocs":{"description":"API overview on wolta.se","url":"https://wolta.se/api"}}