DK Studio/Documentation
ProductChangelog

API Reference

Every HTTP endpoint the studio exposes, grouped by who calls it. The base URL is the studio's own address β€” http://localhost:3000 in development.

Authentication

There are two identities in the platform and each has its own credential.

CallerCredentialSent as
A personThe session cookie.The cookie itself, sent by the browser on every request.
An agentIts apiKey and its agentSecret.In the request body of the REST calls, and in the socket payload of the auth request.

A session is created by POST /api/auth/login and lasts seven days. The cookie is httpOnly and sameSite="lax", and it carries the secure flag when NODE_ENV=production.

An agent holds two credentials, and each belongs to one channel. The apiKey is what the REST endpoints under /api/agent take; the agentSecret is what the socket handshake compares. Neither is ever sent in a header. See Socket Protocol for the socket side.

The studio serves one origin

The web UI, the REST API and the realtime socket are served by the same server on the same port. Run the studio behind TLS, or on a trusted private network, and give every agent an account with the narrowest role that still lets it work.

Conventions

ConventionRule
Request bodiesJSON. Content-Type: application/json.
Response bodiesJSON, except the redirect from GET /api/auth/sso.
IdentifiersOpaque strings. agentId, commandId and the ids under /api/admin are not to be parsed.
TimestampsISO 8601, in UTC.
Errors{"error": "…"}. POST /api/agent/register uses {"success": false, "error": "…"}.
CursorsNone. Collections are returned whole.

Setup and system

GET/api/system/statusWhere the setup currently stands, and the platform version.
{
  "status": "ready",
  "needsSetup": false,
  "databaseConfigured": true,
  "adminCreated": true,
  "version": "1.0.0"
}

status is uninitialized (no database configured), needs_admin (configured but no admin) or ready. version is the platform's own version string β€” see Version Compatibility.

GET/api/system/setupThe current database configuration.
GET/api/system/setup?action=statusThe wizard's progress.
{
  "isComplete": true,
  "currentStep": "complete",
  "startedAt": "2026-10-21T07:30:00.000Z",
  "needsResume": false
}
POST/api/system/setupRun a wizard step.

The body selects the step:

{ "step": "license",   "data": { "licenseKey": "ENT0-DKST-UDIO-2026" } }
{ "step": "database",  "data": { "dbType": "sqlite", "database": "dkstudio" } }
{ "step": "initialize" }
{ "step": "admin",     "data": { "email": "…", "name": "…", "password": "…", "confirmPassword": "…" } }
{ "step": "complete" }

The initialize step applies the schema for the configured engine.

POST/api/system/setup?action=resetDelete the setup state.

Optionally takes { "deleteDatabase": true } to drop the PostgreSQL or MySQL database. For SQLite the database file is always deleted.

Authentication endpoints

POST/api/auth/loginSign in and set the session cookies.
{ "email": "admin@lab.example", "password": "…" }

Answers with the user and sets session (httpOnly) and session_client.

{
  "success": true,
  "user": { "id": "…", "email": "…", "name": "…", "role": "admin" }
}
POST/api/auth/logoutClear the session cookies.
GET/api/auth/statusWho the session belongs to.
{
  "isAuthenticated": true,
  "user": { "id": "…", "email": "…", "name": "…", "role": "admin" }
}
GET/api/auth/ssoSingle sign-on.

Takes a code query parameter, redirects to the SSO provider at https://sso.ingeek.com, and sets the auth_token and refresh_token cookies.

Agent-facing

These are the endpoints the agent calls.

GET/api/agent/licenseLook up a licence by code.

Takes ?code=… and answers with the licence's type, scope and whether it can still be used.

POST/api/agent/registerClaim a licence and receive the agent's identity.
{
  "code": "dk_l_…",
  "name": "Bench 01",
  "studioUrl": "http://localhost:3000",
  "workstation": { "hostname": "bench-01", "platform": "linux", "arch": "x64" },
  "capabilities": []
}

Answers with the agent's identity:

{
  "success": true,
  "agentId": "…",
  "apiKey": "…32 alphanumeric characters…",
  "agentSecret": "…64 hex characters…",
  "licenseType": "personal",
  "scope": "personal",
  "ownerUserId": "…",
  "reRegistered": false
}

message carries a human-readable summary of the outcome. Registering again with the agent's own agentId and agentSecret updates the agent in place, sets reRegistered to true and increments registrationCount.

StatusBodyMeaning
400{"success": false, "error": "…"}A required field is absent.
403invalid_codeThe code matches no licence.
403usedThe licence was already spent on another machine.
403revokedThe licence was revoked.
403expiredThe licence has passed its expiry.
403invalid_credentialsThe agentId and agentSecret pair matches no agent.
POST/api/agent/authExchange an API key for the agent's identity.
{ "apiKey": "…" }
{
  "success": true,
  "agentId": "…",
  "name": "Bench 01",
  "capabilities": []
}

It does not change the agent's status β€” presence comes from the socket. An unknown key is 401 with Invalid API key.

POST/api/agent/heartbeatReport that the agent is alive.
{ "agentId": "…", "status": "active" }

Answers { "success": true, "timestamp": "…" }. Only last_seen_at is updated; the status field in the body is ignored.

POST/api/agent/unregisterMark the agent as unregistered.
{ "agentId": "…" }

Answers { "success": true }. The agent's stored status becomes unregistered and the agent row is kept.

GET/api/agent/commands?agentId=…Claim pending commands.

Returns the agent's pending commands and flips them to in_progress. Each command arrives as the same command request envelope the socket carries:

{
  "commands": [
    {
      "v": 1,
      "type": "command",
      "kind": "request",
      "id": "…",
      "timestamp": "…",
      "payload": { "command": "shell", "args": { "script": "uname -a" } }
    }
  ]
}
POST/api/agent/commands/resultReport a command's outcome.

The body is the command response envelope β€” the same one the socket carries:

{
  "v": 1,
  "type": "command",
  "kind": "response",
  "id": "…",
  "timestamp": "…",
  "payload": {
    "ok": true,
    "data": {
      "status": "success",
      "output": { "stdout": "…", "stderr": "", "exitCode": 0, "durationMs": 42 }
    }
  }
}

Answers { "success": true }. The command row is marked completed and the result is stored as JSON.

Studio

GET/api/agentsThe agents that are online right now.
{
  "agents": [
    { "id": "…", "name": "Bench 01", "status": "online", "lastSeen": "…", "capabilities": [] }
  ],
  "timestamp": "…"
}

Only agents whose stored status is active and that have a live socket are returned.

POST/api/agents/commandQueue a command for an agent.
{
  "agentId": "…",
  "command": "shell",
  "args": { "script": "uname -a", "cwd": "/tmp" },
  "timeout": 30000
}

Answers with a commandId:

{ "success": true, "commandId": "…", "message": "…" }
StatusBodyMeaning
400Missing agentId or commandA required field is absent.
400Agent is offlineThe agent has no live socket.
400Agent is not activeThe agent's stored status is not active.
404Agent not foundNo agent has that id.
GET/api/agents/commands/{commandId}Read a command's state and result.
{
  "id": "…",
  "agentId": "…",
  "commandType": "shell",
  "status": "completed",
  "result": { "status": "success", "output": { "stdout": "…", "exitCode": 0 } },
  "startedAt": "…",
  "completedAt": "…"
}

status is pending, in_progress or completed.

GET/api/dashboard/statsThe dashboard counters.
{
  "totalProjects": 0,
  "activeTestRuns": 0,
  "devicesOnline": 0,
  "successRate": 0
}

Admin

GET/api/admin/agentsList the registered agents.
{
  "agents": [
    {
      "id": "…",
      "name": "Bench 01",
      "status": "active",
      "connectionStatus": "online",
      "capabilities": [],
      "workstation": { "hostname": "bench-01" },
      "lastSeen": "…",
      "registrationCount": 1
    }
  ]
}
GET/api/licensesList the licences you own and the agents you can see.
{
  "userId": "…",
  "licenses": [
    { "id": "…", "code": "dk_l_…", "type": "personal", "ownerUserId": "…", "status": "pending", "expiresAt": null }
  ],
  "agents": []
}
POST/api/licensesIssue a licence.

Takes { "type": "personal" | "lab", "location": "…", "label": "…", "expiresAt": "…" }. A personal licence belongs to the caller; a lab licence can only be issued by an administrator. Answers with the licence and its code β€” the only time the code is shown.

GET/api/admin/agents/{id}Read one agent's detail.

Answers with the agent's detail record.

DELETE/api/admin/agents/{id}Delete an agent.

Deletes the agent row and its command history. An id that matches no agent is 404 Agent not found.

POST/api/licenses/{id}/revokeRevoke a licence.

Answers { "success": true } and sets the licence's status to revoked. The owner of a licence may revoke their own; an administrator may revoke any. A licence that has already been consumed cannot be revoked β€” the machine is on the ledger.

DELETE/api/licenses/{id}Delete a licence.

Administrators only. A licence that has already been consumed cannot be deleted.

GET/api/admin/usersList the users.
{
  "users": [
    { "id": "…", "email": "…", "name": "…", "role": "admin", "createdAt": "…" }
  ]
}
POST/api/admin/usersCreate a user.
{
  "email": "engineer@lab.example",
  "name": "Lab Engineer",
  "password": "…",
  "role": "engineer"
}

Answers { "success": true, "user": { … } }. role is admin, engineer or viewer; anything unrecognised becomes viewer. The email must be unique.

DELETE/api/admin/usersDelete a user.

Takes { "id": "…" } in the body and answers { "success": true }.

Status codes

StatusMeaning
200The request succeeded.
400A required field is missing, or the agent cannot take the command.
401Invalid API key β€” the key matches no agent.
403The licence is invalid, revoked or expired.
404No agent, licence or user has that id.
500Database not configured β€” the endpoint needs the database and setup has not run.

See also