Skip to main content

Set up Event Stream Distributor via REST API

Feature entitlement: eventStreams · Plan: 👑 Ultra Device Plan

Introduction​

This tutorial describes how to set up an Event Stream Distributor (ESD) exporter via the REST API. An exporter forwards signageOS platform events (device pairing, provisioning recipes, telemetry, connection changes) to your own HTTPS webhook. For the concepts behind ESD and the full list of subscribable events, see Introduction to Event Stream Distributor.

The same setup can also be done in the UI: see Set up Event Stream Distributor in Box.

Prerequisites​

All exporter endpoints use your Organization Auth Token in the x-auth header, in the format token_id:token_secret. If you do not have one yet, follow Getting started with REST APIs to create an organization and its token.

Create an exporter​

Send a POST to /v1/event-stream/exporter. The exporter starts disabled unless you set enabled: true. The optional formatVersion (1 or 2) selects the delivery envelope you receive — see Format versions in the Event Stream overview; omitted, it means version 1 (legacy). Set it explicitly (preferably to the newest version) when creating a new integration.

curl -X POST \
https://api.signageos.io/v1/event-stream/exporter \
-H 'Content-Type: application/json' \
-H 'x-auth: 12a15XXX28612d:2e220XXX77745' \
-d '{
"organizationUid": "117b6d8XXXX18ed4c",
"name": "My Webhook Exporter",
"description": "Forwards device connection events to our backend",
"formatVersion": 2,
"subscribedEventTypes": [
"Device.DeviceConnectionAdded",
"Device.DeviceConnectionDeleted"
],
"config": {
"configType": "webhook",
"url": "https://example.com/webhook",
"auth": {
"authType": "httpSignedBody",
"signing": {
"algorithm": "HS256",
"secret": "my-secret-key-at-least-32-chars!!"
}
}
}
}'
warning

The signing secret must be at least 32 characters. It is write-only: it is used to sign your webhook deliveries but is never returned by any read endpoint, so store it safely on your side.

warning

As a response you will get 201 and, in the Headers, the Location link where you can find the new exporter's details. Save the exporter's uid for later use.

List and count exporters​

List all exporters for the organization:

curl -X GET \
https://api.signageos.io/v1/event-stream/exporter \
-H 'x-auth: 12a15XXX28612d:2e220XXX77745'

Each item includes an observability summary (delivered messages, retries, errors, and the last successful delivery time). To get only the total number of exporters:

curl -X GET \
https://api.signageos.io/v1/event-stream/exporter/count \
-H 'x-auth: 12a15XXX28612d:2e220XXX77745'

Get a single exporter​

Fetch one exporter by its uid. The single-exporter read also returns the most recent delivery failures under observability.recentFailures.

curl -X GET \
https://api.signageos.io/v1/event-stream/exporter/61d9XXXX2f7a \
-H 'x-auth: 12a15XXX28612d:2e220XXX77745'
{
"uid": "61d9XXXX2f7a",
"organizationUid": "117b6d8XXXX18ed4c",
"name": "My Webhook Exporter",
"description": "Forwards device connection events to our backend",
"enabled": true,
"formatVersion": 2,
"subscribedEventTypes": [
"Device.DeviceConnectionAdded",
"Device.DeviceConnectionDeleted"
],
"config": {
"configType": "webhook",
"url": "https://example.com/webhook"
},
"createdAt": "2026-07-01T09:25:05.617Z",
"updatedAt": "2026-07-01T09:25:05.617Z",
"observability": {
"exportedMessages": 1200,
"errors": 4,
"retries": 12,
"lastSuccessfulExportAt": "2026-07-01T10:14:00.000Z",
"recentFailures": [
{
"eventId": "0197f2a4-3a5e-7b1c-9d2f-8c4e6a1b3d5f",
"eventType": "Device.DeviceConnectionAdded",
"attempts": 3,
"lastError": "httpStatus=500",
"failedAt": "2026-07-01T10:31:00.000Z"
}
]
}
}
info

The signing secret is not part of the response. Only config.configType and config.url are returned.

Update an exporter​

Send a PUT to /v1/event-stream/exporter/{uid} with the fields you want to change. For example, to enable an exporter and change which events it forwards:

curl -X PUT \
https://api.signageos.io/v1/event-stream/exporter/61d9XXXX2f7a \
-H 'Content-Type: application/json' \
-H 'x-auth: 12a15XXX28612d:2e220XXX77745' \
-d '{
"enabled": true,
"subscribedEventTypes": [
"Device.Verification.DevicePaired",
"Device.Verification.DeviceUnpaired"
]
}'

A successful update returns 204 No Content.

The format version can be switched the same way at any time — for example { "formatVersion": 2 } moves the exporter to the version 2 envelope, and { "formatVersion": 1 } moves it back. The switch applies to deliveries dispatched after the update.

Delete an exporter​

curl -X DELETE \
https://api.signageos.io/v1/event-stream/exporter/61d9XXXX2f7a \
-H 'x-auth: 12a15XXX28612d:2e220XXX77745'

A successful delete returns 204 No Content.

Receive and verify the webhook​

Once the exporter is enabled, each subscribed event is delivered to your url as a signed HTTP POST. The request body (id, kind, type, payload) and the full list of X-SOS-* headers are documented in the Event catalogue; this section covers verifying the signature.

Every delivery carries an X-SOS-Signature header of the form v1=<base64> and an X-SOS-Timestamp in Unix seconds. Verify the signature on every request before trusting the payload: strip the v1= prefix and base64-decode the rest to get the provided signature bytes, recompute the HMAC-SHA256 over the concatenation of the uppercased HTTP method, the delivery URL, the X-SOS-Timestamp value, and the raw request body (with no separators between them), then compare the two in constant time:

const crypto = require('crypto');

const SIGNATURE_VERSION_PREFIX = 'v1=';

// method: 'POST' (deliveries are always POST)
// url: the exact URL the exporter delivers to (your configured config.url)
// rawBody: the exact received body string, verified BEFORE JSON parsing
function isValidSignature(method, url, rawBody, headers, secret) {
const header = headers['x-sos-signature'] ?? '';
if (!header.startsWith(SIGNATURE_VERSION_PREFIX)) {
return false;
}
const provided = Buffer.from(header.slice(SIGNATURE_VERSION_PREFIX.length), 'base64');
const payload = method.toUpperCase() + url + String(headers['x-sos-timestamp']) + rawBody;
const expected = crypto.createHmac('sha256', secret).update(payload).digest();
return expected.length === provided.length && crypto.timingSafeEqual(expected, provided);
}
warning

Verify against the raw request body, not a re-serialized JSON object: re-serializing can change bytes (key order, whitespace) and break the signature. The signed URL is the exporter's configured config.url, and the timestamp is in seconds.

tip

For each event type and its payload, see the Event catalogue. For the concepts, see Introduction to Event Stream Distributor, and for the exporter endpoints, the REST API reference.