# Datto EDR (Infocyte HUNT) API reference Datto EDR == rebranded Infocyte HUNT. The API is a per-tenant **LoopBack** REST service. Everything below was verified live against `azcomp4587.infocyte.com` on 2026-06-25 unless marked otherwise. ## Base URL & auth - Base: `https://.infocyte.com/api` (this tenant: `azcomp4587`). - Self-documenting LoopBack explorer: `https://.infocyte.com/explorer`. - **Auth: raw 64-char token in the `Authorization` header.** NO `Bearer` prefix, no Basic, no OAuth. `Authorization: `. (Source: KaseyaDEDR PowershellTools `requestHelpers.ps1`; confirmed live — `Bearer` prefix would fail.) - Token created in console: username menu → **Admin → Users & Tokens → API Tokens → Create new token**. Shown once. **Expires 1 year after creation.** - Vault: `msp-tools/datto-edr.sops.yaml` field `credentials.api_token`. ## LoopBack conventions - Models are PascalCase collections: `Organizations`, `Locations`, `Targets`, `Agents`, `Alerts`, `Boxes`, `Reports`, `deviceGroups` (lowercase), `agentKeys`. - List with a filter: `GET /?filter=` where the JSON supports `{"where":{...},"limit":N,"order":"field DIR","fields":{"x":true}}`. - `where` operators: `{"field":{"gt":"..."}}`, `{"field":{"inq":[...]}}`, equality `{"field":val}`. - Count: `GET //count?where=` → `{"count":N}`. - Detail: `GET //{id}`. - A model whose route doesn't exist returns `{"error":{"statusCode":404,"message": "There is no method to handle GET /..."}}`. ## Data hierarchy ``` Organization (client) GET /Organizations [id,name,agentCount,alertCount,locationCount,tenantId] └─ Location (site) GET /Locations [id,name,organizationId,agentCount,activeAgentCount,alertCount,lastScannedOn] └─ Agent (endpoint) GET /Agents (Agent.locationId -> Location.id) Target (SCAN group) GET /Targets [id,name,organizationId,agentCount,activeAgentCount,lastScannedOn] (scannable unit) deviceGroup (global category) GET /deviceGroups [id,name,deviceType] ("Servers"/"Workstations" — spans all orgs) Alert (detection) GET /Alerts (carries organizationId/Name, targetGroupId/Name, severity, mitre*) ``` To list **agents for a client**: resolve org → its Locations (`where organizationId`) → `GET /Agents?filter={"where":{"locationId":{"inq":[]}}}`. (Agents do NOT carry organizationId directly; `deviceGroupId` is a global category, not the client.) ## Agent object (key fields) `id, hostname, name, os, osWindows/osLinux/osOsx/osOther, version, ip/ipPub, active (online), heartbeat, isolated (containment state), dattoAvEnabled, markedForUninstall, markedForUpdate, locationId, deviceGroupId, deviceId, authorized, eppData, rwdInfo`. ## Alert object (key fields) `id, name, description, severity (0 info,1 low,2 medium,3 high,4 critical), mitreTactic, mitreId, hostname, organizationId, organizationName, targetGroupId, targetGroupName, deviceId, agentId, hostId, eventTime, createdOn, sourceType, sourceName, responseData, signed, managed, archived`. ## Reads (verified) | Op | Method/path | |---|---| | Tenant counts | `GET /Organizations/count`, `/Targets/count`, `/Agents/count`, `/Alerts/count` | | Organizations | `GET /Organizations` | | Locations (sites) | `GET /Locations` (filter `where organizationId`) | | Scan target groups | `GET /Targets` | | Agents | `GET /Agents` (filter `where locationId` / `inq`) | | Agent detail | `GET /Agents/{id}` | | Alerts | `GET /Alerts` (filter `where` org/severity/createdOn) | | Alert detail | `GET /Alerts/{id}` | | Extensions | `GET /Extensions` | | Agent reg keys | `GET /agentKeys` (each is `{id:, targetId}`) | | Tasks (scan jobs) | `GET /userTasks` (filter `where type`, e.g. `"Scan - EDR"`) | | Task detail | `GET /userTasks/{id}` | ## Mutating (VERIFIED LIVE 2026-06-25; gate behind --confirm) The Infocyte-module scan routes (`targets/{id}/scan`, `targets/scan`, `scans`) are **DEAD (404)**. The live mechanism (read from the console JS bundle + run live) is a single endpoint selecting agents by a LoopBack `where`: | Op | Method/path | Body | |---|---|---| | **Scan agent(s)** | `POST /Agents/scan` | `{"where":{"and":[{"id":["",...]}]}, "options":{}, "taskName":"Scan - EDR"}` | | **Response ext** (isolate/kill) | `POST /Agents/scan` | `{"where":{"and":[{"id":[...]}]}, "options":{"extensions":[{"id":""}]}, "taskName":"Response"}` | | **Cancel a scan/task** | `POST /userTasks/{id}/cancel` | — (204) | | **Create group** | `POST /Targets` | `{"name":"...","organizationId":"..."}` | | **Mint reg key** | `POST /agentKeys` | `{"id":"<10char,caller-supplied>","targetId":""}` | **CRITICAL footguns:** - The `where` is **REQUIRED**. An absent/empty `where` scans the **ENTIRE tenant** ("Scanning 156 hosts"). The CLI refuses an empty agent list. - Use the **AND-wrapped** form `{"where":{"and":[{"id":[...]}]}}`. A bare `{"where":{"id":{"inq":[...]}}}` returns **HTTP 412 "column reference id is ambiguous"** (the scan query joins tables; the `and`-wrap disambiguates). - Targeting is by **Agent.id** (not `deviceId`, which is null right after enroll). - `scanType` is a client-side UI enum, never sent. AV scans (`Scan - AV Quick/Full`) are **policy-driven, not callable** via this endpoint. - Sibling endpoints exist for broader scope: `POST /organizations/scan`, `/locations/scan`, `/locations/{id}/scan` (same `{where, options}` shape). **options** (EDR forensic toggles, all optional; empty `{}` is valid): `process, module, driver, memory, account, artifact, autostart, application, installed, hook, network, events`, plus `extensions:[{id,args,order}]`. `isolate` is shape-correct but RUN-unverified (cuts the endpoint off-network) — confirm the extension id via `GET /Extensions` (e.g. `Host Isolation [Win/Linux]`) and test on an ACG-internal box first. ## Deployment (not a REST call) The agent installs by running the binary on the endpoint: `agent.exe --key --url https://.infocyte.com`, or via the official PowerShell wrapper one-liner. **Pass the FULL `-URL`, not `-InstanceName `** — the install script's loose `.com` regex matches "zcom" inside `azcomp4587`, leaving `--url` empty (`a value is required for '--url'`): ``` Install-EDR -URL "https://azcomp4587.infocyte.com" -RegKey ``` To land an agent in a SPECIFIC group: `POST /Targets` (create group) → `POST /agentKeys` (mint key, caller-supplied `id`) → install with that key. Pull existing keys from `GET /agentKeys` (each `{id:, targetId}`). Push via GuruRMM `/rmm` or any remote-exec channel. Nothing ties install to Datto RMM. ## Liftable client `github.com/KaseyaDEDR/PowershellTools` — the `InfocyteHUNTAPI` module (Apache-2.0) is a complete REST wrapper and the de-facto API spec. `extension-docs` (Lua 5.3) documents the agent response actions (host isolation `isolator:isolate()`, process kill, quarantine). ## Webhooks (for the GuruRMM add-on) Admin → Webhooks → Add Webhook: EDR POSTs full alert JSON over HTTPS on each detection, with custom auth headers supported. This is the recommended near-real-time feed for a GuruRMM "EDR add-on" dashboard, complemented by REST polling here. (Not wired in this skill yet — Feature 6 work.)