Developer API v1

Rain Neural Engine API

Read the state of your organization's protected PCs and manage them from your own servers, scripts and monitoring tools. Developer integration requests are signed with an Ed25519 key whose private half never leaves your side.

Overview

Base URLhttps://brain.raincleaner.eu
FormatJSON over HTTPS only. CSV where noted.
AuthenticationSigned requests, scheme RNE-ED25519-V1
Who can use itOrganizations with a Business Console account. Keys are created by an owner or administrator in the Business Console.
ScopeYour own organization only: /v1/organizations/{organization_id}/…

The API manages devices, policies and rollouts. It never runs commands on a PC, never reads files, screens, user names or process lists, and no policy can end processes, delete files or shut a computer down.

Security model

  • Your private key never reaches us. We store only the public key. A copy of our database or logs cannot sign a single request.
  • Every developer request is signed. Method, host, path, query string, timestamp, a one-time nonce and the SHA-256 of the body. Changing anything breaks the signature.
  • No replays. A request is accepted only within 5 minutes of its timestamp, and each nonce only once.
  • Least privilege. Each key has explicit permissions. Billing, members, device enrollment and key management are never available to a key.
  • Rights follow people. A key never has more rights than the current role of the person who created it. If that person leaves the organization or gets a smaller role, the key loses those rights on the next request.
  • Containment. Mandatory expiry (at most 365 days), an optional IP allowlist, instant revocation, and a rate limit per key.
  • Accountability. Creating and revoking a key needs a fresh authenticator code. Every change made with a key is written to your tamper-evident (hash-chained) activity log together with the key id.
  • No oracle. Every authentication failure gets the same 401 answer.

1. Create a key

In the Business Console open Developer API → New API key, choose a name, the permissions, an optional IP allowlist and the expiry, and enter your authenticator code. Two ways to get the key pair:

  • Generated in your browser (leave the public key field empty). The browser creates the pair, sends only the public key, and saves the private key once as rnek_….pem. Store it like a password; we cannot show it again.
  • Your own key pair, e.g. generated on the server that will use it:
openssl genpkey -algorithm ed25519 -out rne-key.pem
chmod 600 rne-key.pem
openssl pkey -in rne-key.pem -pubout          # paste this PUBLIC key into the console

The console answers with the key id (rnek_ + 24 characters). The key id is not a secret; the .pem file is.

Permissions

PermissionAllows
devices.readOverview, device list, rollout state
audit.readActivity log
devices.manageRevoke, offboard, release a seat, groups, experiments, device data export and erasure
policies.writeSign new policy versions and reverts
rollouts.writeStart and stop rollouts (reading a rollout needs devices.read)

2. Sign each request

Build the string to sign: eight lines joined with a single line feed (\n), no line feed at the end, UTF-8.

RNE-ED25519-V1
{METHOD}
{HOST}
{PATH}
{QUERY}
{TIMESTAMP}
{NONCE}
{BODY_SHA256}
LineExact value
METHODUpper case: GET, POST, PUT
HOSTLower case host you connect to: brain.raincleaner.eu
PATHThe path exactly as sent, including any percent-encoding, e.g. /v1/organizations/…/groups/Front%20desk
QUERYThe query string exactly as sent, without ?; an empty line if there is none
TIMESTAMPUnix time in seconds (10 digits). Must be within ±300 s of our clock — keep NTP on.
NONCE16–64 random characters from A–Z a–z 0–9 _ -, new for every request (e.g. 18 random bytes in base64url)
BODY_SHA256Lower-case hex SHA-256 of the exact body bytes you send. Empty body: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Sign the string with your Ed25519 private key and send the signature, base64url-encoded without padding, in one header:

Authorization: RNE-ED25519-V1 key={KEY_ID},ts={TIMESTAMP},nonce={NONCE},sig={SIGNATURE}

The four parameters appear exactly once each, separated by commas. Send Content-Type: application/json with a JSON body. Answers to signed requests carry Cache-Control: no-store.

Use this signed header for integrations. The Business Console uses its own authenticated browser session for the same organization resources; do not copy a console session token into an integration.

Test vector

Ed25519 signatures are deterministic, so you can check your implementation offline against this example. This key is public and is refused by the API — never use it for real.

Private key (PEM):
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f
-----END PRIVATE KEY-----
Public key (base64): A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=

Request: POST https://brain.raincleaner.eu/v1/organizations/8f6c3d2e-1b4a-4c5d-9e8f-0a1b2c3d4e5f/devices/2d1c7b9e-4a3f-4e8d-b6a5-1f0e9d8c7b6a/experimental
Body (17 bytes): {"excluded":true}
Timestamp: 1793448000    Nonce: q1W8x3rT0nYb6LkEj2HfZa

String to sign:
RNE-ED25519-V1
POST
brain.raincleaner.eu
/v1/organizations/8f6c3d2e-1b4a-4c5d-9e8f-0a1b2c3d4e5f/devices/2d1c7b9e-4a3f-4e8d-b6a5-1f0e9d8c7b6a/experimental

1793448000
q1W8x3rT0nYb6LkEj2HfZa
aeaffb6559bcbd4fcaf28b5c50c93880aede3052278a3dfdedd3057fd88dd81c

Signature (base64url):
dQss8cE3yGsEG96SLCCdJatITtHhUoyUm7V32kQBBioDo3DgmYJ6df_A_iFTZMlbJkq7aiASONVr7Z0IXnvnCQ

3. Examples

Each example reads RNE_KEY_ID, RNE_ORG (your organization id) and RNE_KEY_FILE (path to the .pem) from the environment.

Python 3 (package cryptography)

import base64, hashlib, json, os, secrets, time, urllib.parse, urllib.request
from cryptography.hazmat.primitives.serialization import load_pem_private_key

BASE = os.environ.get("RNE_BASE", "https://brain.raincleaner.eu")
KEY_ID = os.environ["RNE_KEY_ID"]          # rnek_...
ORG = os.environ["RNE_ORG"]                # your organization id
with open(os.environ["RNE_KEY_FILE"], "rb") as f:   # the .pem file of the key
    PRIVATE_KEY = load_pem_private_key(f.read(), password=None)


def b64url(raw: bytes) -> str:
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()


def call(method: str, path: str, query: str = "", body=None):
    data = b"" if body is None else json.dumps(body, separators=(",", ":")).encode()
    ts, nonce = str(int(time.time())), b64url(secrets.token_bytes(18))
    host = urllib.parse.urlsplit(BASE).netloc
    message = "\n".join(["RNE-ED25519-V1", method, host, path, query, ts, nonce,
                         hashlib.sha256(data).hexdigest()])
    sig = b64url(PRIVATE_KEY.sign(message.encode()))
    req = urllib.request.Request(
        BASE + path + ("?" + query if query else ""), method=method,
        data=data if body is not None else None,
        headers={"Authorization": f"RNE-ED25519-V1 key={KEY_ID},ts={ts},nonce={nonce},sig={sig}",
                 "Content-Type": "application/json", "Accept": "application/json",
                 "User-Agent": "rne-client/1.0"})
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.load(r)


if __name__ == "__main__":
    print(call("GET", f"/v1/organizations/{ORG}/dashboard"))
    print(len(call("GET", f"/v1/organizations/{ORG}/audit", "limit=5")["events"]), "audit events")

Node.js 18+ (no packages)

import { createHash, createPrivateKey, randomBytes, sign } from "node:crypto";
import { readFileSync } from "node:fs";

const BASE = process.env.RNE_BASE ?? "https://brain.raincleaner.eu";
const KEY_ID = process.env.RNE_KEY_ID;                       // rnek_...
const ORG = process.env.RNE_ORG;                             // your organization id
const privateKey = createPrivateKey(readFileSync(process.env.RNE_KEY_FILE));
const b64url = (buf) => Buffer.from(buf).toString("base64url");

export async function call(method, path, query = "", body) {
  const data = body === undefined ? "" : JSON.stringify(body);
  const ts = String(Math.floor(Date.now() / 1000));
  const nonce = b64url(randomBytes(18));
  const message = ["RNE-ED25519-V1", method, new URL(BASE).host, path, query, ts, nonce,
    createHash("sha256").update(data).digest("hex")].join("\n");
  const sig = b64url(sign(null, Buffer.from(message), privateKey));
  const res = await fetch(BASE + path + (query ? `?${query}` : ""), {
    method, body: body === undefined ? undefined : data,
    headers: { Authorization: `RNE-ED25519-V1 key=${KEY_ID},ts=${ts},nonce=${nonce},sig=${sig}`,
      "Content-Type": "application/json", Accept: "application/json" },
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return res.json();
}

console.log(await call("GET", `/v1/organizations/${ORG}/dashboard`));
console.log((await call("GET", `/v1/organizations/${ORG}/devices`)).devices.length, "devices");

Bash (OpenSSL 3 and curl)

#!/usr/bin/env bash
# Usage: rne.sh METHOD PATH [QUERY] [JSON_BODY]
set -euo pipefail
BASE="${RNE_BASE:-https://brain.raincleaner.eu}"
METHOD="$1"; PATH_="$2"; QUERY="${3:-}"; BODY="${4:-}"
HOST="${BASE#*://}"
TS=$(date +%s)
NONCE=$(openssl rand -base64 18 | tr '+/' '-_' | tr -d '=')
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -r | cut -d' ' -f1)
MSG=$(mktemp); trap 'rm -f "$MSG"' EXIT
printf 'RNE-ED25519-V1\n%s\n%s\n%s\n%s\n%s\n%s\n%s' \
  "$METHOD" "$HOST" "$PATH_" "$QUERY" "$TS" "$NONCE" "$BODY_HASH" > "$MSG"
SIG=$(openssl pkeyutl -sign -rawin -inkey "$RNE_KEY_FILE" -in "$MSG" | openssl base64 -A | tr '+/' '-_' | tr -d '=')
URL="$BASE$PATH_${QUERY:+?$QUERY}"
AUTH="Authorization: RNE-ED25519-V1 key=$RNE_KEY_ID,ts=$TS,nonce=$NONCE,sig=$SIG"
if [ -n "$BODY" ]; then
  curl -sS --fail-with-body -X "$METHOD" -H "$AUTH" -H 'Content-Type: application/json' --data-binary "$BODY" "$URL"
else
  curl -sS --fail-with-body -X "$METHOD" -H "$AUTH" "$URL"
fi
echo
./rne.sh GET  /v1/organizations/$RNE_ORG/devices format=csv
./rne.sh POST /v1/organizations/$RNE_ORG/devices/$DEVICE_ID/experimental '' '{"excluded":true}'

Endpoints

All paths start with /v1/organizations/{organization_id} (shown as …). IDs are UUIDs. Names of groups and policies: 1–100 characters, letters, digits, space and . _ -, starting with a letter or digit (percent-encode spaces in the path).

Devices and overview

RequestPermissionBody / queryAnswer
GET …/dashboarddevices.read— devices_total, devices_online, devices_offline, devices_with_problems, neural_protection {on, off}, agent_versions, model_versions, active_alerts, unlicensed_devices
GET …/devicesdevices.readformat=csv (optional) {"devices": [...]}, up to 1000; each with id, display_name, os_version, agent_version, model_version, status, health_state (healthy/problem/offline/lost/revoked/retired), health_score, active_alerts, licensed, last_seen_at
GET …/auditaudit.readlimit=1…500 (default 100), format=csv {"events": [{created_at, action, actor_user_id, target, details}]}, newest first

Device management

RequestPermissionBodyAnswer
POST …/devices/{device_id}/revokedevices.manage {"reason": "1–200 chars"}Lost or compromised: certificates revoked at once, seat freed. {device_id, status, certificates_revoked, seats_freed}
POST …/devices/{device_id}/offboarddevices.manage {"reason": "…"}Retired normally; same answer as revoke
POST …/devices/{device_id}/license/releasedevices.manage —{device_id, seats_freed}
POST …/devices/{device_id}/experimentaldevices.manage {"excluded": true|false}{device_id, exclude_experimental}
PUT …/groups/{name}devices.manage {"devices": ["uuid", ...]} (replaces the members){group, devices}
GET …/devices/{device_id}/exportdevices.manage —GDPR export of everything stored about the device: {device_id, format, tables}
POST …/devices/{device_id}/erasedevices.manage {"reason": "…"}Only after revoke/offboard (else 409). {device_id, deleted}

Policies and rollouts

RequestPermissionBodyAnswer
POST …/policies/{name}/versionspolicies.write {"policy": {...}} (below)201 {policy_name, version, signature}
POST …/policies/{name}/revertpolicies.write {"to_version": 3}Signs that version's settings again as the newest version
POST …/rolloutsrollouts.write {"policy_name", "version", "group"}201 rollout state
GET …/rollouts/{rollout_id}devices.read—Rollout state
POST …/rollouts/{rollout_id}/stoprollouts.write {"reason": "1–500 chars"}Emergency stop; rollout state

Rollout state: {rollout_id, state, reason, wave, wave_sizes, targets}. Waves are 5, 25, then the rest; the next wave starts only when every device of the current one succeeded, and one failure halts the rollout.

{
  "features": {"shield": true, "flow": false, "vault": false, "storage": false, "collective_learning": false},
  "max_action_level": 1,                       // 0 explain only, 1 safe automatic, 2 suggestions needing approval
  "updates": {"channel": "stable", "automatic": true},           // channel: stable | beta
  "maintenance_windows": [{"day": "sun", "start": "02:00", "minutes": 120}],   // up to 14; 15–720 minutes
  "limits": {"agent_cpu_percent": 5, "upload_kib_per_day": 256},  // 1–10 %, 16–4096 KiB
  "experimental": false
}

Comments are for reading only; send plain JSON. Unknown fields are refused.

Errors

Errors are JSON: {"error": {"code", "message", "request_id"}}. Every answer has an X-Request-ID header; quote it when you contact us.

StatusCodeMeaning
401unauthenticatedMissing, malformed, wrong, expired, revoked or replayed signature, stale timestamp, or an IP outside the allowlist
403forbiddenValid key, but no permission for this request or another organization
404not_foundNo such device, policy version or rollout
409variesConflict, e.g. device_active (erasing an active device) or no_devices (empty group)
413payload_too_largeBody over 64 KB (may be answered by the edge without a JSON body)
422validation_error, invalid_policyThe body does not match the schema
429rate_limitedToo many requests; wait and retry
5xxinternal_errorOur side; retry with a new nonce after a short pause

Retrying: each attempt needs a new nonce and timestamp (a reused nonce is refused). Do not retry 401, 403 or 422 unchanged.

Limits

  • 120 requests per minute per key. The edge also allows 10 requests per second per client IP, with a brief burst of up to 20 additional requests.
  • Request body: 64 KB.
  • Up to 50 active keys per organization; up to 20 entries in an IP allowlist.

Key lifecycle

  • Rotate before expiry: create a new key, deploy it, check its Last used in the console, then revoke the old one.
  • Revoke at once if a .pem file may have leaked: Developer API → Revoke. The next request with that key gets 401.
  • One key per server or integration, with only the permissions it needs, and an IP allowlist where the server has a fixed address.
  • Never put a private key in a browser, a mobile app, a public repository or a support e-mail. We will never ask you for it.

Versioning

v1 is stable. We may add endpoints and add fields to answers; ignore fields you do not know. Anything that breaks existing clients will come as /v2, announced in advance. Questions: contact us.