Build apps for Lutella homes
Apps use the gateway's client API v2, at home or away, with the same code. A member authorizes your app for one home and a set of scopes; your app gets a short-lived gateway token and calls the gateway on the local network or through the relay. An app never gets more than the member who authorized it, nor more than the scopes they consented to.
The flow
- Register. An Owner registers the app on My homes: a name, exact redirect URIs, and public (no secret: browser and mobile apps) or confidential (a secret, shown once). The app can then be authorized by members of that home.
- Authorize. Send the member's browser to the authorization endpoint with an S256 PKCE
challenge (required;
plainis refused). They sign in, pick the home and see what your app asks for. The code that comes back to your redirect URI is single-use and lives 1m0s. - Token. Exchange the code at the token endpoint. You get a gateway token
(
access_token, 10m0s) and arefresh_token. Itspermissionsare the consented scopes the member still holds right now. - Refresh. Each refresh returns a new gateway token and a new refresh token; the old one stops working. Presenting a used refresh token revokes the whole authorization.
- Revoke. Post the refresh token to the revocation endpoint (RFC 7009). The member can also disconnect your app from their account page, and an Owner from the home.
Discovery: https://lutella.cloud/.well-known/oauth-authorization-server. Gateway tokens are ES256 JWTs from
the issuer https://lutella.eecapital.co, signed with the keys at https://lutella.cloud/.well-known/jwks.json; their
azp claim is your client id.
Scopes
Ask for exactly what you need, space-separated. Each scope is one permission; there are no wildcards, and management permissions are never available to apps.
| Scope | Group | Lets the app |
|---|---|---|
read | See | See rooms, devices and their live state |
control.lights | Control | Turn lights on and off, dim them and change their colour |
control.shades | Control | Open, close and position shades and blinds |
control.climate | Control | Change air conditioning modes, fan speeds and temperatures |
control.media | Control | Change volume, sources and playback on music and TV players |
control.switches | Control | Turn on/off switches such as fans, towel rails and outlets |
control.scenes | Control | Recall scenes and hand rooms to their schedules |
doors | Doors | Open doors and gates, on site or remotely from the console where the release is tracked; app opens remotely need an Owner's per-door allowance |
doors.admin | Doors | Disable and enable door access in the key's rooms |
voice | Voice | Use the voice assistant |
calibrate | Setup | Survey indoor positioning with this device |
Reaching the gateway: LAN or relay
The token response names the gateway (gateway_id, property_id) and how to reach it:
- On the local network, try each of
lan_urls. Before sending the token, callGET {lan}/api/v1/gateway/identity-challenge?nonce=…and check the signature againstdevice_public_key; then call{lan}/api/v2/properties/{property_id}/…. - Away, call
{relay_url}/g/{gateway_id}/api/v2/properties/{property_id}/…. Doors open on site only.
The client API v2 contract is served by the gateway at …/api/v2/properties/{property_id}/openapi.json
(send the same bearer token).
Example: curl
VERIFIER=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
# 1. Open in the member's browser:
echo "https://lutella.cloud/oauth/authorize?response_type=code&client_id=app_example&redirect_uri=http%3A%2F%2F127.0.0.1%3A5310%2Fcb&scope=read%20control.lights&state=xyz&code_challenge=$CHALLENGE&code_challenge_method=S256"
# 2. Exchange the code from the redirect:
curl -s https://lutella.cloud/oauth/token -d grant_type=authorization_code -d client_id=app_example \
-d redirect_uri=http://127.0.0.1:5310/cb -d code="$CODE" -d code_verifier="$VERIFIER"
# 3. Read the home, then turn a light on (through the relay):
BASE=https://relay.lutella.eecapital.co/g/$GATEWAY_ID/api/v2/properties/$PROPERTY_ID
curl -s -H "Authorization: Bearer $ACCESS_TOKEN" $BASE/snapshot
curl -s -X POST -H "Authorization: Bearer $ACCESS_TOKEN" -H "Idempotency-Key: $(uuidgen)" \
-H 'Content-Type: application/json' -d '{"resource_id":"light:example-light","action":"turn_on"}' $BASE/commands
# 4. Later: refresh (rotates the refresh token), or revoke.
curl -s https://lutella.cloud/oauth/token -d grant_type=refresh_token -d client_id=app_example -d refresh_token="$REFRESH_TOKEN"
curl -s https://lutella.cloud/oauth/revoke -d client_id=app_example -d token="$REFRESH_TOKEN"
Example: JavaScript
const cloud = "https://lutella.cloud", clientId = "app_example", redirectUri = location.origin + "/cb";
const b64url = (b) => btoa(String.fromCharCode(...new Uint8Array(b))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
async function signIn() {
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const challenge = b64url(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
sessionStorage.verifier = verifier;
location = cloud + "/oauth/authorize?" + new URLSearchParams({ response_type: "code", client_id: clientId,
redirect_uri: redirectUri, scope: "read control.lights", code_challenge: challenge, code_challenge_method: "S256" });
}
async function onRedirect() {
const code = new URLSearchParams(location.search).get("code");
const t = await (await fetch(cloud + "/oauth/token", { method: "POST", body: new URLSearchParams({
grant_type: "authorization_code", client_id: clientId, redirect_uri: redirectUri, code, code_verifier: sessionStorage.verifier }) })).json();
const base = `${t.relay_url}/g/${t.gateway_id}/api/v2/properties/${t.property_id}`;
return (await fetch(base + "/snapshot", { headers: { Authorization: "Bearer " + t.access_token } })).json();
}
Browser apps calling the token endpoint directly need their origin allowed by the Lutella Cloud operator.