{"openapi":"3.1.0","info":{"title":"Aivy Developer API","description":"\nThe **Aivy Developer API** lets you, or an AI agent acting for you, read and\ncontrol **your own** solar + battery system through the Aivy platform by\nIvy Energy (Australia). One API for every brand Aivy runs: Sungrow,\nSigenergy, FoxESS, AlphaESS, SolaX and more.\n\nWhat you can do:\n\n* **Observe**: live SOC and power flows, 5-minute history, a day's energy\n  priced at Amber's actual tariffs, and what the platform did and why.\n* **See prices**: Amber Electric import and feed-in prices, now and forecast.\n* **Control**: switch between self-consumption, Ai+ (price-driven trading) and\n  manual charge/discharge windows; tune the Ai+ engine.\n* **Plan**: ask the server to propose a sell schedule or a discharge that lands\n  on a target SOC, then apply it.\n\nThe API is designed to be read by a language model as much as by a person:\nevery operation states its units, side effects and the rules the server\nenforces. If you are wiring up an AI assistant, start with\n[Agents and MCP](#description/agents-and-mcp).\n\n## Quickstart\n\n**1. Get an API key.** Either way takes a minute:\n\n* **In the app**: open [bridge.ivyenergy.com.au/developer](https://bridge.ivyenergy.com.au/developer),\n  sign in or sign up, and create a key. Choose *Read only* to observe or\n  *Read and control* to allow changes.\n* **From the API** (agents, scripts): `POST /auth/register` with an email and\n  password sends a verification code; `POST /auth/verify` with the code,\n  `accept_terms: true` and a `key_name` creates the account **and** returns\n  the key. An existing account creates keys with `POST /keys` using HTTP\n  Basic (username and password).\n\nThe key starts with `aivy_sk_` and is shown once. Every key also lists the\n**demo sites** (`sandbox:...`, replayed real data, read-only), so the rest of\nthis Quickstart works before any hardware is connected.\n\n**2. Check the key.**\n\n```bash\ncurl https://bridge.ivyenergy.com.au/v1/me \\\n  -H \"Authorization: Bearer $AIVY_API_KEY\"\n```\n\n```json\n{ \"user\": \"Alice\", \"role\": \"customer\", \"key_name\": \"Claude Code\",\n  \"scope\": \"read\", \"sites\": null, \"rate_per_min\": 120 }\n```\n\n**3. Find your site id.**\n\n```bash\ncurl https://bridge.ivyenergy.com.au/v1/sites \\\n  -H \"Authorization: Bearer $AIVY_API_KEY\"\n```\n\n```json\n{ \"sites\": [ { \"id\": \"sungrow:1445844\", \"vendor\": \"sungrow\", \"name\": \"Home\",\n               \"online\": true, \"current_power_w\": 3013.0 } ],\n  \"warnings\": [] }\n```\n\n**4. Read the battery.**\n\n```python\nimport httpx\n\napi = httpx.Client(base_url=\"https://bridge.ivyenergy.com.au/v1\",\n                   headers={\"Authorization\": f\"Bearer {AIVY_API_KEY}\"})\n\nstatus = api.get(\"/sites/sungrow:1445844/status\").json()\nprint(status[\"soc_pct\"], status[\"battery_w\"], status[\"grid_w\"])\n```\n\n```javascript\nconst api = (path, init = {}) =>\n  fetch(`https://bridge.ivyenergy.com.au/v1${path}`, {\n    ...init,\n    headers: { Authorization: `Bearer ${process.env.AIVY_API_KEY}`,\n               \"Content-Type\": \"application/json\", ...init.headers },\n  }).then((r) => r.json());\n\nconst status = await api(\"/sites/sungrow:1445844/status\");\nconsole.log(status.soc_pct, status.battery_w, status.grid_w);\n```\n\n**5. Make your first change** (control key). Charge at 5 kW to 90 % for the\nnext two hours, then return to whatever the site was doing before:\n\n```bash\ncurl -X PUT https://bridge.ivyenergy.com.au/v1/sites/sungrow:1445844/energy-mode \\\n  -H \"Authorization: Bearer $AIVY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"mode\": \"forced\", \"action\": \"charge\", \"power_w\": 5000,\n       \"target_soc\": 90, \"duration_minutes\": 120}'\n```\n\n```json\n{ \"id\": \"sungrow:1445844\", \"energy_mode\": \"forced\",\n  \"time_slots\": [ { \"type\": \"charge\", \"start\": \"14:05\", \"end\": \"16:05\",\n                    \"power\": 5000, \"target_soc\": 90 } ],\n  \"reverts_at\": \"2026-11-05T05:35:12+00:00\", \"reverts_to\": \"amber_smart\",\n  \"note\": \"applied; the engine picks this up on its next tick (about a minute); reverts to amber_smart in 120 min\" }\n```\n\n**Using an AI assistant instead of code?** Install the MCP server and skip the\nrest of this page:\n\n```bash\nclaude mcp add aivy -e AIVY_API_KEY=aivy_sk_... -- uvx aivy-mcp\n```\n\n## Authentication\n\nSend your key as a bearer token on every request:\n\n```\nAuthorization: Bearer aivy_sk_...\n```\n\n`X-API-Key: aivy_sk_...` is also accepted for clients that cannot set an\n`Authorization` header.\n\n| | |\n|---|---|\n| **Scopes** | `read` observes. `control` may also change the energy mode and Ai+ settings. Scope is fixed when the key is created. |\n| **Visibility** | A key sees exactly the sites its owner sees in the app. It can be narrowed to a list of sites when created, never widened. |\n| **Revocation** | Revoke in the app at any time; the key stops working immediately. Up to 10 active keys per account. |\n| **Secrecy** | Treat the key like a password. It is stored hashed; if you lose it, create a new one. Never put it in client-side code or a public repository. |\n\n## Core concepts\n\n### Sites\n\nEvery system is a **site** with an id of the form `vendor:station_id`, for\nexample `sungrow:1445844` or `sigenergy:9c1f...`. `GET /sites` lists yours;\nevery other operation takes one of those ids. A site you may not see is a\n`404`, never a `403`, so the id space does not leak.\n\n### Connecting a site\n\n`GET /vendors` lists every supported brand and what connecting one needs.\n`POST /sites` (control scope) links a system to the account:\n\n* **Serial-number vendors** (Sungrow, FoxESS: serial + NMI; Sigenergy:\n  system id; most others: serial): the vendor binds it and the new site id\n  comes back in `connected`.\n* **OAuth vendors** (AlphaESS, SolaX): the response carries an\n  `authorize_url`; the user opens it, signs in at the vendor, approves Aivy,\n  and the devices appear in `GET /sites` within a minute.\n\n`DELETE /sites/{id}` removes a site from the account. Demo sites\n(`sandbox:...`) need no connecting and cannot be controlled or removed.\n\n### Units and signs\n\n| Quantity | Unit | Sign |\n|---|---|---|\n| Power (`pv_w`, `load_w`, `grid_w`, `battery_w`, `power_w`) | watts | `battery_w > 0` charging, `grid_w > 0` exporting |\n| Energy | kWh | |\n| State of charge | percent | |\n| Prices | Australian cents per kWh, GST included | feed-in is the amount you **earn** per kWh |\n| Timestamps | ISO 8601 with offset | |\n| `HH:MM` fields | the site's local time (`timezone` on the site) | |\n\n### Energy modes\n\n| Mode | What it does | Permanent? |\n|---|---|---|\n| `selfConsumption` | The inverter's own default: solar to the house, surplus to the battery, then export. | yes, unless you give `duration_minutes` |\n| `amber_smart` (**Ai+**) | Aivy's price-driven engine trades the battery against Amber prices. Tune with `ai-plus`. | yes, unless you give `duration_minutes` |\n| `forced` | Manual charge/discharge windows (`time_slots`). | **no**: always time-boxed |\n\n### Time-boxed control\n\nA manual (`forced`) change made through the API always carries a duration:\n`duration_minutes` (default 240, maximum 1440). When it ends the server\nreturns the site to the mode and windows it had before, and records the\nhand-back in the site's operations. A second timed change while one is\npending keeps the **original** hand-back target, so a chain of overrides\nstill ends where the user left things. This holds even if the agent that\nmade the change is gone.\n\nChanges take effect on the engine's next tick, about a minute later. The\nsite's SOC floor and grid export limits apply in every mode and cannot be\nchanged through this API.\n\n### Ai+ settings\n\n`GET /sites/{id}/ai-plus` returns the engine variant in use, its effective\nsettings and `tunable`: every lever with type, range, unit and default.\n`PATCH` with any subset of those levers; values are validated against the\nvariant that will be running, and if any is out of range nothing is changed.\n\n### Alerts\n\n`POST /sites/{id}/alerts` creates a threshold rule: SOC, import or export\nprice, or forecast temperature, above or below a value, with a quiet time\nafter each firing. Every firing lands in the app inbox and, when the app is\ninstalled, on the phone; with `email: true` it is also sent to the\naccount's verified email address (`email_alerts` on `GET /me` says whether\nthat can work). A rule only ever tells someone something: it never changes\nwhat the battery does. `GET /sites/{id}/alerts/events` lists what fired.\n\n### Planning\n\nThe two `plans` endpoints are pure calculations from the site's real\ncapacity, SOC, limits and the Amber forecast. They return proposed\n`time_slots` and the numbers behind them; nothing is applied until you\n`PUT` the mode.\n\n## Agents and MCP\n\nThe API is the backend of **aivy-mcp**, an open-source\n[MCP](https://modelcontextprotocol.io) server\n([GitHub](https://github.com/IVYEnergy/aivy-mcp),\n[PyPI](https://pypi.org/project/aivy-mcp/)) that gives Claude Code, Claude\nDesktop, Cursor, Codex and other MCP clients fourteen tools over these\nendpoints.\n\n```json\n{ \"mcpServers\": { \"aivy\": {\n    \"command\": \"uvx\", \"args\": [\"aivy-mcp\"],\n    \"env\": { \"AIVY_API_KEY\": \"aivy_sk_...\" } } } }\n```\n\nAssistants that cannot run local tools (ChatGPT in the browser, for example)\ncan use the HTTP API directly. Paste this into the chat:\n\n> Use the Aivy Developer API at https://bridge.ivyenergy.com.au/v1 with the\n> header `Authorization: Bearer aivy_sk_...`. Read\n> https://bridge.ivyenergy.com.au/v1/llms.txt first, then GET /sites.\n\nIf you are building your own agent on this API:\n\n* Give it a **read** key until you trust it; add control later.\n* Have it **confirm with the user before every write** (`PUT energy-mode`,\n  `PATCH ai-plus`). The MCP tool descriptions already ask for this.\n* **Do not poll** faster than once a minute; readings do not change faster\n  than that and the rate limit will stop you.\n* Prefer the **planning** endpoints over arithmetic in the prompt: they use\n  the real capacity, limits and prices.\n\n**The playbook.** Tool and endpoint descriptions say what a call does; the\n[agent playbook](https://bridge.ivyenergy.com.au/v1/agent-guide.md) says how\nto get a job done: which calls answer which questions, when to stop and\nconfirm, the step-by-step workflows and what each error means. It ships as\nthe MCP tool `get_agent_guide`, as a Claude skill\n([`skills/aivy`](https://github.com/IVYEnergy/aivy-mcp/tree/main/skills/aivy))\nand as plain Markdown at `/v1/agent-guide.md`; the repository's `evals/`\nfolder has conversation cases to check an agent against it.\n\nA one-page, machine-readable summary of all of this lives at\n[`/v1/llms.txt`](https://bridge.ivyenergy.com.au/v1/llms.txt).\n\n## Errors and rate limits\n\nErrors are JSON with a `detail` object that always has an `error` and often a\n`hint`:\n\n```json\n{ \"detail\": { \"error\": \"this API key is read-only\",\n              \"hint\": \"create a key with the control scope to change settings\" } }\n```\n\n| Status | Meaning |\n|---|---|\n| `401` | Missing, invalid, revoked or expired key, or not a personal key |\n| `403` | The key is read-only and the operation writes |\n| `404` | Unknown site, or one this key may not see |\n| `409` | The change was not applied; the body says why (rejected windows, out-of-range settings, Amber not linked) |\n| `422` | Malformed request body or parameter |\n| `429` | Rate limit (default 120 requests per minute per key); honour `Retry-After` |\n| `503` | No reading available right now; retry after `Retry-After` |\n\n## Changelog\n\n**2026-11-05, v1.0.0.** First release: account, sites, readings, prices,\nenergy mode with time-boxed manual control, Ai+ settings, planning. MCP\nserver `aivy-mcp 0.1.0`.\n","version":"1.0.0","contact":{"name":"Ivy Energy","url":"https://github.com/IVYEnergy/aivy-mcp"},"license":{"name":"Terms of use","url":"https://bridge.ivyenergy.com.au/terms"}},"servers":[{"url":"https://bridge.ivyenergy.com.au/v1","description":"production"}],"paths":{"/me":{"get":{"tags":["account"],"summary":"Who am I","description":"Who this key is and what it may do. A cheap connectivity check.","operationId":"me_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites":{"get":{"tags":["sites"],"summary":"List sites","description":"Every site this key may see, across vendors. Start here: the `id` is\nwhat every other operation takes.","operationId":"list_sites_sites_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl https://bridge.ivyenergy.com.au/v1/sites \\\n  -H \"Authorization: Bearer $AIVY_API_KEY\""},{"lang":"python","label":"Python","source":"import httpx\n\napi = httpx.Client(base_url=\"https://bridge.ivyenergy.com.au/v1\",\n                   headers={\"Authorization\": f\"Bearer {AIVY_API_KEY}\"})\nsites = api.get(\"/sites\").json()[\"sites\"]"},{"lang":"text","label":"MCP (aivy-mcp)","source":"list_sites()"}]},"post":{"tags":["sites"],"summary":"Connect a site","description":"Link a system to this account (control scope).\n\n* **Serial-number vendors** (Sungrow, FoxESS, Sigenergy, ...): the vendor\n  binds the device and the new site is granted to the account; the\n  response lists `connected` site ids. A site already held by another\n  account is reported in `contested` and left for an administrator.\n* **OAuth vendors** (AlphaESS, SolaX): the response carries\n  `authorize_url`. The user opens it in a browser, signs in at the vendor\n  and approves Aivy; the devices then appear in `GET /sites` within a\n  minute. The URL is valid for 15 minutes.\n\nNothing about the vendor account is stored beyond what the link needs;\nit can be removed with `DELETE /sites/{id}`.","operationId":"connect_site_sites_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectSite"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}":{"get":{"tags":["sites"],"summary":"Get a site","description":"What the site is: battery size, power limits, timezone, current mode,\nplus a live SOC and today's totals when the vendor reports them.","operationId":"get_site_sites__site_id__get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sites"],"summary":"Disconnect a site","description":"Remove the site from this account (control scope). The vendor-side\nlink and the site's history are left alone; the account simply stops\nseeing it. Demo sites cannot be disconnected.","operationId":"disconnect_site_sites__site_id__delete","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/status":{"get":{"tags":["readings"],"summary":"Live status","description":"Live power flows and SOC. Vendors refresh about once a minute; a\nmissing channel is null, not zero.","operationId":"site_status_sites__site_id__status_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteStatus"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/curve":{"get":{"tags":["readings"],"summary":"Power and SOC history","description":"Power and SOC history for charting or analysis.\n\nReturns `times` (UTC ISO), `local_times`, `granularity_seconds` and\n`series` keyed by channel (`pv`, `load`, `battery`, `grid`, `soc`; W and %).\nChannels a vendor does not report are absent.","operationId":"site_curve_sites__site_id__curve_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}},{"name":"range","in":"query","required":false,"schema":{"type":"string","pattern":"^(1d|7d|30d)$","description":"1d = 5-minute points for one local day; 7d / 30d = daily","default":"1d","title":"Range"},"description":"1d = 5-minute points for one local day; 7d / 30d = daily"},{"name":"day","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},{"type":"null"}],"description":"Local calendar day (default today)","title":"Day"},"description":"Local calendar day (default today)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/earnings":{"get":{"tags":["readings"],"summary":"Daily energy and earnings","description":"Energy bought, sold and self-consumed for a day, priced at the actual\nAmber half-hourly prices when Amber is linked. An estimate from the\nvendor's power samples, not the retailer's bill; `coverage` says how much\nof the day the samples cover.","operationId":"site_earnings_sites__site_id__earnings_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}},{"name":"date","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"YYYY-MM-DD, 'today' or 'yesterday' (default today)","title":"Date"},"description":"YYYY-MM-DD, 'today' or 'yesterday' (default today)"},{"name":"window_start","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"HH:MM, with window_end: account for part of the day","title":"Window Start"},"description":"HH:MM, with window_end: account for part of the day"},{"name":"window_end","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Window End"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/operations":{"get":{"tags":["readings"],"summary":"Operations log","description":"What the platform did to this site and why, newest first: every\nautomatic (Ai+) and manual command, with its reason and prices.","operationId":"site_operations_sites__site_id__operations_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/prices":{"get":{"tags":["prices"],"summary":"Amber prices","description":"Amber Electric prices for the site: the current interval and the\nforecast, as import price and feed-in earn (both c/kWh, GST incl.), with\na summary of the cheapest and dearest periods. 409 when the site has no\nAmber account linked.","operationId":"site_prices_sites__site_id__prices_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}},{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":24,"minimum":1,"description":"Forecast horizon (max 4 at 5-minute resolution)","default":8,"title":"Hours"},"description":"Forecast horizon (max 4 at 5-minute resolution)"},{"name":"resolution","in":"query","required":false,"schema":{"type":"integer","description":"30 or 5 (minutes)","default":30,"title":"Resolution"},"description":"30 or 5 (minutes)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/energy-mode":{"get":{"tags":["control"],"summary":"Get energy mode","description":"The site's current mode, its forced windows, and any pending timed\nhand-back.","operationId":"get_energy_mode_sites__site_id__energy_mode_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnergyModeState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["control"],"summary":"Set energy mode","description":"Change the energy mode (control scope).\n\n* `forced` needs windows: pass `time_slots`, or the shorthand `action` +\n  `power_w` + `target_soc`/`stop_soc` to build one window from now for\n  `duration_minutes`. Forced is always time-boxed (default 240 min, max\n  1440): when it ends the site goes back to the mode it had before.\n* `amber_smart` / `selfConsumption` are permanent unless `duration_minutes`\n  is given.\n\nThe engine's SOC floor and export limits still apply; this call cannot\noverride them. 409 when the windows were rejected (the body lists why).","operationId":"set_energy_mode_sites__site_id__energy_mode_put","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnergyModeChange"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnergyModeState"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl -X PUT https://bridge.ivyenergy.com.au/v1/sites/sungrow:1445844/energy-mode \\\n  -H \"Authorization: Bearer $AIVY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"mode\": \"forced\", \"action\": \"charge\", \"power_w\": 5000, \"target_soc\": 90, \"duration_minutes\": 120}'"},{"lang":"python","label":"Python","source":"api.put(\"/sites/sungrow:1445844/energy-mode\", json={\n    \"mode\": \"forced\", \"action\": \"charge\",\n    \"power_w\": 5000, \"target_soc\": 90, \"duration_minutes\": 120,\n}).json()"},{"lang":"text","label":"MCP (aivy-mcp)","source":"set_energy_mode(site_id=\"sungrow:1445844\", mode=\"forced\", action=\"charge\",\n                power_w=5000, target_soc=90, duration_minutes=120)"}]}},"/sites/{site_id}/ai-plus":{"get":{"tags":["control"],"summary":"Get Ai+ settings","description":"Ai+ (amber_smart) configuration: which engine variant runs, its\neffective settings, and `tunable` — every lever with type, range, unit\nand default, so you can change them with PATCH.","operationId":"get_ai_plus_sites__site_id__ai_plus_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["control"],"summary":"Update Ai+ settings","description":"Change Ai+ levers and/or switch engine variant (control scope).\nSettings are validated against the variant that will be running; 409\nwith `rejected` when any is out of range, and then nothing is changed.\nDoes not switch the mode itself: use PUT energy-mode for that.","operationId":"patch_ai_plus_sites__site_id__ai_plus_patch","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AiPlusPatch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"x-codeSamples":[{"lang":"shell","label":"curl","source":"curl -X PATCH https://bridge.ivyenergy.com.au/v1/sites/sungrow:1445844/ai-plus \\\n  -H \"Authorization: Bearer $AIVY_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"settings\": {\"hard_floor\": 15}}'"},{"lang":"text","label":"MCP (aivy-mcp)","source":"update_ai_plus(site_id=\"sungrow:1445844\", settings={\"hard_floor\": 15})"}]}},"/sites/{site_id}/plans/discharge-to-target":{"post":{"tags":["planning"],"summary":"Plan a discharge to a target SOC","description":"Work out the steady discharge power that lands on `target_soc` at\n`end`, from the real capacity, SOC and power limits. Returns the proposed\n`time_slots`; apply them with PUT energy-mode (mode forced).","operationId":"plan_discharge_to_target_sites__site_id__plans_discharge_to_target_post","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DischargeToTarget"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/plans/best-price-discharge":{"post":{"tags":["planning"],"summary":"Plan a best-price sell","description":"Pick the best-paid intervals in the window to sell the battery's\nusable energy, from the Amber forecast. Returns intervals, expected\nrevenue and the proposed `time_slots`; nothing is applied.","operationId":"plan_best_price_discharge_sites__site_id__plans_best_price_discharge_post","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BestPriceDischarge"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/terms":{"get":{"tags":["account"],"summary":"Terms to accept","description":"The terms of use and privacy policy a new account accepts, with the\nversion and hash that `POST /auth/verify` records. Registration is only\nopen while `registration_available` is true.","operationId":"auth_terms_auth_terms_get","parameters":[{"name":"locale","in":"query","required":false,"schema":{"type":"string","maxLength":16,"default":"en","title":"Locale"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[]}},"/auth/register":{"post":{"tags":["account"],"summary":"Start a registration","description":"Create an Aivy account from the API: a verification code goes to the\nemail address (or phone). Then `POST /auth/verify` with the code to\nfinish and, in the same call, get a first API key. The same account\nworks in the Aivy app. Rate limited; the response never reveals whether\nthe address already has an account.","operationId":"auth_register_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Register"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[]}},"/auth/verify":{"post":{"tags":["account"],"summary":"Verify and finish registration","description":"Confirm the code, accept the terms, create the account; with\n`key_name` set, a first API key is returned in the same response (shown\nonce). 409 when the terms were not accepted or the code is wrong.","operationId":"auth_verify_auth_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyAndCreateKey"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[]}},"/keys":{"get":{"tags":["account"],"summary":"List API keys","description":"The account's keys (prefix only, never the key itself).","operationId":"list_keys_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"basicAuth":[]}]},"post":{"tags":["account"],"summary":"Create an API key","description":"Create a personal API key with the account's username and password\n(HTTP Basic). The plaintext key is returned once. Keys cannot create\nkeys: an agent holding a key cannot widen its own access.","operationId":"create_key_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"basicAuth":[]}]}},"/keys/{key_id}":{"delete":{"tags":["account"],"summary":"Revoke an API key","description":"Revoke a key; anything using it gets 401 from the next request.","operationId":"revoke_key_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"integer","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"basicAuth":[]}]}},"/vendors":{"get":{"tags":["sites"],"summary":"Supported vendors","description":"Every brand Aivy can connect, what connecting one needs (`connect`),\nand how much of it the platform can control. `available` is false for a\nbrand this deployment has no credentials for yet.","operationId":"list_vendors_vendors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/alerts":{"get":{"tags":["alerts"],"summary":"List alert rules","description":"The account's alert rules on this site. Each says what it watches,\nwhether it emails, and when it last fired.","operationId":"list_alerts_sites__site_id__alerts_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["alerts"],"summary":"Create an alert rule","description":"Be told when SOC, a price or the forecast temperature crosses a\nthreshold. Every firing lands in the app inbox; `email: true` also mails\nthe account's verified address (see `email_alerts` on GET /me). A rule\nnever changes what the battery does. Evaluated about every five minutes;\nat most one firing per `cooldown_minutes`. Up to 50 active rules.","operationId":"create_alert_sites__site_id__alerts_post","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/alerts/{rule_id}":{"put":{"tags":["alerts"],"summary":"Replace an alert rule","description":"Replace the rule's settings. Firings raised under the old settings stop\nbeing delivered.","operationId":"update_alert_sites__site_id__alerts__rule_id__put","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}},{"name":"rule_id","in":"path","required":true,"schema":{"type":"integer","title":"Rule Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["alerts"],"summary":"Delete an alert rule","description":"Retire the rule. Past firings stay in the inbox.","operationId":"delete_alert_sites__site_id__alerts__rule_id__delete","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}},{"name":"rule_id","in":"path","required":true,"schema":{"type":"integer","title":"Rule Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sites/{site_id}/alerts/events":{"get":{"tags":["alerts"],"summary":"Recent alert firings","description":"What fired on this site, newest first, with the value that fired it,\nhow it was delivered and when it was emailed.","operationId":"alert_events_sites__site_id__alerts_events_get","parameters":[{"name":"site_id","in":"path","required":true,"schema":{"type":"string","title":"Site Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/alerts/test-email":{"post":{"tags":["alerts"],"summary":"Send a test alert email","description":"Send one test alert to the account's verified email so you can see\nwhat a firing looks like. 409 when the account has no verified email or\nmail is not configured. Rate limited like everything else.","operationId":"test_alert_email_alerts_test_email_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AiPlusPatch":{"properties":{"variant":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Variant","description":"Switch engine variant; omitted = keep"},"settings":{"type":"object","title":"Settings","description":"Levers to change; names and ranges from GET ai-plus `tunable`"}},"type":"object","title":"AiPlusPatch"},"AlertRuleIn":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name","description":"Shown as the alert's title"},"metric":{"type":"string","pattern":"^(soc|import_price|export_price|temperature)$","title":"Metric","description":"soc (%), import_price / export_price (c/kWh), temperature (forecast, °C)"},"operator":{"type":"string","pattern":"^(above|below)$","title":"Operator","default":"above"},"threshold":{"type":"number","title":"Threshold"},"email":{"type":"boolean","title":"Email","description":"Also email the account's verified address when it fires","default":false},"enabled":{"type":"boolean","title":"Enabled","default":true},"cooldown_minutes":{"type":"integer","maximum":10080.0,"minimum":15.0,"title":"Cooldown Minutes","description":"Quiet time after a firing","default":360},"hysteresis":{"type":"number","maximum":20.0,"minimum":0.0,"title":"Hysteresis","description":"How far back past the threshold before it may fire again","default":1},"active_start":{"type":"string","title":"Active Start","description":"HH:MM site local; same as active_end = all day","default":"00:00"},"active_end":{"type":"string","title":"Active End","default":"00:00"},"lead_minutes":{"type":"integer","maximum":1440.0,"minimum":0.0,"title":"Lead Minutes","description":"temperature only: how far ahead in the forecast","default":120},"message":{"type":"string","maxLength":400,"title":"Message","description":"Your own words, prefixed to the alert text","default":""},"locale":{"type":"string","pattern":"^(en|ja|th|zh-Hans|zh-Hant)$","title":"Locale","description":"Language of the alert text and email","default":"en"}},"type":"object","required":["name","metric","threshold"],"title":"AlertRuleIn","examples":[{"email":true,"metric":"soc","name":"Battery low","operator":"below","threshold":20},{"cooldown_minutes":60,"email":true,"message":"Good time to sell","metric":"export_price","name":"Price spike","operator":"above","threshold":100}]},"BestPriceDischarge":{"properties":{"window_start":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Window Start","description":"HH:MM"},"window_end":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Window End","description":"HH:MM"},"reserve_soc":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Reserve Soc","description":"Keep at least this much"},"min_sell_c":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Sell C","description":"Only sell above this feed-in price, c/kWh"},"battery_capacity_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Capacity Kwh"}},"type":"object","title":"BestPriceDischarge"},"ConnectSite":{"properties":{"vendor":{"type":"string","title":"Vendor","description":"Vendor id from GET /vendors"},"sn":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Sn","description":"Device / inverter serial number"},"nmi":{"anyOf":[{"type":"string","maxLength":20},{"type":"null"}],"title":"Nmi","description":"National Metering Identifier (Sungrow, FoxESS)"},"system_id":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"System Id","description":"Sigenergy system id"},"region":{"type":"string","pattern":"^(AU|EU|SG)$","title":"Region","description":"AlphaESS portal region","default":"AU"}},"type":"object","required":["vendor"],"title":"ConnectSite","examples":[{"nmi":"2001234567","sn":"A2340000001","vendor":"sungrow"},{"system_id":"3f2a9c...","vendor":"sigenergy"},{"vendor":"alphaess"}]},"DischargeToTarget":{"properties":{"end":{"type":"string","title":"End","description":"HH:MM site local time to reach target_soc"},"target_soc":{"type":"integer","maximum":100.0,"minimum":0.0,"title":"Target Soc"},"start":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Start","description":"HH:MM; omitted = now"},"battery_capacity_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Capacity Kwh"}},"type":"object","required":["end","target_soc"],"title":"DischargeToTarget"},"EnergyModeChange":{"properties":{"mode":{"type":"string","pattern":"^(selfConsumption|forced|amber_smart)$","title":"Mode"},"time_slots":{"anyOf":[{"items":{"$ref":"#/components/schemas/Slot"},"type":"array"},{"type":"null"}],"title":"Time Slots","description":"forced mode windows; omitted = keep existing"},"variant":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Variant","description":"amber_smart engine variant (see GET ai-plus)"},"duration_minutes":{"anyOf":[{"type":"integer","maximum":1440.0,"minimum":0.0},{"type":"null"}],"title":"Duration Minutes","description":"Hand the site back to its previous mode after this long. forced defaults to 240; 0 = permanent (allowed for selfConsumption / amber_smart only)"},"action":{"anyOf":[{"type":"string","pattern":"^(charge|discharge)$"},{"type":"null"}],"title":"Action","description":"Shorthand for forced mode: build one window starting now for duration_minutes with power_w / target_soc / stop_soc"},"power_w":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Power W"},"target_soc":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Target Soc"},"stop_soc":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Stop Soc"}},"type":"object","required":["mode"],"title":"EnergyModeChange","examples":[{"action":"charge","duration_minutes":120,"mode":"forced","power_w":5000,"target_soc":90},{"duration_minutes":180,"mode":"forced","time_slots":[{"end":"20:30","power":5000,"start":"17:30","stop_soc":20,"type":"discharge"}]},{"mode":"amber_smart"}]},"EnergyModeState":{"properties":{"id":{"type":"string","title":"Id"},"energy_mode":{"type":"string","title":"Energy Mode"},"time_slots":{"items":{"type":"object"},"type":"array","title":"Time Slots"},"soc_floor_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Soc Floor Pct"},"variant":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Variant","description":"Ai+ engine variant when mode is amber_smart"},"settings":{"anyOf":[{"type":"object"},{"type":"null"}],"title":"Settings","description":"Effective Ai+ settings when mode is amber_smart"},"reverts_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reverts At","description":"When a timed override ends (ISO 8601)"},"reverts_to":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reverts To"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["id","energy_mode","time_slots"],"title":"EnergyModeState","examples":[{"energy_mode":"forced","id":"sungrow:1445844","note":"applied; the engine picks this up on its next tick (about a minute); reverts to amber_smart in 120 min","reverts_at":"2026-11-05T05:35:12+00:00","reverts_to":"amber_smart","soc_floor_pct":10.0,"time_slots":[{"end":"16:05","power":5000,"start":"14:05","target_soc":90,"type":"charge"}]}]},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"KeyRequest":{"properties":{"name":{"type":"string","maxLength":100,"minLength":1,"title":"Name","description":"What this key is for, e.g. 'Claude Code'"},"scope":{"type":"string","pattern":"^(read|control)$","title":"Scope","default":"read"},"sites":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sites","description":"Optional allow-list of site ids"},"sandbox":{"type":"boolean","title":"Sandbox","description":"Also list the demo sites (read-only)","default":true}},"type":"object","required":["name"],"title":"KeyRequest","examples":[{"name":"Claude Code","scope":"control"}]},"Me":{"properties":{"user":{"type":"string","title":"User","description":"Display name of the account the key belongs to"},"role":{"type":"string","title":"Role"},"key_name":{"type":"string","title":"Key Name"},"scope":{"type":"string","title":"Scope","description":"read or control"},"sites":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Sites","description":"Site allow-list on this key; null = all the account's sites"},"rate_per_min":{"type":"integer","title":"Rate Per Min"},"email_alerts":{"type":"boolean","title":"Email Alerts","description":"True when the account has a verified email and the platform can send alert emails to it","default":false}},"type":"object","required":["user","role","key_name","scope","sites","rate_per_min"],"title":"Me","examples":[{"email_alerts":true,"key_name":"Claude Code","rate_per_min":120,"role":"customer","scope":"read","user":"Alice"}]},"Register":{"properties":{"identifier":{"type":"string","maxLength":254,"title":"Identifier","description":"Email address (or Australian mobile number) to verify"},"password":{"type":"string","maxLength":128,"minLength":8,"title":"Password"},"display_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Display Name"},"locale":{"type":"string","maxLength":16,"title":"Locale","default":"en"}},"type":"object","required":["identifier","password"],"title":"Register","examples":[{"display_name":"Alice","identifier":"alice@example.com","password":"a long private phrase"}]},"Site":{"properties":{"id":{"type":"string","title":"Id","description":"vendor:station_id — use this everywhere else"},"vendor":{"type":"string","title":"Vendor"},"name":{"type":"string","title":"Name"},"online":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Online"},"current_power_w":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Current Power W","description":"Vendor's headline power figure, W"},"demo":{"type":"boolean","title":"Demo","description":"A shared demo site (replayed data, read-only through the API)","default":false}},"type":"object","required":["id","vendor","name"],"title":"Site","examples":[{"current_power_w":3013.0,"id":"sungrow:1445844","name":"Home","online":true,"vendor":"sungrow"}]},"SiteDetail":{"properties":{"id":{"type":"string","title":"Id"},"vendor":{"type":"string","title":"Vendor"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"online":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Online"},"timezone":{"type":"string","title":"Timezone"},"energy_mode":{"type":"string","title":"Energy Mode"},"time_slots":{"items":{"type":"object"},"type":"array","title":"Time Slots"},"battery_capacity_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Capacity Kwh"},"capacity_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Capacity Source"},"max_power_w":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Max Power W","description":"Largest charge/discharge power the battery accepts"},"export_limit_w":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Export Limit W","description":"Grid export cap (min of inverter and DNSP limits)"},"soc_floor_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Soc Floor Pct","description":"Ai+ never discharges below this"},"soc_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Soc Pct"},"usable_kwh_now":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Usable Kwh Now"},"pv_system_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pv System Kw"},"today_energy_kwh":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Today Energy Kwh"},"today_income":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Today Income"}},"type":"object","required":["id","vendor","timezone","energy_mode","time_slots","battery_capacity_kwh","soc_pct","usable_kwh_now"],"title":"SiteDetail"},"SiteList":{"properties":{"sites":{"items":{"$ref":"#/components/schemas/Site"},"type":"array","title":"Sites"},"warnings":{"items":{"type":"string"},"type":"array","title":"Warnings","description":"Vendors that could not be listed right now"}},"type":"object","required":["sites"],"title":"SiteList"},"SiteStatus":{"properties":{"id":{"type":"string","title":"Id"},"at":{"type":"string","title":"At","description":"When this reading was taken (ISO 8601, UTC)"},"soc_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Soc Pct"},"pv_w":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pv W"},"load_w":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Load W"},"grid_w":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Grid W","description":">0 exporting, <0 importing"},"battery_w":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery W","description":">0 charging, <0 discharging"},"source":{"type":"string","title":"Source","description":"vendor (live read) or stored (last reading we kept)"}},"type":"object","required":["id","at","soc_pct","source"],"title":"SiteStatus","examples":[{"at":"2026-11-05T03:35:12+00:00","battery_w":500.0,"grid_w":1693.0,"id":"sungrow:1445844","load_w":820.0,"pv_w":3013.0,"soc_pct":89.5,"source":"vendor"}]},"Slot":{"properties":{"type":{"type":"string","pattern":"^(charge|discharge)$","title":"Type"},"start":{"type":"string","title":"Start","description":"HH:MM site local time"},"end":{"type":"string","title":"End","description":"HH:MM, exclusive; earlier than start = crosses midnight"},"target_soc":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Target Soc","description":"charge slots: stop at this SOC"},"stop_soc":{"anyOf":[{"type":"integer","maximum":100.0,"minimum":0.0},{"type":"null"}],"title":"Stop Soc","description":"discharge slots: stop at this SOC"},"power":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Power","description":"W; on a paced slot the most it may use"},"pace":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Pace","description":"discharge: spread so stop_soc is reached exactly at end"}},"type":"object","required":["type","start","end"],"title":"Slot"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyAndCreateKey":{"properties":{"registration_id":{"type":"string","maxLength":64,"title":"Registration Id"},"registration_session":{"type":"string","maxLength":100,"title":"Registration Session"},"code":{"type":"string","maxLength":8,"minLength":4,"title":"Code","description":"The code from the verification email / SMS"},"accept_terms":{"type":"boolean","title":"Accept Terms","description":"Must be true: the Aivy terms of use and privacy policy (GET /auth/terms) are accepted on this account"},"key_name":{"anyOf":[{"type":"string","maxLength":100},{"type":"null"}],"title":"Key Name","description":"Also create a first API key with this name"},"scope":{"type":"string","pattern":"^(read|control)$","title":"Scope","default":"read"},"locale":{"type":"string","maxLength":16,"title":"Locale","default":"en"}},"type":"object","required":["registration_id","registration_session","code","accept_terms"],"title":"VerifyAndCreateKey"}},"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"aivy_sk_...","description":"Personal API key: from the Aivy app (user menu, API keys and AI access) or POST /keys."},"basicAuth":{"type":"http","scheme":"basic","description":"Account username and password; only for creating and revoking keys."}}},"tags":[{"name":"account","description":"Who the key belongs to","x-displayName":"Account"},{"name":"sites","description":"Your solar + battery systems","x-displayName":"Sites"},{"name":"readings","description":"Live and historical power, SOC and energy","x-displayName":"Readings"},{"name":"prices","description":"Retail prices (Amber Electric) for a site","x-displayName":"Prices"},{"name":"control","description":"Energy mode and Ai+ settings (control scope to write)","x-displayName":"Control"},{"name":"planning","description":"Pure calculations that propose a schedule; nothing is applied","x-displayName":"Planning"},{"name":"alerts","description":"Threshold rules that notify by inbox, push and email; never command a device","x-displayName":"Alerts"}],"security":[{"bearerAuth":[]}]}