REST API
GEM exposes a built-in REST API for external systems that need to authenticate, read data, and dispatch commands or macros. The API is enabled by default ("Enable REST API" during Installation) and gated by the same role-based access control as the socket API.
For custom HTTP endpoints with arbitrary handlers, see Web Services — those are user-defined scripts; the REST API documented here is the fixed contract.
Base URL
All endpoints are mounted under /api/ on the controller (e.g., https://gem.example.com/api/token).
Authentication
POST /api/token
Exchanges a username + password for a session token.
Request body (JSON):
{
"username": "user@example.com",
"password": "secret",
"two_factor_token": "AB23KP"
}
two_factor_token is only required when the user has 2FA enabled and a code has already been emailed (see below).
Success response:
{
"token": "…32-character token…",
"expires": 3600000,
"sites": [{"id": 1, "name": "main"}]
}
2FA challenge response — returned on the first call for a 2FA-enabled user:
{
"error": "two_factor_auth",
"message": "Please enter the 2FA code",
"ttl_seconds": 300
}
A 6-character alphanumeric code is emailed to the user (uppercase A–Z + 2–9, excluding visually ambiguous characters like 0/O and 1/I/L). Resubmit /api/token with the same credentials plus two_factor_token to complete login. The compare is case-insensitive and the code is single-use (cleared on successful login).
ttl_seconds is the recommended display TTL — the actual session lifetime is governed by cfg.api_token_life (default 1 hour). Configure with cfg.api_2fa_ttl_seconds if you need a different value.
Error envelope:
{"error": "unauthorized", "message": "invalid credential"}
Possible message values: missing credential, invalid credential, no sites found, invalid roles, 2FA is enabled but user has no email address, two factor fail, malformed body.
For backwards compatibility with older clients, authentication failures currently return HTTP 200 with an {error, message} envelope rather than 4xx status codes. Newer endpoints (/api/control, /api/data) do use proper status codes.
Using the token
Pass the token on subsequent requests using either header form:
gem-api-token: <token>
Authorization: Bearer <token>
POST /api/logout
Revokes the current session token. Idempotent — returns {success: true} whether or not the token matched.
GET /api/me
Returns the current user identity for the supplied token:
{
"id": 42,
"username": "nathan",
"roles": ["admin"],
"sites": [{"id": 1}],
"two_factor_auth": true,
"issued": 1715000000000
}
Control Endpoints
POST /api/control/command
Dispatches a zone command. Body matches the standard command shape:
{"device": 5, "zone": 11, "action": "on", "level": 100}
Requires the command API permission on the user's role (see Roles).
POST /api/control/macro
Runs a macro by id or name:
{"macro_id": 12}
Requires the macro API permission.
Non-POST methods return 405 Method Not Allowed with an Allow: POST header.
Data Endpoints
GET /api/data/:entity
Queries an entity using URL query parameters as the where clause. Numeric query strings are automatically coerced to numbers.
GET /api/data/zone?subsystem_id=11
GET /api/data/device?id=42
Requires the query API permission.
POST / PATCH / DELETE on /api/data/:entity currently return 501 Not Implemented — use the socket API for writes.
Public Endpoints
These do not require authentication.
GET /api/health
Liveness probe. Always returns {"ok": true}.
GET /api/version
Returns the controller version string:
{"version": "2.0.4521"}
RBAC
The REST API enforces role-based access control on command, macro, and query using the same api rule list as the socket API. Configure under Roles:
allow api command, macro, query
deny api delete_macro
A * wildcard in allow grants every API function unless explicitly denied. Tokens issued from /api/token resolve through Auth.users so role/site memberships and rule maps are up to date at the moment of the call.
Auditing
Every REST API call is recorded to Request History with request_type: rest_api. The row captures the method, path, HTTP status, elapsed time, client IP, user, and grant/deny reason. Use this for security review and troubleshooting permission issues.
Denial reasons include:
| Reason | Description |
|---|---|
malformed_body | Request body was not valid JSON or exceeded the 256KB size cap |
missing_credential | Username or password missing on /api/token |
invalid_credential | Username/password rejected |
no_sites | User has no sites assigned |
no_roles | User has no roles assigned |
2fa_required | First leg of a 2FA login — code emailed |
2fa_mismatch | Submitted 2FA code did not match |
2fa_no_email | 2FA enabled but user has no email address on file |
invalid_session | Token missing, unknown, or expired |
role_denied | User's role does not allow the requested API function |
invalid_action | Unknown /api/control action |
not_implemented | Write method against /api/data |
Limits
- Body size: requests larger than 256KB are rejected with
malformed_body. - Token length: 32-character cryptographically random string.
- Token lifetime: governed by
cfg.api_token_life(milliseconds, default 1 hour). Tokens are evicted lazily on the next call after expiry. - Sessions: held in memory. Restarting GEM invalidates all tokens; clients must re-authenticate.
Disabling the API
The REST API can be disabled globally by clearing rest_api in gem.json (or unchecking "Enable REST API" during Installation). When disabled, every /api/* endpoint except /api/sync returns {"error": "rest api disabled"} with HTTP 401.
/api/sync is the peer-sync transport between paired controllers and is governed separately.
Related Documentation
- Roles — API permissions and rule syntax
- Request History — Audit log for REST API calls
- Web Services — Custom HTTP endpoints with user-defined handlers
- Installation — Enabling/disabling the REST API at install time