Set up Event Stream Distributor via REST API
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.
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",
"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!!"
}
}
}
}'
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.
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,
"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"
}
]
}
}
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.
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);
}
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.
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.