Skip to main content

Web Services

The Web Services page lets you create custom HTTP endpoints that external systems can call to interact with GEM. Each web service is a JavaScript handler function that processes incoming HTTP requests and returns a response. Services are accessible at /web_service/{name} and support sub-paths (e.g., /web_service/{name}/users/42).

Licensing

Web Services are part of the Web Services module. Without it in your license, this page is locked and inbound /web_service/* requests are rejected with HTTP 402 (Payment Required). Trials, development builds, and grandfathered licenses keep existing integrations running. See License.

Service Selection

At the top of the page is a Web Service dropdown listing all existing services.

ButtonDescription
NewClears the form to create a new web service.
CloneDuplicates the current service for editing under a new name.
ExportDownloads the service configuration as a JSON file.
ImportLoads a service from a previously exported JSON file.
DeleteDeletes the currently selected service (with confirmation).

Tabs

The page has three tabs: Configure, Test, and Request Log.


Configure Tab

Templates

When creating a new service, a row of template chips appears to pre-fill the script and settings:

TemplateDescription
JSON APIREST endpoint returning JSON, with GET/POST routing.
Webhook ReceiverAccepts incoming webhooks and processes them.
HTML PageServes a custom HTML page with dynamic data.
ProxyForwards requests to an external API.
Form HandlerServes an HTML form and processes submissions.
CSV ExportExports GEM data as a downloadable CSV file.

Click a template to load it. The template pre-fills the method, content type, description, and script.

Configuration

FieldDescription
NameRequired. Internal identifier (auto-formatted to lowercase_with_underscores). Becomes the URL path: /web_service/{name}.
MethodHTTP method: ANY (all methods), GET, POST, PUT, DELETE, PATCH, or HEAD. Use ANY to handle multiple methods in a single handler.
Content TypeResponse content type with common suggestions: application/json, text/html, text/plain, text/csv, etc.
DescriptionOptional description of what this service does.

Authentication

/web_service/* is served without a login session — unlike the admin interface, there is no signed-in user behind the request. The auth type you pick here is the only thing standing between the network and a script that holds gem, the live server instance. Treat it accordingly.

FieldDescription
Auth TypeNone, Basic Auth, API Key, or Bearer Token. New services default to API Key.
EnabledToggle to enable or disable the service. Disabling takes the endpoint off the air immediately.

Basic Auth fields:

  • Username and Password — checked against the Authorization: Basic header.

API Key fields:

  • Header Name — the header to check (default: X-API-Key).
  • API Key — the expected key value.

Bearer Token fields:

  • Bearer Token — checked against the Authorization: Bearer header.
Choosing "None" makes the endpoint public

Selecting None and saving is an explicit grant of anonymous access: anyone who can reach this controller on the network can call the endpoint and run its script, with no credentials, from any device. The form shows a warning while None is selected.

Anonymous access is recorded as a per-service decision, not inferred from a blank field. A service that arrives without one — from an import, a .gemapp application, or a direct database insert — is not public; it is refused until an administrator opens it here and chooses None deliberately.

A credentialed type with a blank secret is refused, not open

Picking Basic Auth, API Key or Bearer Token and saving without filling in the secret used to leave the endpoint answering anyone who sent a matching empty credential — the list showed the service as authenticated while it was not. Those rows now return 401 on every request, and the editor refuses to save them, so the missing field is reported when you save instead of discovered later.

If a service that used to answer starts returning 401 after an upgrade, open it here and check that the field its auth type needs actually holds a value. Note that the Username, API Key and Bearer Token fields are cleared whenever you switch to a different auth type, so flipping a service from Basic to Bearer and saving before typing the token is the usual way to produce one.

Two rules govern the rest of the ladder, and both fail in the safe direction:

  • An unrecognised auth type is refused with 401. A value outside the four above — a typo, or a type written by a newer build's admin UI onto a controller still running older code — stops the service answering rather than opening it. If a service that used to work starts returning 401 after a downgrade, re-pick its auth type on this page.
  • Auth types are matched case-insensitively, so a row stored as Basic or API_KEY is enforced as Basic and API Key rather than falling through.

Services predating the Auth Type field, which carry only a Username and Password, continue to be enforced as Basic Auth.

Upgrading a site with existing anonymous services

Services that were already answering anonymously keep working across the upgrade — they are grandfathered automatically, and each one is named in the update log so you have the list. Review them here: every entry on that list is an endpoint on this controller that answers without credentials. Switch each to an authenticated type unless the integration calling it genuinely cannot present one.

Taking a service off the air

Delete, the Enabled toggle, and Reload all take effect on the running controller immediately — no restart required. A deleted or disabled service stops answering, and a renamed service stops answering under its old name.

Request Handler Script

A full-featured script editor for writing the JavaScript handler. The script must define a handleRequest function.

Request Object (req)

PropertyDescription
req.methodHTTP method string (GET, POST, etc.).
req.params.pathSub-path after the service name (e.g., /users/42).
req.params.segmentsPath segments as an array (e.g., ['users', '42']).
req.bodyParsed request body (JSON object if parseable, otherwise raw string).
req.queryURL query parameters as key/value pairs.
req.headersHTTP request headers (lowercased keys).

Response Object (res)

MethodDescription
res.json(data)Send a JSON response.
res.send(text)Send a text/HTML response.
res.status(code)Set the HTTP status code (chainable).
res.setHeader(name, value)Set a response header.

Available Globals

VariableDescription
gemGemServer instance — full access to queryAsync, commandAsync, setAttributeAsync, etc.
storePer-service in-memory key-value store (persists across requests, resets on service reload).
fetchGlobal fetch for making outbound HTTP requests.
Buffer, URL, URLSearchParamsStandard Node.js utilities.
setTimeout, clearTimeoutTimer functions.
JSON, Math, Date, etc.Standard JavaScript globals.

Action Buttons

ButtonDescription
ReloadReloads the service handler to apply the latest saved script.
Create / Update ServiceSaves the service configuration.

Test Tab

The Test tab lets you send requests to the service directly from the admin UI without external tools.

Request Configuration

FieldDescription
Path SuffixOptional sub-path appended to the service URL (e.g., /devices/42).
Method OverrideOverride the service's configured method for this test request.
HeadersAdd custom request headers as key-value pairs.
Query ParametersAdd URL query parameters as key-value pairs.
Request BodyRequest body for POST/PUT/PATCH/DELETE requests (shown only for those methods).

A URL preview shows the complete request URL with method badge.

Click Send Request to execute the test. The service is automatically reloaded before each test to ensure the latest script is used.

Response Display

The response section shows:

  • Status code and status text (color-coded green for 2xx, red for 4xx/5xx)
  • Elapsed time in milliseconds
  • Content type
  • Response headers in a collapsible table
  • Response body with syntax highlighting

Request Log Tab

The Request Log tab shows a real-time stream of incoming requests to the selected service.

  • Click Start to begin capturing requests.
  • Click Stop to pause capture.
  • Click Clear to empty the log.

Each entry shows:

  • Time — Timestamp with millisecond precision.
  • Method — HTTP method (GET, POST, etc.).
  • Path — The sub-path of the request.
  • Status — Response status code (color-coded).
  • Elapsed — Response time in milliseconds.

The log retains up to 500 entries. Requests are also persisted to the Request History database for later review.


Example: JSON API with Sub-Path Routing

async function handleRequest(req, res) {
let [resource, id] = req.params.segments;

if (resource === 'zones') {
if (req.method === 'GET' && id) {
let zone = await gem.queryOne('zone', {id: parseInt(id)});
return res.json(zone || {error: 'not found'});
}
let zones = await gem.query('zone', {});
return res.json({zones});
}

res.status(404).json({error: 'not found'});
}

Usage:

GET /web_service/api/zones → list all zones
GET /web_service/api/zones/42 → get zone 42
  • Macros — Triggering macros from webhooks
  • Attributes — Updating attributes from webhooks
  • Triggers — Webhook-triggered automation
  • MQTT — Alternative integration via MQTT