Webhooks

Capabilities

Capability

Support

Comment

Server (receive HTTP(s) requests)

Supported feature

OnSphere exposes inbound HTTP endpoints that external systems call. See configure webhook.

Client (emit HTTP(s) requests)

Partial support

Webhooks only receive requests. To call an external URL from OnSphere, use a script with the HTTP API or the API service.

HTTPS support

Supported feature

Shared with the rest of the OnSphere stack — no additional TLS configuration needed. See configure webhook.

Configurable HTTP methods

Supported feature

POST, GET, PUT and DELETE, declared per endpoint. See HTTP methods.

Bearer token authentication

Supported feature

See Bearer token.

HMAC signature authentication

Supported feature

HMAC-SHA256 only. See HMAC.

Access rights enforcement

Supported feature

Rights are enforced per verb from the API key’s associated user. See access rights.

Writing values (POST / PUT)

Supported feature

Push data from an external source into OnSphere values. See writing values.

Reading values (GET)

Supported feature

Read the current content of values exposed by the endpoint, or of any values by ID via the ?values query parameter. See reading values. This read method is not designed for read large amount of values use it carefully

Extracting a JSON field

Supported feature

Map a specific field from the request body to a value using dot-path notation. See extraction rules.

Extracting an XML field (XPath)

Supported feature

Map a node from an XML request body to a value using an XPath expression. See extraction rules.

Calling a script

Supported feature

Delegate the request to a linked run-script action and return the script response. See calling a script.

Return data to the caller

Supported feature

On GET without a linked script, returns the current endpoint values. On any verb with a linked script, returns the response built by the script (JSON, plain text, XML, or any Content-Type).

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.

root/webhook/script-endpoint/endpoint.webhook
{
    "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

POST / PUT

Written from the request body

Executed after values are written

Receive data from an external source

GET

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.

DELETE

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:

Required rights per verb

Verb

Right on the endpoint

Right on the linked action

POST / PUT

WRITE

WRITE

GET

READ

READ

DELETE

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).

modules/keycloak/keycloak-1/users.keycloak
{
  "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.wind selects the nested field wind.

  • 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/temperature selects the <temperature> element inside <alert>.

  • //sensor[@id='temp1']/value selects the <value> of the sensor with attribute id='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

Supported feature

TEXT

Supported feature

NUMERIC

Supported feature

DECIMAL

Supported feature

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:

  1. The endpoint values — all values linked to the endpoint via owner.webhook (the default).

  2. Specific values by ID — any value the caller can read, selected with the ?values query parameter.

  3. A script response — when the GET verb declares a linkedAction, 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:

root/webhook/boolean-endpoint/boolean-value/owner.webhook
{
    "linkedEndpoint": "root.webhook.boolean-endpoint"
}

The value itself is configured separately:

root/webhook/boolean-endpoint/boolean-value/value.ospp
{
    "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:

Response of a GET on an endpoint exposing four values
[
    { "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 to type (INTEGER / DECIMAL → number, BOOLEAN → boolean, TEXT → string). null when the value has no current content.

  • type: the OnSphere type of the value (INTEGER, DECIMAL, BOOLEAN, or TEXT). null when 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:

root/action/action.ospp
{
    "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:

root/webhook/script-endpoint/endpoint.webhook
{
    "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: the RUN_SCRIPT action 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 the webhook controller.

  • 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

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, or an empty string when the header is absent.

request.body()

The raw request body (POST / PUT).

request.json()

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.

request.query()

The query parameters (GET / DELETE) as a key/value object.

Building the response

  • request.respond(code) sets the HTTP status code with no body.

  • request.respond(code, content, contentType) sets the status code, the response content and its mandatory contentType. content must be a string and is sent as-is — the script produces the final body itself (e.g. JSON.stringify(...) for JSON). contentType must 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.

root/script/handle-request.js
// 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 to 200 when the script does not call request.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: the Content-Type returned 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.