{
  "openapi": "3.1.0",
  "info": {
    "title": "Solar Assistant Cloud API",
    "version": "1",
    "summary": "Site administration, user management, and live inverter data for Solar Assistant installations.",
    "description": "The Solar Assistant Cloud API covers user and site administration as well as\nreal-time metrics read from and settings written to devices through the proxy.\n\n## Authentication\n\nThe operations under the \"Accounts\" tag are how you get a token, so they take none.\nEvery other operation on solar-assistant.io needs one in the `Authorization` header:\n\n    Authorization: Bearer <token>\n\nThe operations under the \"Device\" tag are the exception: they run against a proxy host\nrather than against the cloud, and take the short-lived token from\n`POST /api/v1/sites/{id}/authorize` instead, as a header for REST and as a query\nparameter for the WebSocket.\n\nThere are four kinds of token, and which operations a token may call is decided\nby the scopes it carries:\n\n| Token | How you get it | Scopes |\n| --- | --- | --- |\n| API token | Generated on your user details page (https://solar-assistant.io/user/edit#api) | `sites:read_all`, `sites:write_all`, `sites:recover_all`, `users:read_all`, `orders:read_all`, `orders:write_all`, `products:read_all`, `tickets:read_all`, `tickets:write_all` |\n| Session token | `POST /api/v1/sign_in` and the other account operations | `sites:read_all`, `sites:write_all`, `sites:recover_all`, `users:read_all`, all restricted to the organization you signed in against |\n| OAuth token | Authorization-code flow, after the user grants consent | `openid`, `sites:read_single` for the one site the user picked |\n| MCP token | Authorization-code flow from an MCP client | `openid`, `sites:read_all`, `users:read_all`, `orders:read_all`, `products:read_all` |\n\nEach operation below lists the scope it requires under `security`. A token that\nlacks the scope is refused with 403 before the operation runs, so the scope check\nis not a substitute for the permission check that follows it: a token may hold\n`sites:read_all` and still be refused a site its account cannot see.\n\n## Errors\n\nErrors carry a JSON body. Most are `{\"error\": \"Site not found\"}`; the operations\nthat validate a form return `{\"errors\": {\"password\": [\"is too short\"]}}` with the\nfield names as keys. A handful of back-office operations outside this document\nreturn validation failures as HTTP 403 with `{\"status\": \"error\", \"errors\": {...}}`.\n\n## Reading a site's data\n\nMetrics are not served from solar-assistant.io. Call `POST /api/v1/sites/{id}/authorize`\nto get the proxy `host`, a `token`, and the `site_key`, then call the device's own\nREST and WebSocket API on that host with those as headers. The device API is the\nsame one you would call over the local network.\n\n## Semantic access for agents\n\nAn agent that wants to ask questions rather than call endpoints (\"which of my sites\nstopped reporting?\") should use the MCP server at `POST https://solar-assistant.io/mcp`\ninstead of this API. Its metadata, including the scopes it issues, is published at\nhttps://solar-assistant.io/.well-known/oauth-protected-resource.\n"
  },
  "externalDocs": {
    "description": "Cloud API guide",
    "url": "https://solar-assistant.io/help/integration/cloud-api"
  },
  "servers": [
    {
      "url": "https://solar-assistant.io",
      "description": "Cloud API"
    },
    {
      "url": "https://{proxy_host}",
      "description": "Proxy pass-through (host from the authorize response)",
      "variables": {
        "proxy_host": {
          "default": "us-htz-1.solar-assistant.io",
          "description": "Proxy hostname returned by POST /api/v1/sites/{id}/authorize"
        }
      }
    }
  ],
  "tags": [
    {
      "name": "Accounts",
      "description": "Sign-in, registration, and password handling. These operations take no bearer\ntoken; they are what an organization's own portal or mobile app calls to get one.\n"
    },
    {
      "name": "Users",
      "description": "The signed-in user's profile, and the user directory."
    },
    {
      "name": "Sites",
      "description": "Solar Assistant installations, their details, and access to them."
    },
    {
      "name": "Site members",
      "description": "Who may see and administer a site."
    },
    {
      "name": "Device",
      "description": "Live data read from, and settings written to, the inverter through the cloud\nproxy. Writing a setting changes the physical configuration of the customer's\ninverter and takes effect immediately; there is no confirmation step and no undo.\n"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API token or session token. See the Authentication section above."
      },
      "oauth2": {
        "type": "oauth2",
        "description": "Authorization-code flow with PKCE, as advertised by\nhttps://solar-assistant.io/.well-known/openid-configuration. `code_challenge_method`\nmust be `S256`; `plain` is refused. The scopes an application may ask for are fixed\nwhen the application is registered, so requesting a scope outside that set fails at\nthe authorize step rather than at consent.\n",
        "flows": {
          "authorizationCode": {
            "authorizationUrl": "https://solar-assistant.io/oauth/authorize",
            "tokenUrl": "https://solar-assistant.io/oauth/token",
            "scopes": {
              "openid": "Identify the user who granted consent.",
              "sites:read_single": "Read the one site the user picked while granting consent.",
              "sites:read_all": "Read every site the account can see.",
              "sites:write_all": "Register sites, and add, change or remove site members.",
              "sites:recover_all": "Open and close a support recovery session on a site.",
              "users:read_all": "Read the user directory.",
              "orders:read_all": "Read shop orders.",
              "orders:write_all": "Mark shop orders as paid.",
              "products:read_all": "Read the shop catalogue.",
              "tickets:read_all": "Read support tickets.",
              "tickets:write_all": "Reply to and update support tickets."
            }
          }
        }
      },
      "proxyAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The `token` from the authorize response. Expires after 7 days."
      }
    },
    "parameters": {
      "Offset": {
        "name": "offset",
        "in": "query",
        "description": "Number of records to skip.",
        "schema": {
          "type": "integer",
          "default": 0
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum records to return. The default is 20, so a client that does not paginate sees only the first 20.",
        "schema": {
          "type": "integer",
          "default": 20
        }
      },
      "SiteId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Site id from the list sites response.",
        "schema": {
          "type": "integer"
        }
      },
      "ProxySiteId": {
        "name": "Site-Id",
        "in": "header",
        "required": true,
        "description": "The `site_id` from the authorize response.",
        "schema": {
          "type": "integer"
        }
      },
      "ProxySiteKey": {
        "name": "Site-Key",
        "in": "header",
        "required": true,
        "description": "The `site_key` from the authorize response.",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human readable description of what went wrong."
          }
        },
        "required": [
          "error"
        ]
      },
      "ValidationError": {
        "type": "object",
        "description": "Returned when a submitted field fails validation. Keys are field names.",
        "properties": {
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          }
        },
        "required": [
          "errors"
        ]
      },
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "phone_number": {
            "type": "string"
          },
          "locale": {
            "type": "string",
            "description": "Language code applied to the web pages and emails we send."
          },
          "organization": {
            "description": "Present when the token is scoped to an organization; null otherwise.",
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          }
        }
      },
      "Session": {
        "type": "object",
        "description": "An authenticated session. `token` is the bearer token for every other operation.",
        "properties": {
          "token": {
            "type": "string"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "user": {
            "$ref": "#/components/schemas/User"
          }
        },
        "required": [
          "token",
          "expires_at",
          "user"
        ]
      },
      "SiteMember": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "member"
            ]
          }
        }
      },
      "SiteSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "Also the site's hostname on the proxy. Null until the site is named."
          },
          "description": {
            "type": "string"
          },
          "proxy": {
            "type": "string",
            "description": "Proxy the device is connected to, for example `us-htz-1`."
          },
          "arch": {
            "type": "string",
            "description": "Device CPU architecture."
          },
          "board": {
            "type": "string",
            "description": "Single-board computer the device runs on."
          },
          "beta": {
            "type": "boolean",
            "description": "Whether the device takes beta releases."
          },
          "build_date": {
            "type": "string",
            "format": "date-time",
            "description": "Build date of the Solar Assistant release the device is running."
          },
          "last_seen_at": {
            "type": "string",
            "format": "date-time",
            "description": "Last time the device reached the cloud. A site that has stopped reporting keeps its old value here."
          },
          "local_ip": {
            "type": "string",
            "description": "The device's address on the customer's own network."
          },
          "inverter": {
            "type": "string",
            "description": "Inverter model the device is configured for."
          },
          "inverter_count": {
            "type": "integer"
          },
          "inverter_params": {
            "type": "object",
            "additionalProperties": true
          },
          "battery": {
            "type": "string",
            "description": "Battery or BMS model the device is configured for."
          },
          "battery_count": {
            "type": "integer"
          },
          "battery_params": {
            "type": "object",
            "additionalProperties": true
          },
          "owner": {
            "$ref": "#/components/schemas/SiteMember"
          }
        }
      },
      "Site": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SiteSummary"
          },
          {
            "type": "object",
            "properties": {
              "organization": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "id": {
                    "type": "integer"
                  },
                  "name": {
                    "type": "string"
                  },
                  "cloud_host": {
                    "type": "string"
                  }
                }
              },
              "users": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SiteMember"
                }
              }
            }
          }
        ]
      },
      "SiteAuthorization": {
        "type": "object",
        "description": "Named for the caller rather than for our own fields: `host` is where you send\nAPI calls, `site_host` is where you browse the site.\n",
        "properties": {
          "host": {
            "type": "string",
            "description": "Proxy hostname to use for device requests."
          },
          "site_host": {
            "type": "string",
            "description": "Hostname to browse the site's own web interface on. Absent while the site is unnamed."
          },
          "site_id": {
            "type": "integer"
          },
          "site_name": {
            "type": "string"
          },
          "site_key": {
            "type": "string"
          },
          "local_ip": {
            "type": "string"
          },
          "token": {
            "type": "string",
            "description": "Bearer token for proxy requests. Expires after 7 days."
          }
        },
        "required": [
          "host",
          "site_id",
          "site_key",
          "token"
        ]
      },
      "Metric": {
        "type": "object",
        "properties": {
          "topic": {
            "type": "string",
            "description": "Identifies the value, for example `inverter_1/output_source_priority`."
          },
          "group": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Display name of the metric."
          },
          "value": {
            "description": "Number for a measurement, string for a setting.",
            "type": [
              "number",
              "string"
            ]
          },
          "unit": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "A required parameter is missing or unparseable.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "The bearer token is missing, expired, or not valid.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "The token lacks the scope for this operation, or the account may not act on this record.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "No such record, or none this account may see.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "The request was understood but rejected.",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                {
                  "$ref": "#/components/schemas/Error"
                },
                {
                  "$ref": "#/components/schemas/ValidationError"
                }
              ]
            }
          }
        }
      }
    }
  },
  "paths": {
    "/api/v1/user": {
      "get": {
        "operationId": "getCurrentUser",
        "tags": [
          "Users"
        ],
        "summary": "Get current user",
        "description": "Read-only. Returns the profile of the account the token belongs to. Any valid token may call this.",
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "patch": {
        "operationId": "updateCurrentUser",
        "tags": [
          "Users"
        ],
        "summary": "Update current user",
        "description": "Writes the account's preferred language and returns the updated profile. Only\n`locale` can be set; sending any other field is rejected with 400 rather than\nignored. Any valid token may call this.\n",
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "locale"
                ],
                "properties": {
                  "locale": {
                    "type": "string",
                    "description": "Language code applied to the web pages and emails we send.",
                    "enum": [
                      "af",
                      "cs",
                      "da",
                      "de",
                      "el",
                      "en",
                      "es",
                      "fr",
                      "hu",
                      "it",
                      "lt",
                      "nl",
                      "pl",
                      "pt",
                      "ro",
                      "uk",
                      "vi",
                      "zh_CN",
                      "zh_TW"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/api/v1/users": {
      "get": {
        "operationId": "listUsers",
        "tags": [
          "Users"
        ],
        "summary": "List users",
        "description": "Read-only. Returns the users this account may see, which for an organization\ntoken is the organization's own users.\n",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "users:read_all"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "q",
            "in": "query",
            "description": "Free-text search over the user's id, name, email, and phone number.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/User"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/sites": {
      "get": {
        "operationId": "listSites",
        "tags": [
          "Sites"
        ],
        "summary": "List sites",
        "description": "Read-only. Returns the sites this account may see, newest first for staff and by name otherwise.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "sites:read_all"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Offset"
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "name": "q",
            "in": "query",
            "description": "Free-text search over the site's id, name, description, proxy, inverter, battery, and hardware.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SiteSummary"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          }
        }
      }
    },
    "/api/v1/sites/register": {
      "post": {
        "operationId": "registerSite",
        "tags": [
          "Sites"
        ],
        "summary": "Register a site",
        "description": "Writes. Claims an unregistered Solar Assistant device and associates it with the\nauthenticated account. If the site is already registered to this account, the\nexisting site is returned unchanged. Lookup by `uid` is rate limited.\n",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "sites:write_all"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "uid"
                ],
                "properties": {
                  "uid": {
                    "type": "string",
                    "description": "Unique identifier of the device."
                  },
                  "name": {
                    "type": "string",
                    "description": "Display name for the site, and its hostname on the proxy."
                  },
                  "description": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteSummary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "description": "The device exists but has not been activated yet, or a submitted field is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationError"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sites/{id}": {
      "get": {
        "operationId": "getSite",
        "tags": [
          "Sites"
        ],
        "summary": "Show site",
        "description": "Read-only. Returns full details for a site including its members and organization.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "sites:read_all"
            ]
          },
          {
            "oauth2": [
              "sites:read_single"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/sites/{id}/authorize": {
      "post": {
        "operationId": "authorizeSite",
        "tags": [
          "Sites"
        ],
        "summary": "Generate site access token",
        "description": "Writes a new token, but changes nothing about the site. Returns temporary\ncredentials for direct access to a site through the cloud proxy: send the\nreturned `host` the device requests, with `token` as the bearer token and\n`Site-Id` and `Site-Key` as headers. The token expires after 7 days.\n",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "sites:read_all"
            ]
          },
          {
            "oauth2": [
              "sites:read_single"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteAuthorization"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/sites/{id}/users": {
      "get": {
        "operationId": "listSiteMembers",
        "tags": [
          "Site members"
        ],
        "summary": "List site members",
        "description": "Read-only. Returns all members of a site with their roles. The owner is always included.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "sites:read_all"
            ]
          },
          {
            "oauth2": [
              "sites:read_single"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SiteMember"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "post": {
        "operationId": "inviteSiteMember",
        "tags": [
          "Site members"
        ],
        "summary": "Invite or update a site member",
        "description": "Writes. Invites a user by email. If the email belongs to an existing account they\nare linked immediately; otherwise an account is created for them and they receive\nan invitation email. Posting an existing member's email updates their role instead.\nSetting the role to `owner` transfers ownership of the site.\n",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "sites:write_all"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "role"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "member",
                      "admin",
                      "owner"
                    ]
                  },
                  "first_name": {
                    "type": "string",
                    "description": "Required when inviting a new user."
                  },
                  "last_name": {
                    "type": "string",
                    "description": "Required when inviting a new user."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteMember"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/api/v1/sites/{id}/users/{user_id}": {
      "patch": {
        "operationId": "updateSiteMember",
        "tags": [
          "Site members"
        ],
        "summary": "Update a site member's role",
        "description": "Writes. Requires admin or owner access on the site.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "sites:write_all"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "member",
                      "admin",
                      "owner"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteMember"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "removeSiteMember",
        "tags": [
          "Site members"
        ],
        "summary": "Remove a site member",
        "description": "Writes. Removes a member's access to the site. The site owner cannot be removed.",
        "security": [
          {
            "bearerAuth": []
          },
          {
            "oauth2": [
              "sites:write_all"
            ]
          }
        ],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SiteId"
          },
          {
            "name": "user_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/v1/sign_in": {
      "post": {
        "operationId": "signIn",
        "tags": [
          "Accounts"
        ],
        "summary": "Sign in",
        "description": "Writes a new session token. Authenticates a user and returns a token scoped to\n`organization_id`; pass `\"none\"` for a token that is not scoped to an\norganization. Also accepts a `pending_token` in place of a password, to finish\na sign-in that was blocked on email confirmation.\n\nReturns 412 while the account exists but its email is not yet confirmed. The\n412 body carries a `pending_token`; poll this operation with it until the user\nhas clicked the link in the confirmation email.\n",
        "security": [],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "organization_id"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string",
                    "description": "Required unless using `pending_token`."
                  },
                  "pending_token": {
                    "type": "string",
                    "description": "Token from a previous 412. Use instead of a password once the email is confirmed."
                  },
                  "organization_id": {
                    "description": "Your organization id, or `\"none\"` for an unscoped token.",
                    "type": [
                      "integer",
                      "string"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "412": {
            "description": "Email not yet confirmed. Poll with `pending_token`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "next_step": {
                      "type": "string",
                      "const": "click_email_link"
                    },
                    "pending_token": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/api/v1/register": {
      "post": {
        "operationId": "registerUser",
        "tags": [
          "Accounts"
        ],
        "summary": "Register a new user account",
        "description": "Writes. Creates a user account and sends a confirmation email. Only available to\norganizations, and the confirmation email is branded for the organization you name.\n\nRegistration is a three-step sequence:\n\n1. Open https://solar-assistant.io/register/verify in a WebView (mobile) or iframe\n   (web). The page shows a compact bot-verification widget and on completion sends\n   a postMessage with `{ verification_token: \"...\" }`. The token is single-use and\n   expires after a few minutes.\n2. Call this operation with the email, password, verification token, and\n   organization id. On success you receive a `pending_token`.\n3. Once the user clicks the link in their email, call `POST /api/v1/sign_in` with\n   the email and `pending_token`. Poll until it succeeds; it returns 412 while the\n   account is unconfirmed.\n",
        "security": [],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "password",
                  "verification_token",
                  "organization_id"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "password": {
                    "type": "string"
                  },
                  "verification_token": {
                    "type": "string",
                    "description": "Bot verification token from /register/verify."
                  },
                  "organization_id": {
                    "type": "integer",
                    "description": "Your organization id. The confirmation email is branded for this organization."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Account created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "next_step": {
                      "type": "string",
                      "const": "click_email_link"
                    },
                    "pending_token": {
                      "type": "string"
                    },
                    "user": {
                      "type": "object",
                      "properties": {
                        "email": {
                          "type": "string",
                          "format": "email"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "422": {
            "description": "Validation failed, or the bot verification token was rejected.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationError"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/user/confirm": {
      "post": {
        "operationId": "confirmEmail",
        "tags": [
          "Accounts"
        ],
        "summary": "Confirm email address",
        "description": "Writes. Confirms a user's email address with the token from the confirmation email\nand returns a session token. Use this when your own frontend handles the\nconfirmation URL, rather than polling `POST /api/v1/sign_in` with a `pending_token`.\n",
        "security": [],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token",
                  "organization_id"
                ],
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "Confirmation token from the URL in the email."
                  },
                  "organization_id": {
                    "description": "Your organization id, or `\"none\"` for an unscoped token.",
                    "type": [
                      "integer",
                      "string"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Confirmed and signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "422": {
            "description": "The confirmation token is invalid or has expired, or `organization_id` is not a real organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/password/reset": {
      "post": {
        "operationId": "requestPasswordReset",
        "tags": [
          "Accounts"
        ],
        "summary": "Request password reset",
        "description": "Writes. Sends a password reset email. Always returns 200 whether or not the email\nbelongs to an account, so it cannot be used to discover who has one. Pass\n`organization_id` to brand the email for your organization.\n",
        "security": [],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "verification_token"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "verification_token": {
                    "type": "string",
                    "description": "Bot verification token from /register/verify."
                  },
                  "organization_id": {
                    "type": "integer",
                    "description": "Your organization id. The reset email is branded for this organization."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK. The body is an empty object whether or not the email exists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "422": {
            "description": "Bot verification failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/password/reset/{token}": {
      "put": {
        "operationId": "resetPassword",
        "tags": [
          "Accounts"
        ],
        "summary": "Set new password",
        "description": "Writes. Sets a new password using the token from the reset email, and returns a\nsession token so the user is signed in straight away. An account that was not yet\nconfirmed is confirmed by this.\n",
        "security": [],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "Token from the password reset email.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "password",
                  "organization_id"
                ],
                "properties": {
                  "password": {
                    "type": "string"
                  },
                  "password_confirmation": {
                    "type": "string",
                    "description": "Defaults to `password` when omitted."
                  },
                  "organization_id": {
                    "description": "Your organization id, or `\"none\"` for an unscoped token.",
                    "type": [
                      "integer",
                      "string"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password reset and signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/api/v1/password/set/{token}": {
      "put": {
        "operationId": "setPassword",
        "tags": [
          "Accounts"
        ],
        "summary": "Set password from invite",
        "description": "Writes. Sets the first password for a user who was invited to a site, using the\ntoken from the invitation email, and returns a session token so the user is signed\nin straight away. An account that was not yet confirmed is confirmed by this.\n",
        "security": [],
        "servers": [
          {
            "url": "https://solar-assistant.io"
          }
        ],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "description": "Token from the invitation email.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "password",
                  "organization_id"
                ],
                "properties": {
                  "password": {
                    "type": "string"
                  },
                  "accepted_terms": {
                    "type": "boolean",
                    "description": "Whether the user accepted the terms of service."
                  },
                  "organization_id": {
                    "description": "Your organization id, or `\"none\"` for an unscoped token.",
                    "type": [
                      "integer",
                      "string"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password set and signed in",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Session"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          }
        }
      }
    },
    "/api/v1/metrics": {
      "get": {
        "operationId": "getMetrics",
        "tags": [
          "Device"
        ],
        "summary": "Metrics snapshot",
        "description": "Read-only. Returns the device's current metric values through the cloud proxy.\nThis is the same Solar Assistant device REST API you would call on the local\nnetwork; every path that works there works here.\n\nUse the `host`, `token`, and `site_key` from `POST /api/v1/sites/{id}/authorize`.\nSee https://solar-assistant.io/help/integration/rest-api for the full device API.\n",
        "servers": [
          {
            "url": "https://{proxy_host}",
            "variables": {
              "proxy_host": {
                "default": "us-htz-1.solar-assistant.io",
                "description": "Proxy hostname from the authorize response."
              }
            }
          }
        ],
        "security": [
          {
            "proxyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProxySiteId"
          },
          {
            "$ref": "#/components/parameters/ProxySiteKey"
          },
          {
            "name": "topic",
            "in": "query",
            "description": "Topic glob filter, for example `battery*` or `total/*`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "value",
            "in": "query",
            "description": "Set to 1 to return the value as plain text instead of JSON.",
            "schema": {
              "type": "integer",
              "enum": [
                1
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK. `text/plain` is returned instead of JSON when `value=1`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Metric"
                  }
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "operationId": "writeMetric",
        "tags": [
          "Device"
        ],
        "summary": "Write an inverter setting",
        "description": "Writes to the customer's inverter. This changes the physical configuration of\ntheir installation and takes effect as soon as the inverter accepts it; there is\nno confirmation step and no undo, and a wrong value can stop the installation\ncharging, discharging, or supplying the load. Treat it as an operation a person\nhas explicitly asked for, not one to try speculatively.\n\nOnly some topics are writable. Read the `topic` field of a `GET /api/v1/metrics`\nresponse to know which. A rejected write returns 422 with an error message.\n\nSee https://solar-assistant.io/help/integration/rest-api#adjusting-settings.\n",
        "servers": [
          {
            "url": "https://{proxy_host}",
            "variables": {
              "proxy_host": {
                "default": "us-htz-1.solar-assistant.io",
                "description": "Proxy hostname from the authorize response."
              }
            }
          }
        ],
        "security": [
          {
            "proxyAuth": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/ProxySiteId"
          },
          {
            "$ref": "#/components/parameters/ProxySiteKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "topic",
                  "value"
                ],
                "properties": {
                  "topic": {
                    "type": "string",
                    "description": "The metric topic to write, for example `inverter_1/output_source_priority`."
                  },
                  "value": {
                    "type": "string",
                    "description": "The new value to set."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The inverter accepted the new value.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "topic": {
                      "type": "string"
                    },
                    "result": {
                      "type": "string",
                      "const": "ok"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "description": "The topic is not writable, or the inverter rejected the value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/websocket": {
      "get": {
        "operationId": "streamMetrics",
        "tags": [
          "Device"
        ],
        "summary": "Stream metrics via WebSocket",
        "description": "Read-only. Streams live metrics using the Solar Assistant WebSocket API through\nthe cloud proxy. Connect to `wss://{host}/api/websocket` with the token as a query\nparameter and `Site-Id` and `Site-Key` as headers, then join the metrics channel\nusing the Phoenix Channel V2 protocol.\n\nSee https://solar-assistant.io/help/integration/websocket-api for the channel\nevents and topic format.\n",
        "servers": [
          {
            "url": "https://{proxy_host}",
            "variables": {
              "proxy_host": {
                "default": "us-htz-1.solar-assistant.io",
                "description": "Proxy hostname from the authorize response."
              }
            }
          }
        ],
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "The `token` from the authorize response.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "vsn",
            "in": "query",
            "description": "Phoenix Channel protocol version.",
            "schema": {
              "type": "string",
              "default": "2.0.0"
            }
          },
          {
            "$ref": "#/components/parameters/ProxySiteId"
          },
          {
            "$ref": "#/components/parameters/ProxySiteKey"
          }
        ],
        "responses": {
          "101": {
            "description": "Switching Protocols"
          }
        }
      }
    }
  }
}
