- migration 062: mimecast_tenants table (company_id, client_id/secret, account_code) - Seed Wulf (CUSA13A95) + Seubert (CUSA96A181) tenants - MimecastClient.getHeldMessages(): full pagination via meta.pagination.next cursor (API always returns 10/page regardless of pageSize param, totalCount in meta) - getMimecastClientForTenant() factory for per-tenant instantiation - GET /api/mimecast/held?tenantId=&recipient= — fetches all tenants in parallel, merges + sorts by date, returns per-tenant counts + combined messages[] - Held Mail tab on /admin/sync/mimecast (on-demand load, recipient filter, tenant badges, policy filter dropdown, DMARC/impersonation highlighted red)
10 KiB
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 1–10 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_udf1–device_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
-
Auth URL is different from API URL — token is fetched from
concord-api.centrastage.net/auth/..., API calls go toconcord-api.centrastage.net/api/v2/... -
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.
-
uidvsid— devices and sites have both an integeridand a GUIDuid. The REST API usesuidin paths for most operations. Quick jobs requiredeviceUid(the GUID), not the integer ID. -
UDFs in webhooks vs REST API — The REST API returns UDFs as
udf: { udf1: "...", udf2: "..." }(nested object, up to 10). Webhooks flatten them todevice_udf1throughdevice_udf29as top-level fields (29 total). -
Alert context varies by type — always check
alertContext['@class']to know what fields are available. For PING alerts, fetch/alert/{uid}to getinstanceName(the ping target) since it's not in the bulk alert list response. -
Page size 250 is the effective maximum — don't request more.
-
Resolved alerts grow without bound — never fetch all resolved alerts in production. Limit to recent pages (e.g. 4 pages = ~1000 most recent).
-
Site → Company mapping —
site.autotaskCompanyIdis a string from the API even though it's an integer ID. AlwaysparseInt()it and validate> 0and!isNaN()before using as a FK.
Quick Reference: Alert Priority Values
CriticalHighModerateLowInformation
Quick Reference: Common Alert Types
PING— ping monitor failureDISK— disk space/healthCPU— CPU utilizationMEMORY— RAM utilizationSERVICE— Windows service downEVENTLOG— Windows event log matchPATCH— patch managementANTIVIRUS— 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.