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 URL —
http://localhost:8081from the host machine (the desktop/web console);http://10.0.2.2:8081from an Android emulator (the agent). See agent.md. - Content type —
application/json, encoded/decoded with the sharedMdmJsonconfig (ignoreUnknownKeys,encodeDefaults; see protocol.md). - CORS —
anyHost()withGET/POST/PUTand theContent-Typeheader 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.
DeviceIdserializes 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 onDeviceIdin 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 thedeviceIdinside 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.