Help us improve
Share bugs, ideas, or general feedback.
From kaseya-datto-rmm
Lists, manages, and configures Datto RMM sites for client locations, covering structure, settings, proxy configuration, site variables, device assignment, and scoped operations.
npx claudepluginhub wyre-technology/msp-claude-plugins --plugin datto-rmmHow this skill is triggered — by the user, by Claude, or both
Slash command
/kaseya-datto-rmm:sitesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Sites in Datto RMM represent client organizations or locations. Each site contains devices, has its own settings, and can have site-level variables. Sites provide organizational hierarchy and enable scoped operations - alerts, jobs, and reports can all be filtered by site.
Manages RunZero sites: list/create/update sites, define scan scopes/exclusions, deploy explorers, organize assets by location or client.
Manages Hudu website records for SSL/TLS monitoring, email security (DMARC, DKIM, SPF), DNS records, and company linking. Covers CRUD, monitoring fields, and verification patterns.
Provides Datto RMM API v2 patterns: OAuth 2.0 authentication, 6-platform URLs, token lifecycle, pagination, rate limiting, error handling.
Share bugs, ideas, or general feedback.
Sites in Datto RMM represent client organizations or locations. Each site contains devices, has its own settings, and can have site-level variables. Sites provide organizational hierarchy and enable scoped operations - alerts, jobs, and reports can all be filtered by site.
Account
└── Sites (many)
└── Devices (many per site)
└── Alerts, Jobs, Audit Data
Sites can represent:
| Identifier | Type | Description |
|---|---|---|
siteUid | string | Globally unique identifier |
siteId | integer | Legacy numeric ID |
name | string | Display name |
interface Site {
// Identifiers
uid: string; // Unique site ID
siteId: number; // Legacy numeric ID
name: string; // Site display name
description?: string; // Site description
// Configuration
onDemand: boolean; // On-demand site (no scheduled tasks)
splapiEnabled: boolean; // Service Provider Level API enabled
proxySettings?: ProxySettings; // HTTP proxy configuration
// Counts
devicesCount: number; // Number of devices
openAlertsCount: number; // Active alerts
// Timestamps (Unix milliseconds)
createdAt: number; // When site was created
modifiedAt: number; // Last modification
// Settings
settings: SiteSettings;
}
interface ProxySettings {
enabled: boolean;
host: string;
port: number;
username?: string;
bypassList?: string[]; // Hosts to bypass proxy
}
interface SiteSettings {
autoPatchApproval: boolean;
patchWindow: PatchWindow;
notificationEmail?: string;
timezone: string;
}
interface PatchWindow {
dayOfWeek: number; // 0=Sunday, 6=Saturday
startHour: number; // 0-23
durationHours: number;
}
GET /api/v2/sites?max=250
Authorization: Bearer {token}
Response:
{
"sites": [
{
"uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Acme Corporation",
"description": "Main office",
"devicesCount": 45,
"openAlertsCount": 3,
"onDemand": false
}
],
"pageDetails": {
"count": 1,
"nextPageUrl": null
}
}
GET /api/v2/site/{siteUid}
Authorization: Bearer {token}
Response:
{
"uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"siteId": 12345,
"name": "Acme Corporation",
"description": "Main office - Downtown",
"devicesCount": 45,
"openAlertsCount": 3,
"onDemand": false,
"splapiEnabled": true,
"createdAt": 1680000000000,
"modifiedAt": 1707991200000,
"proxySettings": {
"enabled": false
},
"settings": {
"autoPatchApproval": false,
"timezone": "America/New_York"
}
}
GET /api/v2/site/{siteUid}/devices?max=250
Authorization: Bearer {token}
GET /api/v2/site/{siteUid}/alerts/open
Authorization: Bearer {token}
GET /api/v2/site/{siteUid}/alerts/resolved?max=250
Authorization: Bearer {token}
POST /api/v2/sites
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "New Client Site",
"description": "Client headquarters",
"onDemand": false
}
POST /api/v2/site/{siteUid}
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "Updated Site Name",
"description": "Updated description"
}
DELETE /api/v2/site/{siteUid}
Authorization: Bearer {token}
Warning: Deleting a site does not delete devices - they become unassigned.
async function findSiteByName(client, name) {
const response = await client.request('/api/v2/sites?max=250');
const sites = response.sites || [];
// Exact match first
const exact = sites.find(s =>
s.name.toLowerCase() === name.toLowerCase()
);
if (exact) return { found: true, site: exact };
// Partial match
const matches = sites.filter(s =>
s.name.toLowerCase().includes(name.toLowerCase())
);
if (matches.length === 0) {
return { found: false, suggestions: [] };
}
if (matches.length === 1) {
return { found: true, site: matches[0] };
}
return {
found: false,
ambiguous: true,
suggestions: matches.map(s => ({
name: s.name,
uid: s.uid,
deviceCount: s.devicesCount
}))
};
}
async function getSiteHealth(client, siteUid) {
const [site, devices, alerts] = await Promise.all([
client.request(`/api/v2/site/${siteUid}`),
client.request(`/api/v2/site/${siteUid}/devices?max=250`),
client.request(`/api/v2/site/${siteUid}/alerts/open`)
]);
const deviceList = devices.devices || [];
const alertList = alerts.alerts || [];
// Device status breakdown
const deviceStatus = {
online: deviceList.filter(d => d.status === 'online').length,
offline: deviceList.filter(d => d.status === 'offline').length,
total: deviceList.length
};
// Alert priority breakdown
const alertsByPriority = {
Critical: alertList.filter(a => a.priority === 'Critical').length,
High: alertList.filter(a => a.priority === 'High').length,
Moderate: alertList.filter(a => a.priority === 'Moderate').length,
Low: alertList.filter(a => a.priority === 'Low').length
};
// Calculate health score
const healthScore = calculateSiteHealthScore(deviceStatus, alertsByPriority);
return {
site: {
name: site.name,
uid: site.uid
},
devices: deviceStatus,
alerts: {
total: alertList.length,
byPriority: alertsByPriority
},
healthScore,
status: healthScore >= 80 ? 'healthy' : healthScore >= 50 ? 'warning' : 'critical'
};
}
function calculateSiteHealthScore(devices, alerts) {
let score = 100;
// Deduct for offline devices
const offlinePercent = (devices.offline / devices.total) * 100;
score -= offlinePercent * 0.5;
// Deduct for alerts
score -= alerts.Critical * 15;
score -= alerts.High * 5;
score -= alerts.Moderate * 2;
score -= alerts.Low * 0.5;
return Math.max(0, Math.round(score));
}
async function getAllSitesSummary(client) {
const response = await client.request('/api/v2/sites?max=250');
const sites = response.sites || [];
return sites.map(site => ({
name: site.name,
uid: site.uid,
devices: site.devicesCount,
openAlerts: site.openAlertsCount,
status: site.openAlertsCount === 0 ? 'healthy' :
site.openAlertsCount <= 5 ? 'warning' : 'critical'
})).sort((a, b) => b.openAlerts - a.openAlerts);
}
async function validateSiteSetup(client, siteUid) {
const site = await client.request(`/api/v2/site/${siteUid}`);
const devices = await client.request(`/api/v2/site/${siteUid}/devices?max=250`);
const variables = await client.request(`/api/v2/site/${siteUid}/variables`);
const checks = [];
// Check site has description
checks.push({
item: 'Site description',
status: site.description ? 'pass' : 'fail',
message: site.description || 'No description set'
});
// Check site has devices
checks.push({
item: 'Devices enrolled',
status: devices.devices?.length > 0 ? 'pass' : 'fail',
message: `${devices.devices?.length || 0} devices`
});
// Check critical variables are set
const requiredVars = ['BACKUP_PATH', 'ADMIN_EMAIL'];
requiredVars.forEach(varName => {
const v = variables.variables?.find(v => v.name === varName);
checks.push({
item: `Variable: ${varName}`,
status: v?.value ? 'pass' : 'fail',
message: v?.value || 'Not set'
});
});
return {
siteUid,
siteName: site.name,
checks,
passed: checks.filter(c => c.status === 'pass').length,
total: checks.length
};
}
| Error | Status | Cause | Resolution |
|---|---|---|---|
| Site not found | 404 | Invalid siteUid | Verify site exists |
| Name already exists | 400 | Duplicate site name | Use unique name |
| Cannot delete | 400 | Site has devices | Move devices first |
| Permission denied | 403 | API restrictions | Check permissions |
async function safeSiteOperation(client, operation, siteUid, data) {
try {
switch (operation) {
case 'get':
return await client.request(`/api/v2/site/${siteUid}`);
case 'update':
return await client.request(`/api/v2/site/${siteUid}`, {
method: 'POST',
body: JSON.stringify(data)
});
case 'delete':
// Check for devices first
const devices = await client.request(`/api/v2/site/${siteUid}/devices`);
if (devices.devices?.length > 0) {
throw new Error(`Cannot delete site with ${devices.devices.length} devices`);
}
return await client.request(`/api/v2/site/${siteUid}`, {
method: 'DELETE'
});
}
} catch (error) {
if (error.status === 404) {
return { error: 'Site not found', siteUid };
}
throw error;
}
}
Recommended Format: {ClientName} - {Location/Purpose}
Examples:
Acme Corp - Main OfficeAcme Corp - Remote WorkersTechStart Inc - Data CenterInternal - IT Department