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#
- Every send interval (Settings → Delivery, default 60 seconds), if any signal has finished.
- Sooner when 500 signals are waiting.
- Nothing is sent when nothing finished.
- If the address cannot be reached, the batch is kept on the Mac and retried with growing delays (30 seconds up to 1 hour). Batches older than 7 days are dropped.
- Signals collected while no address was set are kept on the Mac and sent as soon as an address is added.
- When the app quits or is paused, it closes everything in progress and sends it, waiting a few seconds for the upload to finish.
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 ) )- Read the raw request body as bytes. Do not parse and re-serialise it first.
- Reject requests whose
X-CleanSignals-Timestampis more than 5 minutes away from your clock. - Compute the signature and compare it in constant time.
- Reply with any
2xxstatus 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.