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).
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.
| Button | Description |
|---|---|
| New | Clears the form to create a new web service. |
| Clone | Duplicates the current service for editing under a new name. |
| Export | Downloads the service configuration as a JSON file. |
| Import | Loads a service from a previously exported JSON file. |
| Delete | Deletes 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:
| Template | Description |
|---|---|
| JSON API | REST endpoint returning JSON, with GET/POST routing. |
| Webhook Receiver | Accepts incoming webhooks and processes them. |
| HTML Page | Serves a custom HTML page with dynamic data. |
| Proxy | Forwards requests to an external API. |
| Form Handler | Serves an HTML form and processes submissions. |
| CSV Export | Exports 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
| Field | Description |
|---|---|
| Name | Required. Internal identifier (auto-formatted to lowercase_with_underscores). Becomes the URL path: /web_service/{name}. |
| Method | HTTP method: ANY (all methods), GET, POST, PUT, DELETE, PATCH, or HEAD. Use ANY to handle multiple methods in a single handler. |
| Content Type | Response content type with common suggestions: application/json, text/html, text/plain, text/csv, etc. |
| Description | Optional 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.
| Field | Description |
|---|---|
| Auth Type | None, Basic Auth, API Key, or Bearer Token. New services default to API Key. |
| Enabled | Toggle to enable or disable the service. Disabling takes the endpoint off the air immediately. |
Basic Auth fields:
- Username and Password — checked against the
Authorization: Basicheader.
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: Bearerheader.
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.
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 returning401after a downgrade, re-pick its auth type on this page. - Auth types are matched case-insensitively, so a row stored as
BasicorAPI_KEYis 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.
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)
| Property | Description |
|---|---|
req.method | HTTP method string (GET, POST, etc.). |
req.params.path | Sub-path after the service name (e.g., /users/42). |
req.params.segments | Path segments as an array (e.g., ['users', '42']). |
req.body | Parsed request body (JSON object if parseable, otherwise raw string). |
req.query | URL query parameters as key/value pairs. |
req.headers | HTTP request headers (lowercased keys). |
Response Object (res)
| Method | Description |
|---|---|
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
| Variable | Description |
|---|---|
gem | GemServer instance — full access to queryAsync, commandAsync, setAttributeAsync, etc. |
store | Per-service in-memory key-value store (persists across requests, resets on service reload). |
fetch | Global fetch for making outbound HTTP requests. |
Buffer, URL, URLSearchParams | Standard Node.js utilities. |
setTimeout, clearTimeout | Timer functions. |
JSON, Math, Date, etc. | Standard JavaScript globals. |
Action Buttons
| Button | Description |
|---|---|
| Reload | Reloads the service handler to apply the latest saved script. |
| Create / Update Service | Saves 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
| Field | Description |
|---|---|
| Path Suffix | Optional sub-path appended to the service URL (e.g., /devices/42). |
| Method Override | Override the service's configured method for this test request. |
| Headers | Add custom request headers as key-value pairs. |
| Query Parameters | Add URL query parameters as key-value pairs. |
| Request Body | Request 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
Related Documentation
- Macros — Triggering macros from webhooks
- Attributes — Updating attributes from webhooks
- Triggers — Webhook-triggered automation
- MQTT — Alternative integration via MQTT