HTTP API reference

The server is a Ktor (Netty) app that binds 0.0.0.0:8081 (server/src/main/kotlin/com/arganaemre/mdmcore/Application.kt).

  • Base URLhttp://localhost:8081 from the host machine (the desktop/web console); http://10.0.2.2:8081 from an Android emulator (the agent). See agent.md.
  • Content typeapplication/json, encoded/decoded with the shared MdmJson config (ignoreUnknownKeys, encodeDefaults; see protocol.md).
  • CORSanyHost() with GET/POST/PUT and the Content-Type header allowed. This is dev-only so the browser web console (a different origin) can call the API; lock it down before any real deployment.
  • Bodies use wrapped ids. DeviceId serializes as {"value":"device-1"}. The {id} in a URL path is the bare string (device-1); in a JSON body it’s the object. (See the note on DeviceId in protocol.md.)

Endpoints

Method Path Request body Success Errors Caller
GET / 200 text health check
POST /enroll EnrollRequest 200 EnrollResponse agent
GET /devices/{id}/policy 200 DevicePolicy 404 unknown id agent
PUT /devices/{id}/policy PolicyUpdate 200 DevicePolicy 404 unknown id console
POST /devices/{id}/token TokenRegistration 202 404 unknown id agent
POST /devices/{id}/report ComplianceReport 202 agent
GET /devices 200 List<DeviceStatus> console
GET /devices/{id}/status 200 DeviceStatus 404 unknown id console

GET / — health

Returns a plain-text greeting ("Hello, Ktor!"). Handy for “is the server up?”.


POST /enroll

First contact. The server assigns a fresh DeviceId (device-1, device-2, …) and returns it with the default starting policy (version: 1, cameraDisabled: true).

Request:

{ "model": "Pixel 7", "osVersion": "Android 34", "agentVersion": "1.0" }

Response 200:

{ "deviceId": { "value": "device-1" }, "policy": { "version": 1, "cameraDisabled": true } }

GET /devices/{id}/policy

The agent pulls its desired policy. 404 if the id was never enrolled (the agent treats a 404 as “the server forgot me” and re-enrolls).

Response 200:

{ "version": 2, "cameraDisabled": false }

PUT /devices/{id}/policy

The console edits desired policy. The body is a PolicyUpdate (editable fields only). The server bumps the version and returns the new authoritative DevicePolicy. As a side effect it rings the device’s FCM doorbell if a token is registered, so the device re-syncs promptly (best-effort; a no-op if there’s no token or FCM isn’t configured — see fcm-doorbell.md). 404 if the id isn’t enrolled.

Request:

{ "cameraDisabled": false }

Response 200 (note the bumped version):

{ "version": 3, "cameraDisabled": false }

POST /devices/{id}/token

Registers/refreshes the device’s FCM token. 202 Accepted on success; 404 if the id isn’t enrolled (the server never stores a token for an unknown device).

Request:

{ "token": "e1Hf...registration-token...9Qx" }

POST /devices/{id}/report

The agent reports observed state. Always 202 Accepted.

The {id} in the path is ignored here. The handler keys on the deviceId inside the body, not the path segment. Send the correct id in the body; the path is there only for URL symmetry. The endpoint also does not check that the device is enrolled — it stores whatever it receives.

Request:

{
  "deviceId": { "value": "device-1" },
  "appliedPolicyVersion": 2,
  "cameraDisabled": false,
  "compliant": true
}

The server stores this and recomputes compliance itself on the next read; the compliant field is not trusted (see the loop).


GET /devices

The console’s roster: every enrolled device’s status, sorted by id for a stable view.

Response 200:

[
  {
    "deviceId": { "value": "device-1" },
    "desired": { "version": 2, "cameraDisabled": false },
    "lastReport": {
      "deviceId": { "value": "device-1" },
      "appliedPolicyVersion": 1,
      "cameraDisabled": true,
      "compliant": true
    },
    "state": "STALE"
  }
]

(state is STALE here because the last report applied version 1 while desired is now version 2.)


GET /devices/{id}/status

One device’s DeviceStatus. 404 if the id isn’t enrolled. lastReport is null until the device has reported at least once (then state is UNKNOWN).


Try it with curl

Assuming the server is running (./gradlew :server:run) on the host:

# enroll, and capture the assigned id
curl -s -X POST localhost:8081/enroll \
  -H 'Content-Type: application/json' \
  -d '{"model":"Pixel 7","osVersion":"Android 34","agentVersion":"1.0"}'
# -> {"deviceId":{"value":"device-1"},"policy":{"version":1,"cameraDisabled":true}}

# read the desired policy
curl -s localhost:8081/devices/device-1/policy

# edit the desired policy (bumps version, rings the doorbell if a token is registered)
curl -s -X PUT localhost:8081/devices/device-1/policy \
  -H 'Content-Type: application/json' \
  -d '{"cameraDisabled":false}'

# report observed state (deviceId is taken from the body, not the path)
curl -s -X POST localhost:8081/devices/device-1/report \
  -H 'Content-Type: application/json' \
  -d '{"deviceId":{"value":"device-1"},"appliedPolicyVersion":2,"cameraDisabled":false,"compliant":true}'

# the authoritative status
curl -s localhost:8081/devices/device-1/status
curl -s localhost:8081/devices

A full enroll → edit → report → see-it-go-compliant walkthrough is in development.md.