Webhooks
Capabilities
Capability |
Support |
Comment |
|---|---|---|
Server (receive HTTP(s) requests) |
OnSphere exposes inbound HTTP endpoints that external systems call. See configure webhook. |
|
Client (emit HTTP(s) requests) |
Webhooks only receive requests. To call an external URL from OnSphere, use a script with the HTTP API or the API service. |
|
HTTPS support |
Shared with the rest of the OnSphere stack — no additional TLS configuration needed. See configure webhook. |
|
Configurable HTTP methods |
|
|
Bearer token authentication |
See Bearer token. |
|
HMAC signature authentication |
HMAC-SHA256 only. See HMAC. |
|
Access rights enforcement |
Rights are enforced per verb from the API key’s associated user. See access rights. |
|
Writing values (POST / PUT) |
Push data from an external source into OnSphere values. See writing values. |
|
Reading values (GET) |
Read the current content of values exposed by the endpoint, or of any values by ID
via the |
|
Extracting a JSON field |
Map a specific field from the request body to a value using dot-path notation. See extraction rules. |
|
Extracting an XML field (XPath) |
Map a node from an XML request body to a value using an XPath expression. See extraction rules. |
|
Calling a script |
Delegate the request to a linked |
|
Return data to the caller |
On |
Examples
Concept
A Webhook lets an external system push data into OnSphere or trigger an action on it.
OnSphere intentionally goes beyond the classical webhook pattern — which only defines an inbound POST
call — by also supporting GET, PUT and DELETE, value read-back, and synchronous responses
built by a linked script. These extensions turn the endpoint into a small inbound HTTP / REST API,
making it easier for integrators to build richer integrations without requiring a separate API layer.
Note
This design choice enables deeper integration with external systems that are not strictly compliant with the classical webhook specification. The feature is still named webhook for historical reasons, even though its capabilities extend beyond the standard definition.
Configuration
Access
Webhooks listen for HTTP/HTTPS requests on configurable HTTP/HTTPS endpoints, on the same address
and port as the dashboard. The endpoint path is built from the common prefix /osp/webhooks and the
webhook name.
Example : https://stack.onsphere.ch/osp/webhooks/my-first-webhook
Note
HTTPS and certificate
HTTPS termination is handled by the same reverse proxy that serves the rest of the OnSphere stack (dashboard, API, …). Webhook endpoints therefore share the stack hostname, port, and TLS certificate — no separate certificate or TLS configuration is needed for webhooks. If the stack is reachable over HTTPS, webhook endpoints are automatically available over HTTPS on the same address.
HTTP methods
An endpoint declares the HTTP verbs it listens on through its methods list. Each entry specifies a
verb and an optional linkedAction to execute when that verb is called. A request using a verb
that is not declared is rejected with 405 Method Not Allowed.
{
"moduleId": "modules.web.web-1",
"path": "/call-script",
"methods": [
{
"verb": "POST",
"linkedAction": {
"actionId": "root.action",
"parameters": {
"scriptId": {
"type": "CONSTANT",
"value": "root.script"
},
"arguments": {
"type": "LIST",
"values": []
}
},
"expiration": {
"value": 5,
"unit": "SECONDS"
}
}
}
]
}
The verb drives the semantics:
Verb |
Values |
Linked action |
Typical use |
|---|---|---|---|
|
Written from the request body |
Executed after values are written |
Receive data from an external source |
|
Read and returned in the response when no script is linked |
Executed when declared; the script builds the response instead of reading values |
Read the current state of exposed values, or let a script fetch and return data. |
|
Reset values to their initialized state |
Executed |
Clear the endpoint state |
Warning
OnSphere does not enforce idempotency on GET. When a linkedAction is declared for GET,
the script is executed and builds the response; if it performs write operations, those writes will
run. The webhook layer has no knowledge of what the script does internally.
Access rights
Every request must carry an API key in the Authorization header. OnSphere resolves the user
associated with that API key and enforces the user’s standard access rights on the endpoint and,
when applicable, on the linked action.
The required access level depends on the verb:
Verb |
Right on the endpoint |
Right on the linked action |
|---|---|---|
|
WRITE |
WRITE |
|
READ |
READ |
|
WRITE |
WRITE |
Note
The user associated with the API key must be a member of the /internal/data-access group to
access internal values and to be allowed to hold an API key. This is configured in Keycloak
via the apiKey user attribute. See configure API key.
When the endpoint declares a linked action for the requested verb, the caller must hold the required right on both the endpoint and the action. This prevents a caller with only endpoint-level rights from triggering an action indirectly.
Configure API key
An API key must be associated with a Keycloak user via the apiKey attribute.
The user must hold sufficient rights on the endpoint (and its action, if any).
{
"users": [
{
"enabled": true,
"groups": [
"/internal/data-access"
],
"username": "example",
"email": "example@localhost",
"firstName": "example",
"lastName": "example",
"credentials": [
{
"initial": true,
"temporary": true,
"type": "password",
"value": "mysuperpassword"
}
],
"attributes": {
"authorizedKeys": [],
"apiKey": "simple-api-key"
}
}
]
}
Warning
When defining an API key for a user, it may take up to 1 minute for it to be applied.
Payload size limit
An endpoint accepts a request body up to maxPayloadSizeBytes (default 2 MB, i.e. 2097152
bytes). A larger request is rejected with 413 Payload Too Large. Set the field on the endpoint to
raise or lower the limit — for example, set "maxPayloadSizeBytes": 10485760 for a 10 MB limit.
Authentication
Authentication uses the API key configured in the previous section. OnSphere supports two methods to pass that key in the request.
Bearer token
Pass the key in the Authorization header:
POST /osp/webhooks/my-first-webhook HTTP/1.1
Host: stack.onsphere.ch
Accept: application/json
Authorization: Bearer simple-api-key
Content-Type: application/json
Example using curl:
curl -H "Content-Type: application/json" \
-H "Authorization: Bearer simple-api-key" \
-H "Accept: application/json" \
-d "false" \
https://stack.onsphere.ch/osp/webhooks/my-first-webhook
HMAC
Pass the HMAC-SHA256 signature of the request body in the X-Hub-Signature-256 header:
POST /osp/webhooks/my-first-webhook HTTP/1.1
Host: stack.onsphere.ch
Accept: application/json
X-Hub-Signature-256: sha256=3BSccvSzDGsEl1ip5Vd8cL7eUFFQfsiqz1SblIrQ9dg=
Content-Type: application/json
Compute the signature as follows:
signature = 'sha256=' + HMAC256(api-key, body).toBase64().toString('utf-8')
Writing values (POST / PUT)
POST and PUT requests write data into OnSphere values owned by a owner.webhook file. The request body is mapped to the values configured on the endpoint.
If the body is valid JSON or XML, a specific field can be extracted and mapped to a value using dot-path or array-index notation. If no extraction is configured, the entire body is used as the value content.
Extraction rules
Three extraction modes are available, configured per value via owner.webhook:
RAW — the entire request body is used as-is. No path needed.
JSON — a field is selected from the JSON body using dot-path and array-index notation:
Dot-path:
alert.information.windselects the nested fieldwind.Array index:
items[0]selects the first element,items[-1]the last. An empty index (items[]) also selects the first element.
XML — a node is selected from an XML body using an XPath expression:
/alert/temperatureselects the<temperature>element inside<alert>.//sensor[@id='temp1']/valueselects the<value>of the sensor with attributeid='temp1'.
The extracted text content is then coerced to the configured value type. The implementation rejects DOCTYPE declarations and external entities (XXE protection).
Supported value types:
Type |
Supported |
|---|---|
BOOLEAN |
|
TEXT |
|
NUMERIC |
|
DECIMAL |
Return code
The system returns 200 if every value was written successfully, and 500 if at least one value
could not be extracted (invalid JSON, missing field, or incompatible type). All values are attempted
even when one of them fails.
Reading values (GET)
A GET request returns one of three things, depending on how the request and the endpoint are
configured:
The endpoint values — all values linked to the endpoint via
owner.webhook(the default).Specific values by ID — any value the caller can read, selected with the
?valuesquery parameter.A script response — when the
GETverb declares alinkedAction, the script runs instead of reading values and builds the response itself (see calling a script).
Endpoint values (default)
Without the ?values parameter, OnSphere returns the current content of the values explicitly
linked to the endpoint. A value is linked by placing an owner.webhook file in its configuration
directory, referencing the endpoint:
{
"linkedEndpoint": "root.webhook.boolean-endpoint"
}
The value itself is configured separately:
{
"name": "Boolean example",
"description": "Example of boolean value from json",
"type": "BOOLEAN",
"retention": "STEADY"
}
The returned content reflects the last value written via a POST or PUT on this endpoint.
If no write has occurred since startup, content is null.
Specific values by ID (?values=)
Warning
This method is not designed to read large numbers of values at once. Use it for small, scoped reads only.
Pass a comma-separated list of OnSphere item identifiers to read any value regardless of its
owner.webhook configuration:
GET /osp/webhooks/my-endpoint?values=root.sensor.temperature,root.sensor.humidity HTTP/1.1
OnSphere fetches the current state of each requested value from its owning module. The caller must
hold READ access on the endpoint. No additional per-value access check is performed.
Note
Only STEADY values can be reliably read this way. FIRE_AND_FORGET values represent
transient events and are not retained — they will always return null.
Response format
The response is a JSON array. Each element represents one exposed value:
[
{ "itemId": "root.sensor.temperature", "content": 42, "type": "INTEGER" },
{ "itemId": "root.sensor.ratio", "content": 21.5, "type": "DECIMAL" },
{ "itemId": "root.sensor.active", "content": true, "type": "BOOLEAN" },
{ "itemId": "root.sensor.label", "content": "ok", "type": "TEXT" }
]
itemId: the full OnSphere item identifier of the value.content: the current value, typed according totype(INTEGER/DECIMAL→ number,BOOLEAN→ boolean,TEXT→ string).nullwhen the value has no current content.type: the OnSphere type of the value (INTEGER,DECIMAL,BOOLEAN, orTEXT).nullwhen the value is unknown (identifier not found or no data received yet).
A DELETE resets every exposed value to its initialized (unset) state, then executes the linked
action if one is configured.
Calling a script
Each verb of a webhook endpoint can reference a run-script action through the linkedAction
field of its methods entry. When that verb is called, OnSphere executes the script and returns the
HTTP response that the script builds.
A linked script is executed for every verb that declares one — POST, PUT, GET and
DELETE. For POST/PUT the values are written first, then the script runs; for DELETE the
values are reset, then the script runs; for GET the script runs instead of reading the values.
Linked action configuration
The action referenced by linkedAction must be of type RUN_SCRIPT and must declare the
osp-web module as issuer:
{
"moduleId": [
"modules.web.web-1"
],
"type": "RUN_SCRIPT"
}
The linkedAction field in the endpoint’s methods entry references the action and the script
it runs:
{
"moduleId": "modules.web.web-1",
"path": "/call-script",
"methods": [
{
"verb": "POST",
"linkedAction": {
"actionId": "root.action",
"parameters": {
"scriptId": {
"type": "CONSTANT",
"value": "root.script"
},
"arguments": {
"type": "LIST",
"values": []
}
},
"expiration": {
"value": 5,
"unit": "SECONDS"
}
}
}
]
}
Key fields of linkedAction:
actionId: theRUN_SCRIPTaction to execute.parameters.scriptId: the script run by the action.parameters.arguments: required by the action schema but not used by the webhook — pass an empty list. The webhook passes the HTTP request to the script automatically through thewebhookcontroller.expiration: the timeout after which the script execution is aborted.
The webhook script controller
When a script is triggered by a webhook, the incoming HTTP request is carried in the script parameters.
Parse them with webhook.parseArgs(trigger.parameters) to obtain a request object.
See webhook controller for the full reference.
Reading the request
Call |
Returns |
|---|---|
|
The HTTP method ( |
|
All request headers as a key/value object. |
|
A single header value, or an empty string when the header is absent. |
|
The raw request body ( |
|
The request body parsed as JSON. Returns an empty object when the body is absent; throws an error when the body is present but not valid JSON. |
|
The query parameters ( |
Building the response
request.respond(code)sets the HTTP statuscodewith no body.request.respond(code, content, contentType)sets the statuscode, the responsecontentand its mandatorycontentType.contentmust be a string and is sent as-is — the script produces the final body itself (e.g.JSON.stringify(...)for JSON).contentTypemust not be empty.
respond() does not stop the script: execution continues and the last call wins. The response is read
once the script ends, so use return after respond() if you want to short-circuit the rest of the script.
// Linked script executed by the webhook endpoint "root.webhook.script-endpoint".
//
// When a script is triggered by a webhook, the incoming HTTP request is carried by the script
// parameters. Parse them with "webhook.parseArgs(trigger.parameters)" to obtain a request
// object that exposes the request and the response builder:
// * request.method() the HTTP method (POST, GET, PUT or DELETE),
// * request.headers() all request headers as a key/value object,
// * request.header(name) a single header value,
// * request.body() the raw request body (POST / PUT),
// * request.json() the request body parsed as JSON,
// * request.query() the query parameters (GET / DELETE),
// * request.respond(code) sets an HTTP status code with no body,
// * request.respond(code, content, contentType) sets the HTTP response returned to the caller.
//
// respond() does not stop the script: the last call wins and the response is read once the script ends.
// The script is wrapped in a function so "return" can be used to short-circuit after responding.
const request = webhook.parseArgs(trigger.parameters);
(function () {
const contentType = request.header("Content-Type") || request.header("content-type") || "unknown";
let payload;
try {
payload = JSON.parse(request.body());
} catch (parseError) {
// The script controls the HTTP status code returned to the caller and produces the final body.
request.respond(400, JSON.stringify({ error: "The request body is not valid JSON." }), "application/json");
return;
}
const temperature = Number(payload.temperature);
if (!Number.isFinite(temperature)) {
request.respond(422, JSON.stringify({ error: "Field 'temperature' is required and must be a number." }), "application/json");
return;
}
const status = temperature > 30 ? "alert" : "ok";
log.info("Webhook script computed status [{}] for temperature [{}].", status, temperature);
request.respond(201, JSON.stringify({
status: status,
temperature: temperature,
receivedContentType: contentType
}), "application/json");
})();
Response code and content
code: the HTTP status code returned to the caller. Defaults to200when the script does not callrequest.respond().content: the response body, as a string, sent as-is. The script is responsible for producing the final body (e.g.JSON.stringify(...)for JSON). Omitted when the script responds with a code only.contentType: theContent-Typereturned to the caller (e.g."application/json","application/xml","text/plain"). Mandatory whenever a body is provided.
If the script execution itself fails (transport error, timeout, or a result flagged as failure), the
system returns 500.
When a webhook both sets values and calls a script, the values are processed first: if any value fails
to extract, the system returns 500 and the script is not executed.
See Calling a script from a webhook for a complete example.