{"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 (the archive up to 2026-07-01 for SE1-SE4 and the neighbouring zones, the system price, aFRR, mFRR and FCR up to 2026-04-14) and that data may be used freely, including commercially, with attribution. FCR capacity after that comes from Svenska kraftnät's Mimer, which publishes no terms for reuse. Prices after 2026-07-01 - for SE1-SE4 via elprisetjustnu.se - and for other European bidding zones come from the ENTSO-E Transparency Platform under ENTSO-E's terms, not CC BY. The `source` field states the source per response. Wolta's own calculations (optimisation grade, profitability calculation, sizing) require an agreement for commercial use: kontakt@wolta.se. Terms: https://wolta.se/villkor\n\nError messages in the responses' `detail` are in Swedish. Branch on the status code, and on `detail.error` where present, never on the message text.","termsOfService":"https://wolta.se/villkor","contact":{"name":"Wolta","url":"https://wolta.se/api","email":"kontakt@wolta.se"},"version":"0.97.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-01, quarter-hourly after.\n- `resolution=1h` gives hourly means, `1d`/`1mo` aggregates per calendar day and month respectively. Means are weighted by time: a quarter-hourly day weighs the same as an hourly day.\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. Means are weighted by time: a quarter-hourly day weighs the same as an hourly day.","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/control-systems":{"get":{"tags":["plant"],"summary":"Control systems","description":"Lists the control systems a plant can declare, in display order, with a label and the Home Assistant integration domains that usually mean that system. `ha_domains` is a suggestion for prefilling – the user always confirms. Public and cacheable for a day.","operationId":"list_control_systems_api_v1_control_systems_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Response List Control Systems Api V1 Control Systems Get"}}}}}}},"/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. aFRR and mFRR: SVK's open data (CC BY 4.0). FCR: SVK's open data until 2026-04-14, thereafter Mimer (no published terms). The `source` field states the source for the response's window.","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, elprisetjustnu.se, ENTSO-E or interpolated), the period each zone covers 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.\n\nIn `mode=history` the response also carries a `realized` block: `irr` computed on a dated cash-flow series (capex on each stage's own date, the actual annual savings, a projection out to `horizon_end` - a fixed 30-year horizon from the first purchase), `irr_status` (`ok`, `ambiguous` when the series admits more than one mathematical root, `insufficient` under a year of data, `none` with no solution) and `capex_events`. Break-even (`breakeven_date`/`breakeven_total_years`) is the series' LAST zero crossing, not the first.","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/simulate/flex-facit":{"get":{"tags":["grades"],"summary":"Ancillary-service track record per month","description":"What a battery of the given size would have earned in FCR-D per month: the physical cap (gross), the expected net compensation via the chosen aggregator, and the battery's alternative value in spot optimization for a typical profile. Provide `paid_sek` (the whole window, net paid out) for a verdict against the range. At most 12 months, from 2020-01 through the current month (counted through yesterday). Months before 2024-02 carry pay-as-bid prices (`price_basis`) that run systematically above what was actually paid. Nothing is stored.","operationId":"flex_facit_endpoint_api_v1_simulate_flex_facit_get","parameters":[{"name":"zone","in":"query","required":true,"schema":{"type":"string","description":"Price zone (drives the spot price in the alternative; the FCR price is national).","title":"Zone"},"description":"Price zone (drives the spot price in the alternative; the FCR price is national)."},{"name":"kw","in":"query","required":true,"schema":{"type":"number","maximum":10000,"exclusiveMinimum":0,"description":"The battery's rated power (kW).","title":"Kw"},"description":"The battery's rated power (kW)."},{"name":"kwh","in":"query","required":true,"schema":{"type":"number","maximum":10000,"exclusiveMinimum":0,"description":"The battery's rated capacity (kWh).","title":"Kwh"},"description":"The battery's rated capacity (kWh)."},{"name":"from","in":"query","required":true,"schema":{"type":"string","description":"First month, YYYY-MM.","title":"From"},"description":"First month, YYYY-MM."},{"name":"to","in":"query","required":true,"schema":{"type":"string","description":"Last month, YYYY-MM.","title":"To"},"description":"Last month, YYYY-MM."},{"name":"aggregator","in":"query","required":false,"schema":{"type":"string","description":"checkwatt | tibber | annan | flower (flower: no estimate).","default":"checkwatt","title":"Aggregator"},"description":"checkwatt | tibber | annan | flower (flower: no estimate)."},{"name":"paid_sek","in":"query","required":false,"schema":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"description":"Actually paid out for the whole window, SEK net.","title":"Paid Sek"},"description":"Actually paid out for the whole window, SEK net."},{"name":"pv_kwp","in":"query","required":false,"schema":{"type":"number","maximum":1000,"minimum":0,"description":"The typical profile's solar panels (kWp), 0 = no solar.","default":10,"title":"Pv Kwp"},"description":"The typical profile's solar panels (kWp), 0 = no solar."},{"name":"load_annual","in":"query","required":false,"schema":{"type":"number","maximum":1000000,"exclusiveMinimum":0,"description":"The typical profile's annual consumption (kWh).","default":20000,"title":"Load Annual"},"description":"The typical profile's annual consumption (kWh)."},{"name":"house_type","in":"query","required":false,"schema":{"type":"string","description":"heated | unheated.","default":"heated","title":"House Type"},"description":"heated | unheated."}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Flex Facit Endpoint Api V1 Simulate Flex Facit 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/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. `data_since` is the start of the plant's earliest stored interval, quarantined hours included, `null` when there are no rows.","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` and a battery – either `battery_kwh`+`battery_kw` (both > 0) or `battery_declared: true` with the pair omitted, if the capacity is not known yet (Wolta measures it from the operating data). 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. `battery_kwh`/`battery_kw` can never be set to `null` or ≤ 0 (422) - the battery cannot be removed or zeroed via PATCH.\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.\n\n`solar_events` describes solar bought in stages: `[{date, kwp, cost_sek}]`, where each stage states the plant's TOTAL kWp after that stage, not the stage's own increment. `battery_events` can now also carry optional `power_kw` and `eff` per stage with the same total-after-the-stage semantics; a stage that omits them inherits the nearest earlier stage's value.\n\nAs long as a timeline exists, it is the source of truth for its derived fields. `paid_solar_sek` and `purchase_date_solar` can never be set directly while `solar_events` exists (422) - edit the stages instead. A `pv_kwp` PATCH instead writes the value into the solar timeline's newest stage; `battery_kw`/`eff` do the same, but only if the battery timeline already carries the field (otherwise the scalar column still applies). Clearing such a field to `null` while the timeline carries it returns 422 instead of silently contradicting the stages. `purchase_date` may be set to a date on or before the earliest stage - the legitimate case of a component with no date of its own - but both a later date and an explicit `null` (clearing it) return 422 while a timeline exists, because both contradict the stages.","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. Each clock hour is additionally sanity-checked against already stored state: if any field exceeds a physical or statistical limit, for example a meter reading instead of an hour's energy, the whole hour is quarantined and not counted, instead of the batch being rejected. `quarantined_hours` and `quarantined_rows` in the response count them, and `repaired_battery_rows` counts the quarantined rows that carried battery flow. A corrected resend brings the hour back.\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**).\n\n`battery_state` and `battery_sources` are optional: they are collected for later capacity measurement and do not affect the grade. An invalid value in them returns 422 for the whole call, flows included.\n\n`data_since` in the response is the start of the plant's earliest stored interval after the call, quarantined hours included, `null` when there are no rows. A client can compare it with its own history and send what is missing before it; a quarantined hour has been received and should not be sent again.","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`.\n\n`history.realized` gives the realised return on a dated cash-flow series: `capex_events` books capex as negative entries on each stage's own date, followed by the actual annual savings and a projection out to `horizon_end` - a fixed 30-year horizon from the first purchase. `irr_status` is `ok`, `ambiguous` (the series admits more than one mathematically valid rate, so no single figure is honest), `insufficient` (under a year of data, too short to state an annual rate) or `none` (no rate exists). Break-even is the series' LAST zero crossing, not the first: a later purchase can un-pay a plant that was already paid off.","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. `battery_state` and `battery_sources` are optional: they are collected for later capacity measurement and do not affect the grade. An invalid value in them returns 422 for the whole call, flows included. New clients should use the header variant. `data_since` in the response is the start of the plant's earliest stored interval after the call, quarantined hours included, `null` when there are no rows.","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.\n\n`battery_declared=true` without `nameplate_kwh`/`nameplate_kw` means the battery exists but its capacity is to be measured from the operating data. The measurement requires the series to be stored, so combining it with `share_profile=false` returns **422**: supply nameplate data, a known `battery_kwh`/`battery_kw` pair, or save the profile.","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/analysis":{"get":{"tags":["grades"],"summary":"Analysis of the control","description":"Where the battery control loses value: regret per day against the best possible action, summed per kind of deviation and per hour, plus a fingerprint of how the battery was actually run (`fingerprint`: sums per month and local hour). The hour is the interval's local clock hour, 0-23: on the day summer time begins hour 2 is missing, and on the day it ends hour 2 holds two hours' worth of intervals. The fingerprint's `a_ch_an` and `a_dis_an` are measured charge and discharge over the same intervals as the optimal action (`o_ch`, `o_dis`, count `o_intervals`), so the two can be compared over the same days.\n\n`tz` is the plant's time zone (IANA name), the one days and hours are counted in. `wear_ore_per_kwh` is the wear per kWh charged or discharged that the amounts were computed with. Every day row carries `computed_at`, when the row was last computed. `summary.day_bucket` is regret per day and kind of deviation, with the same selection as `summary.buckets` (the sum over days is the bucket's), one row per day and bucket that has intervals.\n\n`status` is `no_data`, `pending`, `running`, `done` or `error`. `pending` and `running` mean a run is waiting or in progress; days already computed are included in the response, and `progress` says how far the run has come (the numbers do not always move forward: a run that is put back in the queue may start over). `done` means there are days and no run is waiting or in progress. `done` together with `job.status` = `error` means the latest update did not complete, so the stored days may be out of date. `error` means the run failed and no days exist. `no_data` means no analysis has been run. `summary` is `null` and `fingerprint` an empty list when there are no days.\n\nThe analysis is computed only when Wolta keeps the hourly series and the grade is measured and accepted. Otherwise `status` is `no_data` and `unavailable_reason` says why: `series_not_kept` (an upload without a stored hourly series), `grade_not_measured` (the grade is an estimate, not measured) or `grade_rejected` (the grade was rejected, e.g. because charge and discharge look swapped). `unavailable_reason` is `null` when the analysis may be computed and when no grade has been computed yet. A preliminary grade, and a grade held out of the comparison because of external control, get an analysis as usual.\n\nEvery day is valued under the tax and credit rules of its own calendar year (`rule_year` on the day). `regime_year` is the year the grade is computed with. `year_terms` lists the terms per calendar year among the days: `rule_year` (the rule year; `rules_fallback` is `true` when the year is missing from the tax table and the rules of `regime_year` are used), `credit_ore` (the tax reduction for micro-production that year, öre per kWh) and `credit_margin`. The reduction's base is the lowest of energy fed in, energy taken out and 30,000 kWh during the calendar year, and `credit_margin` says which term binds: `export` (energy fed in during the year is lowest, so an exported kWh earns the reduction), `import` (energy taken out is lowest, so an imported kWh earns it), `none` (the 30,000 kWh cap, or fed in equal to taken out) or `null` (no reduction that year). The term is decided on `export_kwh` and `import_kwh`, the year's totals in the stored series; `observed_days` of `days_in_year` says how much of the year they cover, and an incomplete year is an estimate. Every day carries its own `credit_margin`.\n\n`worst_days` lists at most 3 analysed days with the largest regret, largest first (equal regret: the later day first).\n\n`view=status` gives a light read for following a run while `status` is `pending` or `running`: only `status`, `unavailable_reason`, `job`, `progress`, `analysis_version`, `days_stored` (the number of stored days the full read shows) and `last_computed_at` (the latest `computed_at` among them, `null` without days). `status` and `progress` are the same as in the full read. Without the parameter, or with `view=full`, the whole response is returned; other values give 422.\n\nThe analysis is queued after every grade calculation. A day that cannot be analysed stays in `days`, with the reason in `undefined_reason`: `reservation`, `no_lookahead`, `parameter_change`, `export_over_import`, `okand_startniva`, `external_infeasible` or `berakningsfel`. When several reasons apply, the day is reported under the first in that order. The last day of the series, and the day before a gap in the series, has no following day to compare against and so cannot be analysed (`no_lookahead`); the analysis therefore reaches at most the day before the last day of the series, and in that respect lags the grade by a day. `okand_startniva` (unknown starting level): the battery's level at the start of the day is not known - the day lies at the start of the analysis period or after a gap in the series, and the battery has not yet been empty or full. The level becomes known once it has been, and the following day is the first to be analysed. History before the period is used when it exists. On such a day the measured level (`open_soc`, `close_soc`, `a_soc_min`, `a_soc_max` and the intervals' `a_soc`) is a lower bound, and an interval only counts as outside the battery model when it is so from every possible level.\n\nThe plant's profile token is sent in `Authorization: Bearer`.","operationId":"analysis_bearer_api_v1_analysis_get","security":[{"ProfileToken":[]}],"parameters":[{"name":"view","in":"query","required":false,"schema":{"enum":["full","status"],"type":"string","description":"`full` (default) gives the whole response, `status` only status and progress for polling.","default":"full","title":"View"},"description":"`full` (default) gives the whole response, `status` only status and progress for polling."}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Analysis Bearer Api V1 Analysis Get"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."},"422":{"description":"`view` is neither `full` nor `status`."}}}},"/api/v1/analysis/intervals":{"get":{"tags":["grades"],"summary":"Analysis intervals","description":"The interval rows for the days `from`–`to` (both inclusive, at most 31 days per call), in columns: `columns` holds one list per field with one value per interval in time order - flows, prices, measured and optimal action, regret and classification. For days that were not analysed, optimal action, regret and classification are `null`; flows, prices and measured level are still present.\n\nThe plant's profile token is sent in `Authorization: Bearer`.","operationId":"analysis_intervals_bearer_api_v1_analysis_intervals_get","security":[{"ProfileToken":[]}],"parameters":[{"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 Analysis Intervals Bearer Api V1 Analysis Intervals Get"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."},"422":{"description":"The range is invalid: `from` is after `to`, the range exceeds 31 days, or a date is missing or not a valid date."}}}},"/api/v1/quarantine":{"get":{"tags":["plant"],"summary":"Hours that are not counted","description":"Clock hours that are not counted because a measured value is physically impossible, for example a whole meter reading instead of one hour of energy. The hours are excluded from the grade, the annual figures, the economics and the analysis. Newest first.\n\nFor each hour the response lists the fields that broke the rule, the value and the cap in kWh for the clock hour, and which layer of the rule rejected the hour: `A` more than 200 kWh in one hour, `B` solar production while the sun was below the horizon across the whole country, `C` above the battery power × 1.5 or the solar peak power × 1.3, `D` above four times the plant's own 99th percentile (with a floor per field). `evidence` is present only on layer C: `soc_refuted` (the state of charge contradicts the value), `unsupported` (the hour's other measurements do not carry the value) or `none` (the value cannot be confirmed).\n\n`systematic` is true when implausible hours are so many that the error looks systematic (wrong unit or a miswired sensor). Nothing is then quarantined: the hours are counted, but the plant is not compared with others. The list is then empty.\n\nAn hour sent again with corrected values is judged again.","operationId":"quarantine_bearer_api_v1_quarantine_get","responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuarantineOut"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"404":{"description":"Unknown token."}},"security":[{"ProfileToken":[]}]}},"/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/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. With `system` = `annat`, the name of the system can be given in `system_name`.","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, as does an id that is just `.` or `..`. 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":false,"title":"Body","description":"Partner-owned fields. All are optional and only the ones you send are changed. Never send `null`; that returns 422 - omit the field instead. A field not listed here returns 403.","properties":{"battery_kw":{"description":"The battery's power rating in kW.","examples":[5],"exclusiveMinimum":0,"title":"Battery Kw","type":"number"},"battery_kwh":{"description":"The battery's capacity in kWh.","examples":[10],"exclusiveMinimum":0,"title":"Battery Kwh","type":"number"},"control_system":{"description":"The control system operating the battery. Strictly validated against the list - use `other` if none of the values fit.","enum":["aikion","checkwatt","emaldo","emhass","ferroamp","greenely","huawei","keepai","manual","other","pixii","reduxi","self_consumption","sigenergy","sonnen","tibber"],"examples":["other"],"title":"Control System","type":"string"},"eff":{"description":"Round-trip efficiency, greater than 0 and at most 1.","examples":[0.9],"title":"Eff","exclusiveMinimum":0,"maximum":1,"type":"number"},"nameplate_kwh":{"description":"Nameplate capacity, if it differs from the deliverable capacity (for example on a degraded battery).","examples":[15],"title":"Nameplate Kwh","exclusiveMinimum":0,"type":"number"},"reserve_pct":{"description":"The share of the battery, in percent, reserved and not used for optimisation.","examples":[10],"title":"Reserve Pct","maximum":100,"minimum":0,"type":"number"},"zone":{"description":"Accepted only with the same value already stored (a no-op). A changed value returns 422: the zone is immutable after creation.","examples":["SE3"],"title":"Zone","type":"string"}}},"example":{"battery_kwh":15}}}},"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\n`ts_utc` is the start of the hour, on the full hour. Insertion is an upsert on the exact timestamp, so resending the same window is safe. A row at `00:30` does not replace the row at `00:00`, however: it is stored alongside it and counted twice. 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. `quarantined_hours` and `quarantined_rows` count the hours and rows quarantined for breaking physical or statistical limits, and `repaired_battery_rows` the quarantined rows that carried battery flow. 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, on the full 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"},"external_control":{"default":false,"description":"True if the battery was controlled by an external flexibility service (for example Tibber Grid Rewards) during the hour. Such hours are neutralised in the optimisation grade instead of counting as poor price control. Stored as false when omitted.","title":"External Control","type":"boolean"}},"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/plants/{client_plant_id}/quarantine":{"get":{"tags":["partner"],"summary":"Hours that are not counted","description":"Clock hours in one of your plants that are not counted because a measured value is not physically possible. The same response the plant's owner sees.\n\nClock hours that are not counted because a measured value is physically impossible, for example a whole meter reading instead of one hour of energy. The hours are excluded from the grade, the annual figures, the economics and the analysis. Newest first.\n\nFor each hour the response lists the fields that broke the rule, the value and the cap in kWh for the clock hour, and which layer of the rule rejected the hour: `A` more than 200 kWh in one hour, `B` solar production while the sun was below the horizon across the whole country, `C` above the battery power × 1.5 or the solar peak power × 1.3, `D` above four times the plant's own 99th percentile (with a floor per field). `evidence` is present only on layer C: `soc_refuted` (the state of charge contradicts the value), `unsupported` (the hour's other measurements do not carry the value) or `none` (the value cannot be confirmed).\n\n`systematic` is true when implausible hours are so many that the error looks systematic (wrong unit or a miswired sensor). Nothing is then quarantined: the hours are counted, but the plant is not compared with others. The list is then empty.\n\nAn hour sent again with corrected values is judged again.","operationId":"plant_quarantine_api_v1_partner_plants__client_plant_id__quarantine_get","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/QuarantineOut"}}}},"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/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/{connector}/connect":{"post":{"tags":["integrations"],"summary":"Start connecting to the provider","description":"Starts the sign-in at the provider's cloud and returns the address the browser should be sent to. The response sets a short-lived connect cookie; 404 if the connector is not enabled, 429 on too many requests.","operationId":"start_connect_api_v1__connector__connect_post","parameters":[{"name":"connector","in":"path","required":true,"schema":{"type":"string","title":"Connector"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Start Connect Api V1  Connector  Connect Post"}}}},"401":{"description":"The provider requires a signed-in account. Sign in and try again."},"404":{"description":"The connector does not exist, or is not enabled."},"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":"Too many requests from the same client. Wait a while."}}}},"/api/v1/{connector}/callback":{"get":{"tags":["integrations"],"summary":"Return from the provider","description":"Receives the provider's response after the owner has approved, fetches the owner's plants and forwards to the selection page. Always responds with a redirect. 404 if the connector is not enabled.","operationId":"callback_api_v1__connector__callback_get","parameters":[{"name":"connector","in":"path","required":true,"schema":{"type":"string","title":"Connector"}},{"name":"code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"error","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"}}],"responses":{"302":{"description":"Successful response.","content":{"application/json":{"schema":{}}}},"404":{"description":"The connector does not exist, or is not enabled."},"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/{connector}/connect/session":{"get":{"tags":["integrations"],"summary":"Status of the connection in progress","description":"The plants the owner approved, with a price-zone hint and what was found on each plant. 404 when no connection is in progress.","operationId":"connect_session_api_v1__connector__connect_session_get","parameters":[{"name":"connector","in":"path","required":true,"schema":{"type":"string","title":"Connector"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Connect Session Api V1  Connector  Connect Session Get"}}}},"404":{"description":"No connection in progress."},"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/{connector}/connect/signup":{"post":{"tags":["integrations"],"summary":"Create an account for connecting","description":"For providers where connecting requires a Wolta account. Sends a link to the email address; if there is no account, one is created. The link leads back to the connect page. Always returns **202**, even for known addresses: any other response would reveal which addresses have an account.\n\nRequires a valid Turnstile token. The route is built for the browser, not for machine use.","operationId":"connect_signup_api_v1__connector__connect_signup_post","parameters":[{"name":"connector","in":"path","required":true,"schema":{"type":"string","title":"Connector"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSignupIn"}}}},"responses":{"202":{"description":"The request was accepted and queued. Poll for the result.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Connect Signup Api V1  Connector  Connect Signup Post"}}}},"404":{"description":"The connector does not exist, is not enabled, or does not require an account."},"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/{connector}/connect/form":{"post":{"tags":["integrations"],"summary":"Connect with serial number and code","description":"For providers where the owner connects with the inverter's serial number and verification code instead of signing in at the provider. Requires a signed-in account and explicit consent. Responds with the page for step 2 and sets a short-lived connect cookie.","operationId":"connect_form_api_v1__connector__connect_form_post","parameters":[{"name":"connector","in":"path","required":true,"schema":{"type":"string","title":"Connector"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Connect Form Api V1  Connector  Connect Form Post"}}}},"401":{"description":"Not signed in."},"404":{"description":"The connector does not exist, is not enabled, or is connected another way."},"409":{"description":"The plant is already connected to another account."},"422":{"description":"Consent is missing, the details do not match, or the plant has no meter."},"429":{"description":"Too many attempts. Try again tomorrow."},"503":{"description":"The provider is busy or not responding. Try again in a minute. With `reason: config` the fault is Wolta's own configuration, not the provider's. With `reason: refused` the provider answered and refused the connection with an error code Wolta does not recognise; `code` carries the code when it has the form of an error code."}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"additionalProperties":false,"description":"Serial number and code from the inverter's rating plate, plus consent to what the connection grants.","properties":{"sn":{"description":"The inverter's serial number, as printed on the rating plate.","maxLength":40,"minLength":1,"title":"Sn","type":"string"},"check_code":{"description":"The verification code from the rating plate or the provider's app. Used only for connecting and never stored.","maxLength":20,"minLength":1,"title":"Check Code","type":"string"},"consent":{"description":"The owner's consent to the rights the connection gives Wolta.","title":"Consent","type":"boolean"}},"required":["sn","check_code","consent"],"title":"ConnectFormIn","type":"object"},"example":{"sn":"A1B2C3D4E5F6G7H8","check_code":"123456","consent":true}}}}}},"/api/v1/{connector}/connect/plant":{"post":{"tags":["integrations"],"summary":"Connect the chosen plant","description":"Creates the plant in Wolta and starts fetching its history. The price zone and the control system (what controls the battery) must be given (422 otherwise), and a plant without a meter cannot be connected (422). Battery details are optional – without them the plant is registered as a solar plant without a battery. For connections made with a serial number the owner chooses how far back the history is fetched: the whole history, or from a date at most 730 days back (422 otherwise). 409 if the plant belongs to another account, 404 when the connect session has expired.","operationId":"connect_plant_api_v1__connector__connect_plant_post","parameters":[{"name":"connector","in":"path","required":true,"schema":{"type":"string","title":"Connector"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectPlantIn"}}}},"responses":{"201":{"description":"The resource was created.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Connect Plant Api V1  Connector  Connect Plant Post"}}}},"401":{"description":"The provider requires a signed-in account. Sign in and try again."},"403":{"description":"The connection was started by a different account."},"404":{"description":"No connection in progress."},"409":{"description":"The plant is already connected to another account."},"422":{"description":"The price zone, control system or history choice is missing or invalid, or the plant has no meter."}}}},"/api/v1/{connector}/status":{"get":{"tags":["integrations"],"summary":"Status of the connection","description":"How far the fetch has come, when it last ran and what looks doubtful in the data. 401 without a valid token, 404 when the plant has no such connection.","operationId":"connector_status_api_v1__connector__status_get","security":[{"ProfileToken":[]}],"parameters":[{"name":"connector","in":"path","required":true,"schema":{"type":"string","title":"Connector"}}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Connector Status Api V1  Connector  Status Get"}}}},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"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/{connector}/binding":{"delete":{"tags":["integrations"],"summary":"Disconnect from the provider","description":"Stops the fetch and releases the connection at the provider. The stored key stays encrypted at Wolta only until the disconnect at the provider has been confirmed, and is deleted afterwards. Data already fetched stays in place. 401 without a valid token, 404 without a connection.","operationId":"connector_detach_api_v1__connector__binding_delete","security":[{"ProfileToken":[]}],"parameters":[{"name":"connector","in":"path","required":true,"schema":{"type":"string","title":"Connector"}}],"responses":{"204":{"description":"Successful response, empty body."},"401":{"description":"Missing or invalid `Authorization: Bearer` header."},"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/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, or to the plant behind a linking code from the Home Assistant integration - give exactly one of them. Returns **202** once the email is sent, regardless of whether the address already exists as an account, so the response never reveals which addresses have an account. An unknown profile token, or an invalid, expired or already-used linking code, instead returns 400, and too many code attempts from the same client return 429.\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"}}}},"400":{"description":"Invalid, expired or already-used claim_code, or unknown profile_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":"Too many code attempts from the same client. Wait a minute."}}}},"/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"}}}},"400":{"description":"The link could not be redeemed. `reason` says why: `expired`, `used` (already used) or `unknown`. `next` gives where a new link is requested, when the link was created on such a page."},"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. Provider connections that require an account are disconnected at the provider. 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. A provider connection that requires an account is disconnected at the provider.","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}/analysis":{"get":{"tags":["account"],"summary":"Analysis of the control (signed in)","description":"The same response as `GET /analysis`, for a plant the account owns.","operationId":"analysis_plant_api_v1_account_plant__submission_id__analysis_get","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}},{"name":"view","in":"query","required":false,"schema":{"enum":["full","status"],"type":"string","description":"`full` (default) gives the whole response, `status` only status and progress for polling.","default":"full","title":"View"},"description":"`full` (default) gives the whole response, `status` only status and progress for polling."}],"responses":{"200":{"description":"Successful response.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Response Analysis Plant Api V1 Account Plant  Submission Id  Analysis 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":"`view` is neither `full` nor `status`. A plant id that is not an integer also returns 422."}}}},"/api/v1/account/plant/{submission_id}/analysis/intervals":{"get":{"tags":["account"],"summary":"Analysis intervals (signed in)","description":"The same response and the same limits as `GET /analysis/intervals`, for a plant the account owns.","operationId":"analysis_intervals_plant_api_v1_account_plant__submission_id__analysis_intervals_get","security":[{"Session":[]}],"parameters":[{"name":"submission_id","in":"path","required":true,"schema":{"type":"integer","title":"Submission Id"}},{"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 Analysis Intervals Plant Api V1 Account Plant  Submission Id  Analysis Intervals 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":"The range is invalid: `from` is after `to`, the range exceeds 31 days, or a date is missing or not a valid date. A plant id that is not an integer also returns 422."}}}},"/api/v1/account/plant/{submission_id}/quarantine":{"get":{"tags":["account"],"summary":"Hours that are not counted (signed in)","description":"The same response as `GET /quarantine`, for a plant the account owns.","operationId":"quarantine_plant_api_v1_account_plant__submission_id__quarantine_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":{"$ref":"#/components/schemas/QuarantineOut"}}}},"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}/connector":{"get":{"tags":["account"],"summary":"Status of the connection (signed in)","description":"The same status as the token route, for a plant the account owns.","operationId":"plant_connector_status_api_v1_account_plant__submission_id__connector_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 Plant Connector Status Api V1 Account Plant  Submission Id  Connector 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 from the provider (signed in)","description":"Stops the fetch and releases the connection at the provider. The stored key stays encrypted at Wolta only until the disconnect at the provider has been confirmed, and is deleted afterwards. Data already fetched stays in place.","operationId":"plant_connector_detach_api_v1_account_plant__submission_id__connector_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":{"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"},"BatteryStateRow":{"properties":{"unit":{"type":"string","maxLength":128,"minLength":1,"title":"Unit","description":"The client's id for the battery unit, for example the sensor's entity_id. Never stored in plain text."},"ts":{"type":"string","format":"date-time","title":"Ts","description":"Start of the interval. Timezone-aware and evenly divisible by period_s."},"period_s":{"type":"integer","enum":[900,3600],"title":"Period S","description":"Length of the interval in seconds: 900 (quarter hour) or 3600 (hour)."},"soc_mean":{"anyOf":[{"type":"number","maximum":100,"minimum":0},{"type":"null"}],"title":"Soc Mean","description":"Time-weighted mean of the state of charge, in percent."},"soc_min":{"anyOf":[{"type":"number","maximum":100,"minimum":0},{"type":"null"}],"title":"Soc Min","description":"Lowest state of charge in percent, including the value at the start of the interval."},"soc_max":{"anyOf":[{"type":"number","maximum":100,"minimum":0},{"type":"null"}],"title":"Soc Max","description":"Highest state of charge in percent, including the value at the start of the interval."},"soh_pct":{"anyOf":[{"type":"number","maximum":110,"minimum":0},{"type":"null"}],"title":"Soh Pct","description":"The battery's state of health in percent as reported by the battery, time-weighted mean."},"bms_capacity_kwh":{"anyOf":[{"type":"number","maximum":1000,"exclusiveMinimum":0},{"type":"null"}],"title":"Bms Capacity Kwh","description":"The battery's own figure for its full capacity in kWh, time-weighted mean."},"soc_max_gap_s":{"anyOf":[{"type":"integer","maximum":86400,"minimum":0},{"type":"null"}],"title":"Soc Max Gap S","description":"The largest gap in seconds between consecutive state-of-charge readings the interval rests on, including from the last reading to the end of the interval. Omitted when unknown."}},"type":"object","required":["unit","ts","period_s"],"title":"BatteryStateRow"},"BatteryStateSource":{"properties":{"unit":{"type":"string","maxLength":128,"minLength":1,"title":"Unit","description":"The same id as in battery_state."},"source":{"type":"string","maxLength":200,"minLength":1,"title":"Source","description":"Where the value comes from, for example ha:<platform>:<entity_id>."},"semantics":{"type":"string","enum":["bms","user","unknown"],"title":"Semantics","description":"bms if the state of charge is the battery management system's own, user if it is scaled to the usable window, otherwise unknown.","default":"unknown"}},"type":"object","required":["unit","source"],"title":"BatteryStateSource"},"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","description":"Controls how the upload is validated and whether battery metrics are computed. The stored battery flag is derived from battery_kwh/battery_kw: with has_battery=false, no battery values are stored, even if sent.","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"},"battery_declared":{"type":"boolean","title":"Battery Declared","default":false},"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":"off"},"persons":{"type":"integer","maximum":10,"minimum":1,"title":"Persons","default":1},"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"},"ConnectPlantIn":{"properties":{"external_id":{"type":"string","maxLength":200,"minLength":1,"title":"External Id"},"zone":{"type":"string","title":"Zone"},"control_system":{"type":"string","maxLength":40,"minLength":1,"title":"Control System"},"control_system_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Control System Name"},"battery_kwh":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Battery Kwh"},"battery_kw":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Battery Kw"},"eff":{"type":"number","maximum":1,"exclusiveMinimum":0,"title":"Eff","default":0.9},"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"},"history_from":{"anyOf":[{"type":"string","maxLength":10},{"type":"null"}],"title":"History From"}},"additionalProperties":false,"type":"object","required":["external_id","zone","control_system"],"title":"ConnectPlantIn","description":"The fields the connect flow supports: the same plant details as the guide, plus the choice of the provider's plant."},"ConnectSignupIn":{"properties":{"email":{"type":"string","maxLength":200,"title":"Email"},"turnstile_token":{"type":"string","title":"Turnstile Token"}},"type":"object","required":["email","turnstile_token"],"title":"ConnectSignupIn"},"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 rows in this batch's hours that were quarantined and carried battery flow. Above 0 indicates a faulty sensor configuration or a jump in your data.","examples":[0]},"quarantined_hours":{"type":"integer","title":"Quarantined Hours","description":"Number of clock hours in this batch quarantined because a field broke physical or statistical limits. The hours do not count towards the grade.","examples":[0]},"quarantined_rows":{"type":"integer","title":"Quarantined Rows","description":"Number of stored rows in the quarantined hours.","examples":[0]}},"type":"object","required":["inserted_hours","first","last","repaired_battery_rows","quarantined_hours","quarantined_rows"],"title":"DataAppendResult"},"DataPut":{"properties":{"rows":{"items":{"$ref":"#/components/schemas/DataRow"},"type":"array","maxItems":40000,"minItems":1,"title":"Rows"},"battery_state":{"items":{"$ref":"#/components/schemas/BatteryStateRow"},"type":"array","maxItems":40000,"title":"Battery State","description":"Optional state of charge per battery unit and interval. Collected so that the battery's capacity can be measured later. Does not affect the grade."},"battery_sources":{"items":{"$ref":"#/components/schemas/BatteryStateSource"},"type":"array","maxItems":16,"title":"Battery Sources","description":"The source of each unit in battery_state. Required the first time a unit is sent."}},"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},"external_control":{"type":"boolean","title":"External Control","description":"True if the battery was controlled by an external flexibility service (for example Tibber Grid Rewards) during the hour. Such hours are neutralised in the optimisation grade instead of counting as poor price control. Stored as false when omitted.","default":false}},"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":["7"]},"flex":{"anyOf":[{"$ref":"#/components/schemas/PartnerFlex"},{"type":"null"}],"description":"External control and reservation during the grading window. The key is OMITTED entirely when the window has neither external control nor reservation - it is never null."}},"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"},"InterestIn":{"properties":{"system":{"type":"string","maxLength":40,"title":"System"},"system_name":{"anyOf":[{"type":"string","maxLength":60},{"type":"null"}],"title":"System Name","description":"The name of the system as free text. Accepted only together with `system` = `annat`; with any other system the field returns 422."},"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":{"anyOf":[{"type":"string","minLength":1},{"type":"null"}],"title":"Profile Token","description":"The plant's owner token. Omit when claim_code is given."},"claim_code":{"anyOf":[{"type":"string","maxLength":64,"minLength":1},{"type":"null"}],"title":"Claim Code","description":"Linking code from the Home Assistant integration. Creates the account and links the plant on confirmation. Omit when profile_token is given."},"turnstile_token":{"type":"string","title":"Turnstile Token"}},"type":"object","required":["email","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"},"PartnerFlex":{"properties":{"flagged_hours":{"type":"integer","title":"Flagged Hours","description":"The number of hourly rows YOU flagged with `external_control: true` in the grading window. Counts only your own flagging - see `comparable` for the verdict, which also weighs in reservation.","examples":[731]},"flagged_share":{"type":"number","title":"Flagged Share","description":"The share of rows in the grading window YOU flagged, rounded to four decimals. Counts only your own flagging, not reservation. Never gate on the share alone - it can round to 0 while `flagged_hours` is above zero.","examples":[0.0834]},"foregone_value_sek":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Foregone Value Sek","description":"Forgone value in SEK: the difference in the plant's total economic value (including grid tariff and tax, not just the spot price) between an unconstrained battery and what was actually run during BOTH externally controlled and reserved periods, measured over the GRADING WINDOW - not over `period_start`-`period_end`. Can be negative: the actual control may happen to yield a higher value than the unconstrained case. Null when the figure could not be computed.","examples":[214.5]},"comparable":{"type":"boolean","title":"Comparable","description":"False when the plant falls outside Wolta's comparison group because too much of the time was externally controlled OR reserved - the union of the two. The grade in `score` still applies to the plant itself - it is the comparison with others that does not hold.","examples":[true]}},"type":"object","required":["flagged_hours","flagged_share","comparable"],"title":"PartnerFlex"},"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, and not just `.` or `..`. 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","keepai","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 per control system in the comparison data.","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":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Battery Kwh"},"battery_kw":{"anyOf":[{"type":"number","exclusiveMinimum":0},{"type":"null"}],"title":"Battery Kw"},"battery_declared":{"type":"boolean","title":"Battery Declared","description":"The plant has a battery but the capacity/power are not known yet: Wolta measures them from the operating data. Omit `battery_kwh`/`battery_kw` in that case.","default":false},"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"],"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"},"solar_events":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Solar 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"},"flex_compensation":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Flex Compensation"},"flex_aggregator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flex Aggregator"},"flex_bid_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Flex Bid Kw"},"flex_reservation":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Flex Reservation"},"flex_reserved_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Flex Reserved From"},"flex_reservations":{"anyOf":[{"items":{"additionalProperties":true,"type":"object"},"type":"array"},{"type":"null"}],"title":"Flex Reservations"}},"type":"object","title":"ProfilePatch","description":"All fields optional - only those supplied are updated. cost_sek/purchase_date = economics (cheap UPDATE); battery_*/eff/pv_kwp = plant (recalculates the grade). zone may be corrected within the same currency, a change across the currency boundary returns 422, and the same value as already stored is a no-op."},"QuarantineField":{"properties":{"field":{"type":"string","enum":["solar_kwh","grid_import_kwh","grid_export_kwh","batt_charged_kwh","batt_discharged_kwh"],"title":"Field","description":"The field that broke the rule."},"value":{"type":"number","title":"Value","description":"The field's sum for the clock hour, kWh."},"cap":{"type":"number","title":"Cap","description":"The cap the field broke, kWh for the clock hour."},"layer":{"type":"string","enum":["A","B","C","D"],"title":"Layer","description":"The layer of the rule that rejected the hour."},"evidence":{"anyOf":[{"type":"string","enum":["soc_refuted","unsupported","none"]},{"type":"null"}],"title":"Evidence","description":"Only on layer C: why other measurements did not confirm the value."}},"type":"object","required":["field","value","cap","layer","evidence"],"title":"QuarantineField"},"QuarantineHour":{"properties":{"hour_utc":{"type":"string","title":"Hour Utc","description":"Start of the clock hour in UTC (ISO 8601)."},"fields":{"items":{"$ref":"#/components/schemas/QuarantineField"},"type":"array","title":"Fields","description":"The fields that broke the rule, in a fixed order: solar, grid import, grid export, charged, discharged."}},"type":"object","required":["hour_utc","fields"],"title":"QuarantineHour"},"QuarantineOut":{"properties":{"systematic":{"type":"boolean","title":"Systematic","description":"True when the error looks systematic; the list is then empty."},"total_hours":{"type":"integer","title":"Total Hours","description":"Number of hours in the list."},"tz":{"type":"string","title":"Tz","description":"The plant's time zone (IANA), for local time."},"hours":{"items":{"$ref":"#/components/schemas/QuarantineHour"},"type":"array","title":"Hours","description":"The hours, newest first."}},"type":"object","required":["systematic","total_hours","tz","hours"],"title":"QuarantineOut"},"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":"off"},"persons":{"type":"integer","maximum":10,"minimum":1,"title":"Persons","default":1},"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, elprisetjustnu.se 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 direct connections to the manufacturers' cloud services."},{"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"}}