Appearance
Authentication
Homecast supports three authentication models. Choose based on your use case.
| Model | Format | Use case | Lifetime |
|---|---|---|---|
| JWT | eyJ... | Web and mobile app sessions | 7 days |
| Access Token | hc_... | Programmatic API scripts | Until revoked or expired |
| OAuth 2.1 | Bearer token | Third-party integrations, MCP | 1 hour (refreshable) |
All three are sent the same way:
http
Authorization: Bearer <token>JWT sessions
First-party sessions for the web and mobile apps. Obtained via login or signup GraphQL mutations.
Claims
| Claim | Type | Description |
|---|---|---|
sub | string | User ID (UUID) |
email | string | User email address |
iat | datetime | Issued at |
exp | datetime | Expiration (issued + 7 days) |
home_permissions | object | null | Home access scope (only for OAuth-issued tokens) |
Configuration
| Setting | Value |
|---|---|
| Algorithm | HS256 |
| Expiry | 168 hours (7 days) |
Login
graphql
mutation {
login(email: "user@example.com", password: "password") {
success
token
user {
id
email
name
}
error
}
}Response:
json
{
"data": {
"login": {
"success": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": { "id": "uuid", "email": "user@example.com", "name": "Alice" },
"error": null
}
}
}Signup
graphql
mutation {
signup(email: "user@example.com", password: "password", name: "Alice") {
success
token
user { id email name }
error
}
}Access tokens
Programmatic tokens for scripts and automations. Prefixed with hc_.
Create a token
graphql
mutation {
createAccessToken(
name: "My Script"
homePermissions: "{\"home-uuid\": \"control\"}"
) {
success
token
accessToken {
id
name
tokenPrefix
}
error
}
}The full token value (starting with hc_) is only returned on creation. Store it immediately.
Home permissions
The homePermissions field is a JSON string mapping home UUIDs to access levels:
json
{
"550e8400-e29b-41d4-a716-446655440000": "control",
"6ba7b810-9dad-11d1-80b4-00c04fd430c8": "view"
}| Level | Can read state | Can control devices |
|---|---|---|
control | Yes | Yes |
view | Yes | No |
If homePermissions is null or omitted, the token has full access to all homes.
Token properties
| Property | Description |
|---|---|
id | Token UUID |
name | User-defined label |
tokenPrefix | First characters of token (for identification) |
lastUsedAt | Last time the token was used |
expiresAt | Expiration date (null = never expires) |
revokedAt | Revocation date (null = active) |
createdAt | Creation timestamp |
List and revoke
graphql
# List all tokens
{ accessTokens { id name tokenPrefix lastUsedAt expiresAt createdAt } }
# Revoke a token
mutation { revokeAccessToken(tokenId: "token-uuid") { success error } }OAuth 2.1
Full OAuth 2.1 with PKCE for third-party integrations and MCP clients.
Discovery
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-authorization-server | Server metadata (RFC 8414) |
GET /.well-known/openid-configuration | OpenID Connect Discovery |
GET /.well-known/oauth-protected-resource | Resource metadata (RFC 8707) |
Server metadata response:
json
{
"issuer": "https://api.homecast.cloud",
"authorization_endpoint": "https://api.homecast.cloud/oauth/authorize",
"token_endpoint": "https://api.homecast.cloud/oauth/token",
"registration_endpoint": "https://api.homecast.cloud/oauth/register",
"scopes_supported": ["mcp:read", "mcp:write", "mcp:admin"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none", "client_secret_post"]
}Scopes
| Scope | Description |
|---|---|
mcp:read | Read device state |
mcp:write | Read and control devices |
mcp:admin | Full access including management |
Flow
1. Dynamic client registration
http
POST /oauth/register
Content-Type: application/json
{
"redirect_uris": ["https://your-app.com/callback"],
"client_name": "My Integration",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}Response:
json
{
"client_id": "generated-uuid",
"client_secret": "generated-secret",
"client_id_issued_at": 1708099200,
"client_secret_expires_at": 0,
"redirect_uris": ["https://your-app.com/callback"],
"client_name": "My Integration",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}2. Authorization request (with PKCE)
GET /oauth/authorize?
client_id={client_id}&
redirect_uri=https://your-app.com/callback&
response_type=code&
code_challenge={challenge}&
code_challenge_method=S256&
scope=mcp:write&
state={random_state}The user logs in, selects which homes to grant access to, and approves. Homecast redirects back:
https://your-app.com/callback?code={auth_code}&iss=https://api.homecast.cloud&state={random_state}3. Token exchange
http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code={auth_code}&
redirect_uri=https://your-app.com/callback&
code_verifier={verifier}Response:
json
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "refresh_token_value",
"scope": "mcp:write"
}4. Refresh
http
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&
refresh_token={refresh_token}Returns a new access token and a rotated refresh token.
Token lifetimes
| Token | Lifetime |
|---|---|
| Authorization code | 10 minutes |
| Access token | 1 hour |
| Refresh token | 30 days |
Refresh tokens use rotation — each refresh returns a new refresh token and invalidates the old one.
Managing authorized apps
graphql
# List apps with OAuth access
{ authorizedApps { clientId clientName scope createdAt } }
# Revoke an app's access
mutation { revokeAuthorizedApp(clientId: "client-uuid") { success error } }Error responses
| Status | Body | Meaning |
|---|---|---|
| 401 | {"error": "invalid_token"} | Token is expired, revoked, or malformed |
| 403 | {"error": "insufficient_permissions"} | Token doesn't have access to the requested home or action |
| 401 | {"error": "invalid_client"} | OAuth client credentials are invalid |
| 400 | {"error": "invalid_grant"} | Authorization code or refresh token is invalid/expired |