Datto SaaS Protection API Patterns
Status note
The MCP server (datto-saas-protection-mcp) and SDK (@wyre-technology/node-datto-saas-protection) are in development.
Overview
Datto SaaS Protection (rebranded Backupify, now also folded together with Spanning under the same product family) provides cloud-to-cloud backup for Microsoft 365 and Google Workspace tenants.
Base URLs (per region):
https://api.datto.com/api/v1 (US)
https://api.eu.datto.com/api/v1 (EU)
The MCP server takes a region credential field (us or eu); never hard-code.
Anti-triggers
- Spanning — Kaseya's other cloud-to-cloud backup product covers
the same Microsoft 365 and Google Workspace workloads (plus
Salesforce) under separate credentials and a separate API; use
spanning-api-patterns. - Datto's appliance backup — SIRIS/Alto image backup is BCDR, a
different product and API; use
datto-bcdr-api-patterns.
Authentication
API key issued from the SaaS Protection partner portal:
- SaaS Protection portal → Settings → API → Create Key
- Optional: scope key to specific clients
- Send on every request:
Authorization: Bearer <api_key>
Keys are long-lived; rotate from the same UI.
Object model
Partner
└── Client (a customer organization)
└── Domain (M365 tenant / Google domain)
└── Seat (mailbox / OneDrive / SharePoint site / Google user)
└── Backup runs / restore points
Common endpoints
| Domain | Endpoint | Notes |
|---|---|---|
| Clients | GET /clients |
All client orgs |
| Domains | GET /clients/{clientId}/domains |
M365/Google tenants |
| Seats | GET /clients/{clientId}/domains/{domainId}/seats |
Active + archived |
| Single seat | GET /seats/{seatId} |
|
| Backup status | GET /seats/{seatId}/backups |
Most recent backup runs |
| Activity log | GET /clients/{clientId}/activity |
Org-level events |
| Restore (request) | POST /seats/{seatId}/restores |
Queue restore |
| Restore status | GET /restores/{restoreId} |
|
| License usage | GET /clients/{clientId}/usage |
Seat counts |
Pagination
Cursor-based:
GET /clients?limit=100
→ { items: [...], nextCursor: "abc123" }
GET /clients?limit=100&cursor=abc123
Default limit is 50, max is 250. Stop when nextCursor is missing or null.
Restore operations
Restores are async. The flow:
1. POST /seats/{seatId}/restores → { restoreId, status: "queued" }
2. Poll GET /restores/{restoreId} → status transitions queued → running → completed | failed
3. On completed, fetch the result location (URL or destination metadata)
Typical restore wall-clock: minutes to hours depending on data volume. Poll at 30-second intervals; do not poll faster.
Rate limits
60 req/min per API key under default policy. HTTP 429 includes Retry-After. Aggregate operations (list all seats across all clients) should use Promise.all with concurrency capped at 4.
Error handling
| HTTP | Meaning | Action |
|---|---|---|
| 200 | OK | |
| 400 | Bad request | Validate inputs |
| 401 | Bad / expired key | Rotate key |
| 403 | Key lacks scope on this client | Verify scope |
| 404 | Unknown seatId / clientId / domainId | |
| 409 | Restore already queued for this seat | Surface; offer to cancel-and-retry |
| 429 | Rate limited | Back off per Retry-After |
| 500-503 | Transient | Exponential backoff |
Gotchas
- Region selection is sticky: A US-region key cannot call EU endpoints. The error is a generic 401, not a 404 — surface a clear message asking the user to check the region credential.
- Archived seats: By default, list endpoints return only active seats. Pass
?includeArchived=trueto see seats whose source mailboxes were deleted but whose backups are retained. - Spanning vs. SaaS Protection: These are different products that share marketing branding. Spanning has its own API (
spanning-mcpplugin) — don't conflate keys. - Restore destination quirks: Restoring an M365 mailbox to an existing user requires Graph API permissions on the target tenant; the SaaS Protection API surfaces the dependency as a 400 once the restore starts running, not at queue time.
Related skills
When the build-out lands, expect domain skills for: clients, seats, restores, activity-logs, license-usage.