Delivery format

CleanSignals sends signals to one webhook address with an HTTPS POST. Each request carries one batch: a small envelope with the signals inside.

When batches are sent#

Request#

HTTP request
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: CleanSignals/0.1.0 (macOS)
X-CleanSignals-Timestamp: 1791644400
X-CleanSignals-Signature: v1=pdneHSOiM+CGzizQlAMRyRwwOefZLA2WqC/aSIF0Z0c=
JSON
{
  "type": "signals.batch",
  "schema_version": "1.0",
  "batch_id": "01JA3Q7V9K2M4N6P8R0T2W4Y6Z",
  "seq": 4182,
  "sent_at": "2026-10-10T12:00:03Z",
  "device_id": "dev_c0ffee91a2b3c4d5",
  "org_id": "acme",
  "subject": "sub_4f9c2e7a1b8d0e33",
  "external_xref": "hris-emp-00421",
  "config_hash": "sha256:1f2e3d4c5b6a7980",
  "nonce": "9a8b7c6d5e4f3a2b",
  "app_version": "0.1.0",
  "data": [
    {"id": "01JA3P…", "type": "app.focus", "v": 1, "bundle_id": "com.microsoft.Excel", "app_name": "Excel", "category": "analysis", "start": "2026-10-10T09:14:50Z", "end": "2026-10-10T09:52:07Z", "duration_s": 2237}
  ]
}

Envelope fields#

Field Meaning
type Always signals.batch.
schema_version Version of the envelope. Currently 1.0.
batch_id Unique id of this batch. The same batch can arrive twice after a retry; store it and ignore repeats.
seq Number that goes up by one for each batch from this Mac. A gap means a batch has not arrived yet.
sent_at When the batch was built.
device_id Id of the Mac, made when the app is first opened.
org_id Organisation id from Settings → Identity.
subject Coded id of the person: HMAC of the Mac user name with this Mac's private salt.
external_xref Optional reference typed in Settings → Identity, such as an HR system id. null if empty.
config_hash Short code of the settings that produced the batch (without secrets), so you can tell when settings changed.
nonce Random value.
app_version Version of the CleanSignals app.
data The signals. See the signal catalogue.

Checking the signature#

If a secret is set, every request is signed:

Text
X-CleanSignals-Signature = "v1=" + base64( HMAC-SHA256( secret, timestamp + "." + raw_body ) )
  1. Read the raw request body as bytes. Do not parse and re-serialise it first.
  2. Reject requests whose X-CleanSignals-Timestamp is more than 5 minutes away from your clock.
  3. Compute the signature and compare it in constant time.
  4. Reply with any 2xx status quickly. Any other status, or no reply, means the app will retry.

Node.js#

JavaScript
const crypto = require("crypto");

function verify(secret, req, rawBody) {
  const ts = req.headers["x-cleansignals-timestamp"];
  const sig = req.headers["x-cleansignals-signature"] || "";
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const mac = crypto.createHmac("sha256", secret).update(`${ts}.`).update(rawBody).digest("base64");
  const expected = Buffer.from("v1=" + mac);
  const given = Buffer.from(sig);
  return expected.length === given.length && crypto.timingSafeEqual(expected, given);
}

Python#

Python
import base64, hashlib, hmac, time

def verify(secret: str, timestamp: str, signature: str, raw_body: bytes) -> bool:
    if abs(time.time() - int(timestamp)) > 300:
        return False
    mac = hmac.new(secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256).digest()
    expected = "v1=" + base64.b64encode(mac).decode()
    return hmac.compare_digest(expected, signature)

Test value: secret test_secret, timestamp 1700000000, body {"hello":"world"} gives v1=pdneHSOiM+CGzizQlAMRyRwwOefZLA2WqC/aSIF0Z0c=.

Trying it without a server#

Put a temporary address from a request-inspection site (such as webhook.site) in Settings → Delivery and click Send test batch. You will see the exact request, headers and body. Or create a free account on the CleanSignals dashboard to get an address that stores and charts your signals.