Skip to main content

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.

note

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:

ReasonDescription
malformed_bodyRequest body was not valid JSON or exceeded the 256KB size cap
missing_credentialUsername or password missing on /api/token
invalid_credentialUsername/password rejected
no_sitesUser has no sites assigned
no_rolesUser has no roles assigned
2fa_requiredFirst leg of a 2FA login — code emailed
2fa_mismatchSubmitted 2FA code did not match
2fa_no_email2FA enabled but user has no email address on file
invalid_sessionToken missing, unknown, or expired
role_deniedUser's role does not allow the requested API function
invalid_actionUnknown /api/control action
not_implementedWrite 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.

  • 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