Authentication API Reference
This guide walks an external developer through the steps needed to obtain credentials and call a WoowTech instance on a user's behalf. The flow is built on the OAuth 2 framework together with the IndieAuth client-discovery extension, so no app pre-registration is required — your public URL is your registration.
Identifying Your Application
Because of IndieAuth, you do not request a client secret or sign up anywhere. Instead, the public address where your app lives doubles as its identifier. Two values matter:
- Client identifier — the canonical URL of your service, e.g.
https://app.brightpanel.example. - Callback (redirect) target — where WoowTech sends the user back to. It has to live on the exact same scheme, hostname, and port as the identifier, e.g.
https://app.brightpanel.example/woow/return.
Mobile and Desktop Callbacks
Native clients usually can't receive a browser redirect to an https:// page, so they rely on a custom URI scheme such as brightpanel://oauth. Since that scheme won't match your https client identifier, you have to advertise the allowed callback. Publish a discovery tag in the markup of your client-identifier page:
<link rel="redirect_uri" href="brightpanel://oauth">
When validating, WoowTech fetches your page and inspects only the leading 10 kilobytes for these redirect_uri link tags, so place them high in the document.
Step 1 — Send the User to Authorize
Open the user's browser at the /auth/authorize route of their server, passing your identifier and callback as URL-encoded query parameters:
https://demo.local:8123/auth/authorize?client_id=https%3A%2F%2Fapp.brightpanel.example&redirect_uri=https%3A%2F%2Fapp.brightpanel.example%2Fwoow%2Freturn
A common pattern is to attach a state value so you remember which instance the person was logging into. Round-tripping the base URL works well:
https://demo.local:8123/auth/authorize?client_id=https%3A%2F%2Fapp.brightpanel.example&redirect_uri=https%3A%2F%2Fapp.brightpanel.example%2Fwoow%2Freturn&state=https%3A%2F%2Fdemo.local%3A8123
Once the user signs in and grants access, WoowTech bounces them back to your callback with a one-time code (and the state you supplied, untouched):
https://app.brightpanel.example/woow/return?code=7f3a9b21&state=https%3A%2F%2Fdemo.local%3A8123
Step 2 — Trade the Code for Tokens
All token operations are POST requests sent to /auth/token with a application/x-www-form-urlencoded body.
Grant type: authorization_code
Convert the short-lived code into a token pair:
grant_type=authorization_code&code=7f3a9b21&client_id=https%3A%2F%2Fapp.brightpanel.example
A successful exchange yields JSON like this:
{
"access_token": "ya29.brightAccess01",
"expires_in": 1800,
"refresh_token": "rt.brightRefresh99",
"token_type": "Bearer"
}
The access_token lasts only 30 minutes (expires_in is in seconds). Hold onto the refresh_token — it's how you stay logged in afterward. A malformed body comes back as HTTP 400; a request for a user who has been disabled returns HTTP 403.
Grant type: refresh_token
When the access token ages out, mint a fresh one without bothering the user:
grant_type=refresh_token&refresh_token=rt.brightRefresh99&client_id=https%3A%2F%2Fapp.brightpanel.example
Note there is no new refresh token in the reply — keep reusing the original:
{
"access_token": "ya29.brightAccess02",
"expires_in": 1800,
"token_type": "Bearer"
}
Tearing Down a Session
To log the user out everywhere, revoke the refresh token, which also kills every access token derived from it. The client_id field is unnecessary here:
token=rt.brightRefresh99&action=revoke
The endpoint always answers HTTP 200 with no payload, whether or not the token actually existed.
Long-Lived Access Tokens
For unattended integrations — background daemons, webhook receivers, scripts — there is a token variety that stays valid for roughly a decade. A user can mint one from the bottom of their WoowTech profile screen, or a frontend can request one over the WebSocket connection:
{
"id": 27,
"type": "auth/long_lived_access_token",
"client_name": "Greenhouse Telemetry",
"client_icon": null,
"lifespan": 730
}
The server replies with the raw token string in result (you only see it once):
{
"id": 27,
"type": "result",
"success": true,
"result": "llat.greenhouse.deadbeef"
}
Calling the REST API With a Token
Attach whichever token you hold as a bearer credential in the Authorization header:
Authorization: Bearer ya29.brightAccess01
Shell (cURL)
curl -X GET \
'https://home.brightpanel.example/api/config' \
-H 'Authorization: Bearer ya29.brightAccess01'
Python (requests)
import requests
endpoint = "https://home.brightpanel.example/api/config"
auth = {"Authorization": "Bearer ya29.brightAccess01"}
reply = requests.get(endpoint, headers=auth)
print(reply.json())
Browser / Node (fetch)
const token = "ya29.brightAccess01";
fetch("https://home.brightpanel.example/api/config", {
headers: { Authorization: `Bearer ${token}` },
})
.then((res) => {
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
})
.then((data) => console.log(data))
.catch((err) => console.error(err));
If a call comes back as HTTP 401, the access token has lapsed. Run the refresh flow; should that fail too, walk the user back through the authorization step.
Signed Paths
Sometimes you need a plain GET URL — say, an <img> source or a file a browser downloads directly — that carries its own authentication and therefore needs no header. WoowTech can hand you a temporary, signature-bearing URL for exactly this. By default the signature is good for 30 seconds.
From an integration
Inside Python integration code, pull in the helper:
from homeassistant.components.http.auth import async_sign_path
From the frontend
Over the WebSocket API, ask for a signed version of a path:
{
"type": "auth/sign_path",
"path": "/api/camera_proxy/camera.front_door",
"expires": 45
}
The response appends an authSig query argument:
{
"path": "/api/camera_proxy/camera.front_door?authSig=signed.token.value"
}
Things to know about signed URLs
- The signature dies if the originating refresh token is removed.
- Deleting the owning user also voids it.
- A WoowTech restart invalidates outstanding signatures.
- Validity is checked the moment the request arrives, so an already-streaming long download keeps going even after the expiry passes.
Authentication API Reference
This guide walks an external developer through the steps needed to obtain credentials and call a WoowTech instance on a user's behalf. The flow is built on the OAuth 2 framework together with the IndieAuth client-discovery extension, so no app pre-registration is required — your public URL is your registration.
Identifying Your Application
Because of IndieAuth, you do not request a client secret or sign up anywhere. Instead, the public address where your app lives doubles as its identifier. Two values matter:
- Client identifier — the canonical URL of your service, e.g.
https://app.brightpanel.example. - Callback (redirect) target — where WoowTech sends the user back to. It has to live on the exact same scheme, hostname, and port as the identifier, e.g.
https://app.brightpanel.example/woow/return.
Mobile and Desktop Callbacks
Native clients usually can't receive a browser redirect to an https:// page, so they rely on a custom URI scheme such as brightpanel://oauth. Since that scheme won't match your https client identifier, you have to advertise the allowed callback. Publish a discovery tag in the markup of your client-identifier page:
<link rel="redirect_uri" href="brightpanel://oauth">
When validating, WoowTech fetches your page and inspects only the leading 10 kilobytes for these redirect_uri link tags, so place them high in the document.
Step 1 — Send the User to Authorize
Open the user's browser at the /auth/authorize route of their server, passing your identifier and callback as URL-encoded query parameters:
https://demo.local:8123/auth/authorize?client_id=https%3A%2F%2Fapp.brightpanel.example&redirect_uri=https%3A%2F%2Fapp.brightpanel.example%2Fwoow%2Freturn
A common pattern is to attach a state value so you remember which instance the person was logging into. Round-tripping the base URL works well:
https://demo.local:8123/auth/authorize?client_id=https%3A%2F%2Fapp.brightpanel.example&redirect_uri=https%3A%2F%2Fapp.brightpanel.example%2Fwoow%2Freturn&state=https%3A%2F%2Fdemo.local%3A8123
Once the user signs in and grants access, WoowTech bounces them back to your callback with a one-time code (and the state you supplied, untouched):
https://app.brightpanel.example/woow/return?code=7f3a9b21&state=https%3A%2F%2Fdemo.local%3A8123
Step 2 — Trade the Code for Tokens
All token operations are POST requests sent to /auth/token with a application/x-www-form-urlencoded body.
Grant type: authorization_code
Convert the short-lived code into a token pair:
grant_type=authorization_code&code=7f3a9b21&client_id=https%3A%2F%2Fapp.brightpanel.example
A successful exchange yields JSON like this:
{
"access_token": "ya29.brightAccess01",
"expires_in": 1800,
"refresh_token": "rt.brightRefresh99",
"token_type": "Bearer"
}
The access_token lasts only 30 minutes (expires_in is in seconds). Hold onto the refresh_token — it's how you stay logged in afterward. A malformed body comes back as HTTP 400; a request for a user who has been disabled returns HTTP 403.
Grant type: refresh_token
When the access token ages out, mint a fresh one without bothering the user:
grant_type=refresh_token&refresh_token=rt.brightRefresh99&client_id=https%3A%2F%2Fapp.brightpanel.example
Note there is no new refresh token in the reply — keep reusing the original:
{
"access_token": "ya29.brightAccess02",
"expires_in": 1800,
"token_type": "Bearer"
}
Tearing Down a Session
To log the user out everywhere, revoke the refresh token, which also kills every access token derived from it. The client_id field is unnecessary here:
token=rt.brightRefresh99&action=revoke
The endpoint always answers HTTP 200 with no payload, whether or not the token actually existed.
Long-Lived Access Tokens
For unattended integrations — background daemons, webhook receivers, scripts — there is a token variety that stays valid for roughly a decade. A user can mint one from the bottom of their WoowTech profile screen, or a frontend can request one over the WebSocket connection:
{
"id": 27,
"type": "auth/long_lived_access_token",
"client_name": "Greenhouse Telemetry",
"client_icon": null,
"lifespan": 730
}
The server replies with the raw token string in result (you only see it once):
{
"id": 27,
"type": "result",
"success": true,
"result": "llat.greenhouse.deadbeef"
}
Calling the REST API With a Token
Attach whichever token you hold as a bearer credential in the Authorization header:
Authorization: Bearer ya29.brightAccess01
Shell (cURL)
curl -X GET \
'https://home.brightpanel.example/api/config' \
-H 'Authorization: Bearer ya29.brightAccess01'
Python (requests)
import requests
endpoint = "https://home.brightpanel.example/api/config"
auth = {"Authorization": "Bearer ya29.brightAccess01"}
reply = requests.get(endpoint, headers=auth)
print(reply.json())
Browser / Node (fetch)
const token = "ya29.brightAccess01";
fetch("https://home.brightpanel.example/api/config", {
headers: { Authorization: `Bearer ${token}` },
})
.then((res) => {
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
})
.then((data) => console.log(data))
.catch((err) => console.error(err));
If a call comes back as HTTP 401, the access token has lapsed. Run the refresh flow; should that fail too, walk the user back through the authorization step.
Signed Paths
Sometimes you need a plain GET URL — say, an <img> source or a file a browser downloads directly — that carries its own authentication and therefore needs no header. WoowTech can hand you a temporary, signature-bearing URL for exactly this. By default the signature is good for 30 seconds.
From an integration
Inside Python integration code, pull in the helper:
from homeassistant.components.http.auth import async_sign_path
From the frontend
Over the WebSocket API, ask for a signed version of a path:
{
"type": "auth/sign_path",
"path": "/api/camera_proxy/camera.front_door",
"expires": 45
}
The response appends an authSig query argument:
{
"path": "/api/camera_proxy/camera.front_door?authSig=signed.token.value"
}
Things to know about signed URLs
- The signature dies if the originating refresh token is removed.
- Deleting the owning user also voids it.
- A WoowTech restart invalidates outstanding signatures.
- Validity is checked the moment the request arrives, so an already-streaming long download keeps going even after the expiry passes.
Start writing here...