---
name: msp360
description: "Manage ACG's MSP360 Managed Backup Service (MBS) tenant via the provider REST API: the authoritative fleet backup-status source (who is backing up, last run, failures, stale plans) plus user/company/license provisioning. Reads run freely; writes (create/delete user or company, grant/release/revoke license) are gated --confirm. Triggers: msp360, mspbackups, managed backup, is X backing up, backup status, backup failures, create backup user, add backup company, grant backup license."
---

# MSP360 Managed Backup Service Skill

Live client for ACG's **MSP360 Managed Backup Service (MBS)** provider tenant. This is the
**authoritative answer to "is client X actually backing up"** — the monitoring feed reports
every backup plan's last run, status, and data across the whole fleet. B2/Seafile/ownCloud
are just where the data lands; MSP360 is where you see whether a backup *ran*.

Built as a sibling to the `b2` / `seafile` / `owncloud` / `datto-workplace` backup skills;
shares `.claude/scripts/backup_common.py` for repo-root/vault/HTTP plumbing.

## Running

```bash
MSP=".claude/skills/msp360/scripts/msp360.py"

# --- reads (free) ---
py "$MSP" status                              # reachable + counts + plans needing attention
py "$MSP" monitoring                          # every plan: company/computer/status/last-run/data
py "$MSP" monitoring --failed                 # only plans NOT in a clean state (the triage view)
py "$MSP" monitoring --company Dataforth       # scope to one client
py "$MSP" monitoring --stale 30               # plans whose last run is >=30 days old (or never)
py "$MSP" companies                           # 37 companies with ids + storage limits
py "$MSP" users --company "Business Services"  # backup users (+ space used) in a company
py "$MSP" licenses --available                 # free vs taken licenses
py "$MSP" billing --json                       # billing feed
py "$MSP" raw GET /api/Users/{id}              # generic passthrough for any GET endpoint

# --- writes (require --confirm; without it you get a dry-run preview) ---
py "$MSP" create-company --name "New Client LLC" --confirm
py "$MSP" create-user --email backup@client.com --company "New Client LLC" \
       --first Jane --last Doe --notify admin@acg.com --confirm
py "$MSP" grant-license   --user-id <UUID> --license-id <UUID> --confirm
py "$MSP" release-license --user-id <UUID> --confirm
py "$MSP" delete-user  --id <UUID> [--keep-data] --confirm
py "$MSP" delete-company --id <UUID> --confirm
py "$MSP" raw POST /api/Accounts/CreateDestination --data '{...}' --confirm
```

Add `--json` to any read for machine-readable output. `--base` overrides the API URL.

## Credentials

Loaded at runtime from the SOPS vault (never hardcoded):
**`msp-tools/msp360-api.sops.yaml`** -> `credentials.login` / `credentials.password`.
These are the **API-specific** creds
generated in the MSP360 console (Settings > General) by the main admin account — NOT the
console login. Auth flow: `POST /api/Provider/Login {UserName, Password}` -> Bearer
`access_token` (per-run, never cached to disk) -> `Authorization: Bearer <token>` on every
request. Base: `https://api.mspbackups.com`.

## What "backing up?" looks like

`monitoring` returns one row per backup plan with `Status` (0/1=Success, 2=Warning,
3=Failed, 4=Running, 6=Interrupted, 7=Completed-with-warnings), `LastStart`, `DataCopied`,
`CompanyName`, `ComputerName`, `PlanName`. Key reads:

- **A company with 0 rows in `monitoring` is not backing up** even if it exists (this is how
  the GPS audit found Arizona Medical Transit + T&C Sorensen billed-but-unprotected).
- `--failed` surfaces failing/warning plans; `--stale N` surfaces plans that stopped running.
- A B2 bucket with files does NOT prove a recent backup — always confirm with `monitoring`.

## Writes & safety

- **Every write is gated `--confirm`.** Without it the command prints a `[DRY-RUN]` line (and,
  for create-*, the exact JSON payload) and changes nothing.
- `delete-user` defaults to removing metadata **and all backup data**; pass `--keep-data` to
  drop only the metadata (`DELETE /api/Users/{id}/Account`) and retain the backups.
- `delete-company` / `delete-user` print a `[WARNING]` describing the blast radius first.
- create-user / grant-license payloads are built from the live API schema (User fields:
  Email/FirstName/LastName/Company/Enabled/NotificationEmails; grant: UserID + LicenseID).
  On first real provisioning, eyeball the API's response — it returns the created object or a
  validation error, which the command prints verbatim.

## Full endpoint surface (via `raw` when a typed command doesn't exist)

The MBS API groups (all reachable through `raw <METHOD> <path>`): **Provider** (login),
**Users** (GET/POST/PUT, DELETE `/{id}` or `/{id}/Account`), **Companies** (GET/POST/PUT/DELETE),
**Monitoring** (GET), **Licenses** (GET, POST Grant/Release/Revoke), **Destinations**
(GET/POST/PUT/DELETE), **Accounts** (GET/POST/PUT + Add/Create/Edit/RemoveDestination),
**Packages** (GET/POST/PUT/DELETE), **Billing** (GET/PUT), **Builds** (GET, POST
RequestCustomBuilds), **Administrators** (GET/POST/PUT/DELETE). Reference:
`https://help.mspbackups.com/mbs-api-specification/methods`.

## Notes / gotchas

- **`/api/Computers` returns HTTP 400 "Remote Management API methods are not enabled"** —
  that feature is off on this account. `computers` prints a clean [INFO] and points you at
  `users` + `monitoring`, which cover endpoint/backup data without it.
- **Git-Bash path mangling:** a leading-slash `raw` path like `/api/Users` gets rewritten to
  `C:/Program Files/Git/api/Users` by MSYS. The `raw` command auto-recovers the `/api/...`
  tail, so pass the path normally.
- MSP360 CompanyName is the join key to B2 buckets (e.g. `ACG-PST`=Peaceful Spirit,
  `ACG-TCA`=Tucson Coin and Autograph) — useful when reconciling destinations.
- Backup *targets* are a per-client decision: report mismatches, don't provision jobs off a
  billing count. See memory `feedback_backup_targets_never_guessed` +
  `reference_msp360_backup_monitoring`.
- Failures are logged to the fleet `errorlog.md` via the canonical helper; expected
  conditions (feature-gated endpoint, no matching rows) are not.
