wulfclaw/DATTO_RMM_API_GUIDE.md
Claude 368a85d48a docs: add Datto RMM API guide for openclaw
Comprehensive reference covering auth (OAuth2 password grant), all key
endpoints, pagination, DB schema, webhook payload shape, common gotchas,
and pipeline integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:47:19 -04:00

10 KiB
Raw Blame History

Datto RMM API Guide for openclaw

Everything learned from building the Datto RMM integration in Pulse. Use this as a reference for any future work touching the Datto RMM API.


Authentication

Datto RMM API v2 uses OAuth2 with password grant type — not a simple API key header.

  • Auth endpoint: https://concord-api.centrastage.net/auth/oauth/token
  • Base API URL: https://concord-api.centrastage.net/api/v2
  • Client credentials: always public-client:public (this is the public OAuth client — not your credentials)
  • Your credentials: API Access Key = username, API Secret Key = password
POST https://concord-api.centrastage.net/auth/oauth/token
Authorization: Basic cHVibGljLWNsaWVudDpwdWJsaWM=   (base64 of "public-client:public")
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=YOUR_API_KEY&password=YOUR_API_SECRET

Response returns access_token (bearer token). Tokens last ~1 hour; refresh proactively at 50 minutes.

Environment variables used in Pulse:

DATTO_RMM_API_URL=https://concord-api.centrastage.net
DATTO_RMM_API_KEY=<your API access key>
DATTO_RMM_API_SECRET=<your API secret key>
DATTO_RMM_WEBHOOK_SECRET=<shared secret for webhook validation>

Making API Calls

All requests go to https://concord-api.centrastage.net/api/v2{endpoint} with:

Authorization: Bearer {access_token}
Content-Type: application/json
Accept: application/json

The response body is always JSON. Always parse it — the API never returns empty 200s (except for write operations where you should guard with text ? JSON.parse(text) : {}).


Key Endpoints

Sites

GET  /account/sites                  → { sites: [...], pageDetails: {...} }
GET  /site/{siteUid}/devices         → { devices: [...] }

Sites map to Autotask companies via autotaskCompanyId / autotaskCompanyName fields on the site object. This is the primary join key between the two systems.

Devices

GET  /account/devices                → { devices: [...], pageDetails: {...} }
GET  /devices/{deviceId}             → { item: DattoRMMDevice }
GET  /device/{deviceId}/auditdata    → detailed hardware info (bios, processors, memory, disks)

Default page size is 250. Always paginate using pageDetails.nextPageUrl — follow it until null.

Alerts

GET  /account/alerts/open            → { alerts: [...], pageDetails: {...} }
GET  /account/alerts/resolved        → { alerts: [...], pageDetails: {...} }
GET  /alert/{alertUid}               → single alert with full alertContext

Resolved alerts can be huge — cap page fetches (e.g. 4 pages) rather than fetching all.

For PING alerts, alertContext['@class'] === 'ping_ctx' and alertContext.instanceName holds the ping target hostname.

Components & Quick Jobs

GET  /account/components             → { components: [...] }   (automation scripts/tasks)
PUT  /device/{deviceUid}/quickjob    → run a component on a device
GET  /job/{jobUid}/results/{deviceUid} → job output/results

Quick job payload:

{
  "jobName": "My Job Name",
  "jobComponent": {
    "componentUid": "abc-123-...",
    "variables": [
      { "name": "VAR_NAME", "value": "value" }
    ]
  }
}

Pagination Pattern

The API uses pageDetails.nextPageUrl for cursor-based pagination. Always follow it:

let url: string | null = `https://concord-api.centrastage.net/api/v2/account/sites?pageSize=250`;
while (url) {
  const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  const body = await resp.json();
  items.push(...(body.sites || []));
  url = body.pageDetails?.nextPageUrl ?? null;
}

Some older endpoints use ?page=1&pageSize=250 query params — check which pattern each endpoint uses. The fetchAllPages helper in datto-rmm-client.ts handles the nextPageUrl style.


Data Model (PostgreSQL Tables)

datto_rmm_sites

Column Notes
id Datto integer ID (PK)
uid Datto GUID string (unique)
name Site display name
autotask_company_id FK → companies(id) — join key
autotask_company_name Denormalized for display
number_of_devices Online/offline counts from API
portal_url Deep link to Datto portal

datto_rmm_devices

Column Notes
id Datto integer ID (PK)
uid Datto GUID (unique, used for API calls and quick jobs)
site_id FK → datto_rmm_sites(id)
hostname Device hostname
online / suspended / deleted Status flags
device_type_category e.g. "Desktop", "Server", "Laptop"
operating_system OS string
antivirus_product / antivirus_status AV info
patch_status Patch management summary
patches_approved_pending Count of pending patches
last_seen Timestamp (ms epoch from API, stored as TIMESTAMPTZ)
web_remote_url Direct remote session URL
udf JSONB — User Defined Fields 110 from API

datto_rmm_alerts

Column Notes
alert_uid PK (Datto GUID string)
device_uid / device_name Source device
site_uid / site_name Source site
priority "Critical", "High", "Moderate", "Low", "Information"
alert_context JSONB — varies by alert type, contains @class discriminator
resolved Boolean
resolved_on Timestamp
muted Boolean
ticket_number Linked Autotask ticket if any
alert_category e.g. "Patch Management"
alert_type e.g. "PING", "DISK", "CPU"
alert_message_en Human-readable alert message
device_udf1device_udf29 All 29 UDF fields from webhook payloads
ping_target Resolved from API for PING alerts
triggered Raw webhook "True"/"False" string

datto_rmm_webhook_logs

Raw capture table — stores every incoming webhook payload verbatim for inspection before processing.


Webhooks

Datto RMM can POST alert events to your endpoint when alerts fire or resolve.

Webhook receiver: POST /api/webhooks/datto-rmm

Authentication: Datto sends a shared secret in the X-Datto-Webhook-Secret header. Validate it against DATTO_RMM_WEBHOOK_SECRET env var.

Alert webhook payload shape (flat JSON, not nested like the REST API):

{
  "alert_uid": "abc-123-...",
  "triggered": "True",          // "True" = alert fired, "False" = resolved
  "alert_type": "PING",
  "alert_category": "Networking",
  "alert_priority": "Critical",
  "alert_message_en": "Ping monitor failed for ...",
  "device_uid": "...",
  "device_hostname": "SERVER01",
  "device_ip": "10.0.0.1",
  "device_os": "Windows Server 2019",
  "device_description": "...",
  "device_id": "12345",
  "site_uid": "...",
  "site_name": "ACME Corp",
  "site_id": "678",
  "platform": "Windows",
  "last_user": "DOMAIN\\user",
  "device_udf1": "...",
  // ... device_udf2 through device_udf29
}

Key gotcha: triggered === "True" means the alert is active (not resolved). triggered === "False" means it resolved. Map triggered === "False"resolved = true.

Always return HTTP 200 even on errors — Datto will disable your webhook endpoint if it receives repeated non-200 responses.


Sync Architecture in Pulse

The sync pipeline runs in order: sites → devices → open_alerts → resolved_alerts

Sites must sync before devices (FK constraint). The sync service guards against orphaned FK refs by pre-fetching known IDs and setting FK fields to null when the parent doesn't exist yet.

Timestamps from the Datto API come as millisecond epoch integers. Convert with new Date(milliseconds) before storing in PostgreSQL.

The factory singleton (datto-rmm-factory.ts) is the standard way to get a client instance in API routes:

import { getDattoRMMClient } from '@/lib/services/datto-rmm-factory';
const client = getDattoRMMClient();

Common Gotchas

  1. Auth URL is different from API URL — token is fetched from concord-api.centrastage.net/auth/..., API calls go to concord-api.centrastage.net/api/v2/...

  2. Parse auth response carefully — if auth fails, the server may return HTML (an error page) instead of JSON. Always try/catch the JSON.parse and log the raw text on failure.

  3. uid vs id — devices and sites have both an integer id and a GUID uid. The REST API uses uid in paths for most operations. Quick jobs require deviceUid (the GUID), not the integer ID.

  4. UDFs in webhooks vs REST API — The REST API returns UDFs as udf: { udf1: "...", udf2: "..." } (nested object, up to 10). Webhooks flatten them to device_udf1 through device_udf29 as top-level fields (29 total).

  5. Alert context varies by type — always check alertContext['@class'] to know what fields are available. For PING alerts, fetch /alert/{uid} to get instanceName (the ping target) since it's not in the bulk alert list response.

  6. Page size 250 is the effective maximum — don't request more.

  7. Resolved alerts grow without bound — never fetch all resolved alerts in production. Limit to recent pages (e.g. 4 pages = ~1000 most recent).

  8. Site → Company mappingsite.autotaskCompanyId is a string from the API even though it's an integer ID. Always parseInt() it and validate > 0 and !isNaN() before using as a FK.


Quick Reference: Alert Priority Values

  • Critical
  • High
  • Moderate
  • Low
  • Information

Quick Reference: Common Alert Types

  • PING — ping monitor failure
  • DISK — disk space/health
  • CPU — CPU utilization
  • MEMORY — RAM utilization
  • SERVICE — Windows service down
  • EVENTLOG — Windows event log match
  • PATCH — patch management
  • ANTIVIRUS — AV status

Pipeline Integration

In Pulse, incoming Datto RMM webhooks can trigger the pipeline engine (fire-and-forget):

pipelineEngine.processTrigger('datto_rmm', payload).catch(err =>
  console.error('[DATTO-RMM-WEBHOOK] Pipeline processing error:', err)
);

The trigger type 'datto_rmm' matches pipeline rules configured in the admin UI. Pipelines can take actions like creating Autotask tickets, sending notifications, etc.