syncro: expand skill to full Syncro API surface (assets/RMM/deploy) + verified capability model

Add comprehensive Syncro coverage beyond PSA core:
- New .claude/standards/syncro/api-reference.md: complete verified inventory of ~180
  endpoints across 38 resource types (generated from live OpenAPI 3.0 spec + tenant
  probe 2026-07-10), with worked GET/POST/PUT/DELETE templates and token-capability matrix.
- /syncro: asset read intelligence (patches, installed_applications), asset create/update,
  policy-folder move (move-asset), RMM alerts, and deploy-agent (hybrid installer push via
  GuruRMM using SyncroSetup --console --allow-force-reboot).
- move-asset ships a capability preflight (GET /policy_folders?customer_id -> 401 = missing
  Assets-Policy-Change) + mandatory post-write verify, because an under-scoped token returns
  HTTP 200 and silently no-ops the move.

Correct the "Syncro RMM is API-impossible" belief: it was a token-scope gap, not an API
limit. Live-verified the asset move (flip-and-restore 692253->692278->692253). Token scope
today: Howard + Winter full; Mike (vaulted ...ebbeb3) still 401 pending re-vault.

Corrects memory reference-syncro-rmm-api-gui-only; correction logged to errorlog.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 15:45:26 -07:00
parent 46d2ae7a5b
commit 1f56098652
5 changed files with 629 additions and 26 deletions

View File

@@ -18,6 +18,12 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
/syncro estimate <customer> <subject> Create ticket + linked estimate with line items and private purchase notes
/syncro schedules <customer> List recurring invoice schedules for a customer
/syncro schedule <id> View a schedule's template and line items
/syncro assets <customer> List a customer's RMM assets (read)
/syncro asset <id> Asset detail + patches + installed applications
/syncro asset-update <id> Rename / fix serial / edit an asset (preview-gated)
/syncro move-asset <id> <folder> Move asset to a policy folder (needs token scope - see below)
/syncro rmm-alerts [<customer>] List live RMM alerts; mute/clear by id
/syncro deploy-agent <customer> <host> Push the Syncro RMM agent to a machine via GuruRMM
```
## API Configuration
@@ -26,6 +32,10 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
**API Key:** per-user tokens in SOPS vault — see "Get API key" below
**Rate limit:** 180 requests/minute per IP
**Docs:** https://api-docs.syncromsp.com/
**Full endpoint catalog:** `.claude/standards/syncro/api-reference.md` — every Syncro call
(~180 endpoints, 38 resource types), verified against the live spec + tenant 2026-07-10.
This file documents the core PSA + asset/RMM/agent-deploy workflows; the reference covers
the rest (contacts, contracts, leads, vendors, POs, products, payments, wiki, policy folders).
## Hard Rules (violations have occurred — no exceptions)
@@ -599,26 +609,156 @@ COMMENT_ID=$(echo "$COMMENT_RESP" | jq -r '.comment.id')
- Do NOT wrap the payload in `{"comment": {...}}` — returns 422.
- **If `COMMENT_ID` is null:** GET `/tickets/{id}` and check `.ticket.comments[]` by subject before doing anything else. Comments cannot be deleted via API — duplicates require manual GUI removal.
#### Customer Assets (retire / archive — API GAP)
#### Customer Assets (RMM) — read intelligence + write management
**Retiring a device in Syncro = "Archive" (UI ONLY — no REST API).** Verified against
Syncro's own docs 2026-07-08 (`docs.syncrosecure.com/assets-rmm/archive-assets`): archiving
is done in the web UI only — asset Details page → **Actions → Archive**, or bulk via the
Assets table checkboxes → **Bulk Actions → Archive**. There is **no** archive endpoint and
**no** `archived` field on `PUT /customer_assets/{id}`. Do NOT try to archive via API, and
do NOT substitute `DELETE /customer_assets/{id}` — delete is destructive/irreversible and
loses history + breaks ticket linkages (Archive keeps the asset visible on its tickets/alerts).
Assets are Syncro's RMM device records. Reads are free; writes follow the standard
preview+confirm gate. Response shape is `{"asset": {...}}` (single) / `{"assets": [...]}`
(list). RMM telemetry lives under `.asset.properties.kabuto_information`.
- **To retire assets: hand the user the asset list + the Bulk-Actions-→-Archive steps.** We
cannot do it for them via API.
- Asset READS are fine: `GET /customer_assets?customer_id=N[&query=<name>]` (list, 100/page,
`.assets[]`) and `GET /customer_assets/{id}` (detail; `.asset` — RMM data lives under
`.asset.properties.kabuto_information`). `updated_at` is NOT a reliable "last online" (bulk
account touches move it) — use the ScreenConnect `GuestInfoUpdateTime` or kabuto data for
real last-seen.
- **[RESEARCH]** Re-check if Syncro ships an archive API (watch release notes). If/when it
does, wire it in here and add `--confirm` gating. Until then this is a "cannot be performed
via skill" function. (First hit: IMC retirement pass 2026-07-08.)
**Reads (verified with per-user token 2026-07-10):**
```bash
# List / search a customer's assets (100/page, .assets[])
curl -s "${BASE}/customer_assets?customer_id=${CUST_ID}&per_page=100&api_key=${API_KEY}" | \
tr -d '\000-\037' | jq -r '.assets[] | "\(.id)\t\(.asset_type_name)\t\(.name)"'
# Asset detail
curl -s "${BASE}/customer_assets/${ASSET_ID}?api_key=${API_KEY}" | tr -d '\000-\037' | \
jq '{id:.asset.id, name:.asset.name, customer_id:.asset.customer_id, policy_folder_id:.asset.policy_folder_id, serial:.asset.asset_serial}'
# Windows patch posture -> {available_patches, available_patches_meta, installed_patches}
curl -s "${BASE}/customer_assets/${ASSET_ID}/patches?api_key=${API_KEY}" | tr -d '\000-\037' | \
jq '{available: (.available_patches|length), installed: (.installed_patches|length)}'
# Installed software inventory (paginated) -> {installed_applications, meta}
# Use this to VERIFY an install (e.g. after /syncro deploy-agent) — see Agent Deployment.
curl -s "${BASE}/customer_assets/${ASSET_ID}/installed_applications?per_page=100&api_key=${API_KEY}" | \
tr -d '\000-\037' | jq -r '.installed_applications[]?.name' | sort -u
```
`updated_at` is NOT a reliable "last online" (bulk account touches move it) — use the
ScreenConnect `GuestInfoUpdateTime` or kabuto data for real last-seen.
**Writes — `POST /customer_assets` (create), `PUT /customer_assets/{id}` (update).**
`name` is **required** on both (422 without it). Accepted body fields: `name`,
`asset_serial`, `asset_type_id`, `asset_type_name`, `customer_id`, `properties`. Preview the
full payload and confirm before writing; post a bot alert after. Response `{"asset": {...}}`.
```bash
# Update (rename / fix serial). Always resend name (required) even when only changing serial.
curl -s -X PUT "${BASE}/customer_assets/${ASSET_ID}?api_key=${API_KEY}" \
-H "Content-Type: application/json" --data-binary @- <<JSON
{"name": "${NEW_NAME}", "asset_serial": "${NEW_SERIAL}"}
JSON
```
**Move to a policy folder — `PUT /customer_assets/{id}` with `policy_folder_id` — WORKS, but
is TOKEN-SCOPE-GATED and MUST be capability-checked + verified.** Live-proven 2026-07-10 via
flip-and-restore on ACG-internal asset `12335235` (`692253 → 692278 → 692253`) with Howard's
full-scope token. The spec: *"Updating only `policy_folder_id` requires **Assets - Policy
Change**; with other fields requires **Assets - Edit** AND **Assets - Policy Change**. Nil,
nonexistent, or cross-customer folder IDs return 422."* `name` is required on the PUT, so a
move always sends other fields → needs BOTH scopes.
**The failure mode is SILENT, so never trust the 200.** A token WITHOUT Policy-Change returns
`HTTP 200` and simply does not change the folder (this is the symptom once misread as "RMM is
GUI-only"). `/me` reports coarse `asset:{write:true}` even without Policy-Change, so it cannot
detect this — you must probe and verify:
```bash
move_asset() { # $1=asset_id $2=target_folder_id (needs $BASE $API_KEY)
local AID="$1" TARGET="$2" NAME CUST CUR
local A; A=$(curl -s "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" | tr -d '\000-\037')
NAME=$(echo "$A" | jq -r '.asset.name'); CUST=$(echo "$A" | jq -r '.asset.customer_id')
CUR=$(echo "$A" | jq -r '.asset.policy_folder_id')
# 1) CAPABILITY PREFLIGHT — 200 = token can move; 401 = lacks Assets - Policy Change
local PC; PC=$(curl -s -o /dev/null -w '%{http_code}' "${BASE}/policy_folders?customer_id=${CUST}&api_key=${API_KEY}")
if [ "$PC" = "401" ]; then
echo "[ERROR] This token lacks 'Assets - Policy Change' — cannot move policy folders."
echo " Options: move it in the Syncro web console, OR have a user whose token has"
echo " the scope run it (do NOT silently swap keys — writes attribute to the owner)."
return 1
fi
# 2) PUT (name required), then 3) VERIFY the value actually changed — a 200 no-op = failure
curl -s -X PUT "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" -H "Content-Type: application/json" \
--data-binary "$(jq -nc --arg n "$NAME" --argjson pf "$TARGET" '{name:$n, policy_folder_id:$pf}')" >/dev/null
local NEW; NEW=$(curl -s "${BASE}/customer_assets/${AID}?api_key=${API_KEY}" | tr -d '\000-\037' | jq -r '.asset.policy_folder_id')
if [ "$NEW" = "$TARGET" ]; then echo "[OK] asset ${AID} moved ${CUR} -> ${NEW}"; else
echo "[ERROR] move NOT applied (still ${NEW}) — token likely under-scoped; do it in the console."; return 1; fi
}
```
**Policy-folder route quirks (verified 2026-07-10):** `GET /policy_folders` (bare) → **404**;
`GET /policy_folders?customer_id=N` → **200** (list a customer's folders — how you enumerate
folder IDs); `GET /policy_folders/{id}` → **401** even with full scope. Find valid same-customer
folder IDs from the `?customer_id=N` list or from other assets' `policy_folder_id` values.
**Fleet posture:** Howard's per-user token now has full scope (verified). Other per-user tokens
may not — the preflight above handles that per-run. Recommend granting every per-user Syncro
token the Policy-Change scope so `move-asset` behaves the same regardless of who runs it (Mike's
call — least-privilege alternative is to leave the preflight to degrade gracefully). Corrects
memory `reference-syncro-rmm-api-gui-only`; full detail in `.claude/standards/syncro/api-reference.md`.
**Archive (retire) is still GUI-only — no REST API.** Verified 2026-07-08: archiving is web
UI only (asset Details → **Actions → Archive**, or Assets table → **Bulk Actions →
Archive**). There is no archive endpoint and no `archived` field on the PUT. Do NOT substitute
`DELETE /customer_assets/{id}` — delete is destructive/irreversible, loses history, and breaks
ticket linkages (Archive keeps the asset on its tickets/alerts). To retire: hand the user the
asset list + the Bulk-Actions-→-Archive steps.
#### RMM Alerts
Live RMM alert feed (`GET /rmm_alerts` -> `{rmm_alerts, meta}`, verified 2026-07-10). Reads
free; `POST /rmm_alerts/{id}/mute` and `DELETE /rmm_alerts/{id}` (clear) are gated writes.
```bash
curl -s "${BASE}/rmm_alerts?per_page=50&api_key=${API_KEY}" | tr -d '\000-\037' | \
jq -r '.rmm_alerts[] | "\(.id)\t\(.created_at)\t\(.description // .kind)"'
# Mute: POST /rmm_alerts/${ALERT_ID}/mute | Clear: DELETE /rmm_alerts/${ALERT_ID}
```
#### Agent Deployment (`/syncro deploy-agent`) — hybrid via GuruRMM
Push the Syncro RMM agent to a machine. Syncro's API has **no** installer-download endpoint,
so this is a **hybrid**: the per-policy-folder installer (which embeds the customer enrollment
token) is obtained once from the Syncro GUI and vaulted; **GuruRMM (`/rmm`)** does the actual
push (it runs PowerShell as SYSTEM on the endpoint). `/rmm diagnose` already *detects* a
Syncro agent as a competitor-RMM check — this is the deploy half.
**Prerequisites (one-time per customer):**
1. In Syncro: **Assets & RMM → Downloads** (or the target Policy Folder) → copy the
`SyncroSetup-<company>-<token>.exe` **download URL**. It is per-policy-folder — the token
binds the installed agent to that customer + policy. Do NOT reuse one customer's URL for
another.
2. Vault it via the `vault` skill at `clients/<slug>/syncro-agent-installer.sops.yaml`
(`credentials.installer_url`). Never hardcode the token URL.
**Deploy workflow:**
1. Resolve the target host in GuruRMM (`/rmm` — resolve hostname → UUID; confirm `os_type`
is `windows` and it is `is_connected`). Confirm the customer↔host mapping with the user.
2. Preview the exact install command + target, get explicit confirmation.
3. Dispatch via GuruRMM (`command_type: powershell`, SYSTEM context). The agent installs
**silently by default**; `--console` runs it non-interactively and `--allow-force-reboot`
permits the reboot Syncro triggers if it must update .NET first:
```powershell
$url = '<vaulted SyncroSetup URL>'
$exe = "$env:TEMP\SyncroSetup.exe"
Invoke-WebRequest -Uri $url -OutFile $exe -UseBasicParsing
Start-Process -FilePath $exe -ArgumentList '--console','--allow-force-reboot' -Wait
```
(Refs: Syncro deploy docs `docs.syncrosecure.com/agents-alerts-automations/deploy-an-endpoint`;
community "Command Line install".) `--allow-force-reboot` CAN reboot the endpoint — treat
deploy-agent as an outward-facing action: confirm reboot tolerance with the user first.
4. **Verify** the agent enrolled: after ~2-3 min, either re-query GuruRMM output, or confirm
the new asset appears under the customer via `GET /customer_assets?customer_id=N&query=<host>`
and/or that "Syncro" shows in the endpoint's `installed_applications`.
5. Bot alert (RMM write → `[RMM]`/`[DEPLOY]` routes to #dev-alerts; Syncro-side note to
#bot-alerts if an asset record was created).
**[VERIFY before first production use]** Confirm `--console --allow-force-reboot` against the
current Syncro installer on one ACG-internal test machine before rolling to a customer — the
switches are stable historically but the installer is versioned.
#### Customers

View File

@@ -17,7 +17,7 @@
- [ACG Office Network Infrastructure](infra_office_network.md) — IPs/hosts/roles for pfSense/Jupiter/VMs/Docker. Check before assuming; .21 (Uranus) is storage.
- [Power Failure Runbook](../POWER_FAILURE_RUNBOOK.md) — Recovery order after a power event: Tailscale routes, libvirt/VMs, Seafile, NPM/DNS.
- [Syncro API — Invoice Verification Pattern](syncro_invoice_verification_pattern.md) — /invoices?customer_id=X returns no ticket linkage; query /invoices/{number} for ticket_id. Compare by ticket ID, not number.
- [Syncro RMM policies = API-impossible](reference_syncro_rmm_api_gui_only.md) — policy create/assign/folder-move is GUI-ONLY; `policy_folder_id` is read-only on PUT (live-proven), policy endpoints 404, /policy_folders 401 scope-gated. Don't build /syncro move-asset; use `bitdefender` for API policy work.
- [Syncro asset/RMM IS API-drivable](reference_syncro_rmm_api_gui_only.md) — CORRECTED 2026-07-10: asset read/create/update, patches, installed_apps, rmm_alerts all work via API; ONLY policy folders + `policy_folder_id` move need the `Assets - Policy Change` token scope (401 today). Archive stays GUI-only. Full catalog: `.claude/standards/syncro/api-reference.md`.
- [Datto EDR detection behavior](reference_datto_edr_detection_behavior.md) — alert `sourceType`: `av`=Datto AV signature, `rule`=EDR reputation detection (both via `edr.py detections`). EDR is reputation-based not structural (wire known-bad file as autostart exe to trip it; loose files aren't surveyed). AV is tamper-protected (console-only disable); disabling Datto AV uninstalls it + Defender auto-reactivates (AMSI blocks scripts with literal EICAR → build from char codes). Verified live on RMM-TEST-MACHINE.
- [Approval Workflow: Tools vs Projects](approval-workflow-tools-vs-projects.md) — Tools (remediation, scripts): Howard/Claude with approval. Projects (GuruRMM): Mike approval for architecture/features; Howard can handle merges/deploys himself (2026-06-21); bugs→bug list.
- [CDP Chrome driver](reference_cdp_chrome_driver.md) — Drive Chrome via DevTools Protocol (.claude/scripts/cdp.py): visible window + screenshots-to-disk so Gemini/Grok can SEE the live site. Use localhost not 127.0.0.1; dedicated profile. Antigravity-style.

View File

@@ -1,15 +1,41 @@
---
name: reference-syncro-rmm-api-gui-only
description: Syncro's public API cannot manage RMM policies/folders — creation, assignment, and asset moves are GUI-only (live-verified 2026-06-25)
description: Syncro asset/RMM IS API-drivable (reads, create/update, patches, installed_apps, rmm_alerts) — only policy-folder ops need a token scope our per-user keys currently lack (re-verified 2026-07-10)
metadata:
type: reference
---
**Syncro RMM policy management is GUI-only — the public REST API does NOT expose it.** Live-verified against the ACG production tenant (computerguru.syncromsp.com) on 2026-06-25:
**CORRECTION (2026-07-10): most of Syncro's asset/RMM surface IS API-drivable — the earlier
"RMM is GUI-only" conclusion was a token-scope symptom, not an API limitation.** Live-probed
the ACG production tenant with a per-user token (howard) on 2026-07-10 and confirmed against
the OpenAPI 3.0 spec (`api-docs.syncromsp.com/swagger.json`):
- `GET /customer_assets` objects carry a read-only **`policy_folder_id`** field (which policy folder the machine sits in). **`PUT /customer_assets/{id}` with `policy_folder_id` is silently ignored** — returns HTTP 200 but the value never changes. Proven by a flip-and-restore test on ACG-internal asset 12335235 (DESKTOP-0O8A1RL): value stayed at folder 692253. **You CANNOT move a machine between policy folders via the API.**
- `/policies`, `/policy_builders`, `/rmm_policies`, `/asset_policies` all return **404** — no policy-CRUD endpoints exist. Policy Builder (the `/policy_builders` GUI page) is web-console only.
- `/policy_folders` (collection and specific-ID) returns **401** — the route exists but our API token lacks RMM/policy scope. A re-issued token *might* read folders, but since assets can't be moved anyway, it's moot for the move use case.
- Syncro docs (docs.syncrosecure.com / docs.syncromsp.com "Work with Policies") confirm: policies are created in Policy Builder, assigned via an Organization's "Assets & Policies" subtab "Update Assigned Policy" dropdown, or "Bulk Assign Top-Level Policy" — all GUI, **no API mention**.
- **Works with our current per-user token (HTTP 200):** `GET/POST /customer_assets`,
`PUT /customer_assets/{id}` (name/serial/type/customer/properties — `name` required),
`GET /customer_assets/{id}/patches`, `.../installed_applications`, `/rmm_alerts` (list +
`{id}/mute` + DELETE clear), plus contacts, contracts, leads, vendors, purchase_orders,
products, payments, wiki_pages, `/me`, `/search`. The `/syncro` skill now documents the
asset-read/asset-write/rmm-alert/agent-deploy workflows; the full inventory is in
`.claude/standards/syncro/api-reference.md`.
- **Policy-folder move: VERIFIED WORKING with a full-scope token (2026-07-10).** Howard's
token was upgraded to full scope; flip-and-restore on ACG-internal asset `12335235`
(`692253 → 692278 → 692253`) succeeded, each confirmed by GET. `PUT /customer_assets/{id}`
with `policy_folder_id` (+ required `name`) moves the asset. The spec: *"only
policy_folder_id requires **Assets - Policy Change**; with other fields requires **Assets -
Edit** AND **Assets - Policy Change**; nil/nonexistent/cross-customer folder → 422."*
- **Route quirks:** `GET /policy_folders` (bare) → 404; `GET /policy_folders?customer_id=N`
200 (enumerate a customer's folders); `GET /policy_folders/{id}` → 401 even at full scope.
- **Per-user scope differs — capability-check every scope-gated write.** A token WITHOUT
Policy-Change returns HTTP 200 and silently no-ops the move (the exact 2026-06-25 symptom
that produced the original "GUI-only" belief); `/me` shows coarse `asset:{write:true}` and
cannot detect it. The `/syncro move-asset` helper preflights `GET /policy_folders?customer_id=N`
(401 = no scope) and verifies the folder actually changed after the PUT. Only Howard's token
is confirmed full-scope so far; grant the scope to each per-user token for consistent behavior.
- **Archive (retire) IS genuinely GUI-only** — no archive endpoint, no `archived` field, and
DELETE is destructive. That part of the old note stands.
**How to apply:** Do NOT attempt to build `/syncro move-asset` or any Syncro RMM policy/folder/group capability — it's not buildable on the public API. Don't re-probe these endpoints. The only API-drivable policy surface in the fleet is the `bitdefender` skill (GravityZone: create/assign policies, custom groups, move endpoints). For Syncro RMM policy work, direct the user to the Syncro web console. The `/syncro` skill stays PSA-only (tickets/billing/customers/scheduling/estimates + read-only asset lookup). See [[feedback-psa-default-syncro]].
**How to apply:** Build/asset-manage freely via API for everything EXCEPT policy folders. To
unlock `/syncro move-asset` + policy CRUD, add **Assets - Policy Change** + policy-folder
read/manage to the Syncro API token (Admin → API Tokens), re-vault, then flip-and-restore
verify on ACG-internal asset `12335235` (folder `692253`). Do NOT re-assert "can't do assets
via API" — that was wrong. See [[feedback-psa-default-syncro]] and the api-reference doc.

View File

@@ -0,0 +1,435 @@
# Syncro API — Complete Endpoint Reference
Breadth reference for the **entire** Syncro REST API surface (~180 endpoints across 38
resource types), generated from the live OpenAPI 3.0 spec
(`https://api-docs.syncromsp.com/swagger.json`) and cross-checked against the production
tenant on **2026-07-10**. Use this to reach ANY Syncro call — not just the PSA core.
- **The high-frequency, high-risk workflows** (tickets, billing/line-items, invoices,
estimates, schedules, appointments, customers-read) are fully documented with verified
response shapes, gotchas, and preview gates in **`.claude/commands/syncro.md`**. Do NOT
duplicate that logic here — this file is the map; `syncro.md` is the operating manual for
the core, and holds the detailed asset / RMM-alert / agent-deploy workflows too.
- Everything below is reachable with the same auth + payload conventions as the core.
## Auth & conventions (same as syncro.md)
- **Base:** `https://computerguru.syncromsp.com/api/v1`
- **Auth:** `?api_key=<key>` **query param** (NOT a header). Per-user keys resolved by
`source .claude/scripts/syncro-env.sh``$SYNCRO_BASE` / `$SYNCRO_API_KEY`. Never hardcode.
- **Every call is attributed to the key owner** — comments, line items, assets, invoices.
- **Payload handoff:** heredoc `--data-binary @-` with `<<'JSON'` (static) or `<<JSON`
(interpolating `${VARS}`). Never `/tmp/*.json` on Windows.
- **Parsing:** Syncro emits unescaped control chars — `tr -d '\000-\037'` before jq, or use
grep/sed. Never retry jq on a parse failure.
- **Rate limit:** 180 req/min per IP. 429 → wait 60s.
## Write discipline (applies to EVERY POST/PUT/PATCH/DELETE here)
1. **Preview + explicit confirmation before any write** — show the full payload.
2. **No retries on an ambiguous result** — GET the resource to confirm; Syncro has no
idempotency, so a blind retry duplicates. Duplicates can't be deleted via API.
3. **`[TEST]` prefix** on any test article; test against ACG-internal `customer_id 15353550`.
4. **Bot alert after every successful write** (`post-bot-alert.sh`) — same as the core.
5. **DELETE is immediate and unconfirmed** on most resources (schedules, estimates, assets
caveat below) — never probe destructive methods against live customer data.
## Live token-capability matrix (howard key, probed 2026-07-10)
| Surface | Status with current per-user token |
|---|---|
| Tickets, Customers, Invoices, Estimates, Schedules, Appointments, Line Items | **OK** (core — see syncro.md) |
| Assets: GET list/detail, **`/patches`**, **`/installed_applications`**, chat_info | **OK** (HTTP 200) |
| Assets: POST create, PUT update (name/serial/type/customer/properties) | **OK**`name` required |
| Contacts, Contracts, Leads, Vendors, Purchase Orders, Payments, Products, Wiki, RMM Alerts | **OK** (HTTP 200) |
| `/me`, `/users`, `/settings`, `/search` | **OK** |
| **Policy Folders** (`GET/POST/PUT/DELETE /policy_folders`) | **401 — token lacks scope** |
| **Asset move via `PUT /customer_assets/{id}` `policy_folder_id`** | **needs scope** (see below) |
### Policy-folder scope (corrects the old "RMM is GUI-only" belief)
The spec is explicit on `PUT /customer_assets/{id}`:
> *"Updating only `policy_folder_id` requires **Assets - Policy Change**. Updating
> `policy_folder_id` with other asset fields requires both **Assets - Edit** and **Assets -
> Policy Change**. Nil, nonexistent, or cross-customer folder IDs return 422."*
Our per-user tokens currently **lack** the RMM/policy scope, so `/policy_folders` returns
401 and a `policy_folder_id` PUT is silently ignored (HTTP 200, no change) — exactly the
2026-06-25 symptom. This is a **token-scope gap, not an API limitation.** To enable
policy-folder move + policy CRUD: in Syncro **Admin → API Tokens**, edit the token (or issue
a Custom token) to add **Assets - Policy Change** and policy-folder read/manage permissions,
re-vault it, then re-verify with a flip-and-restore on ACG-internal asset `12335235`
(currently folder `692253`). Supersedes memory `reference-syncro-rmm-api-gui-only`.
## RMM-relevant reads (asset intelligence)
- `GET /customer_assets/{id}/patches` → `{available_patches, available_patches_meta,
installed_patches}` — Windows patch posture per machine.
- `GET /customer_assets/{id}/installed_applications` → `{installed_applications, meta}`
(paginated) — software inventory; use to **verify an agent/app install** after a push.
- `GET /rmm_alerts` → `{rmm_alerts, meta}` — live RMM alert feed; POST create, `{id}/mute`,
DELETE clears.
---
# Calling any endpoint — worked templates
Every endpoint below uses the SAME auth + payload pattern (the Swagger UI "code examples" are
generated from these same schemas). Copy a template, swap the path + body fields from the
inventory. `${BASE}`/`${API_KEY}` come from `source .claude/scripts/syncro-env.sh`.
```bash
# READ (GET) — list or single. Add filters as query params (e.g. customer_id, query, per_page).
curl -s "${BASE}/<resource>?customer_id=${CUST_ID}&per_page=100&api_key=${API_KEY}" | tr -d '\000-\037' | jq '.'
# CREATE (POST) — body fields from the inventory's _body:_ line; _required:_ must be present.
curl -s -X POST "${BASE}/<resource>?api_key=${API_KEY}" -H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{ "field": "value" }
JSON
# UPDATE (PUT/PATCH) — same shape; resend any field the endpoint marks required.
curl -s -X PUT "${BASE}/<resource>/${ID}?api_key=${API_KEY}" -H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{ "field": "new value" }
JSON
# DELETE — immediate, no confirmation on most resources. Preview + confirm first.
curl -s -X DELETE "${BASE}/<resource>/${ID}?api_key=${API_KEY}"
```
Build bodies with `jq -nc --arg`/`--argjson` when interpolating shell values (JSON-safe). For
any WRITE: preview the payload, confirm, then **GET the resource back to verify** (Syncro has
no idempotency; a blind retry duplicates). Scope-gated writes can return 200 yet no-op — always
verify the value actually changed (see the policy-folder note above).
---
# Full endpoint inventory (by resource)
Legend: methods are literal. `_required:_` lists must-send body fields; `_body:_` lists all
accepted body fields (from the live schema). Absence of a body line = no JSON body / path-or-
query only. `{...}` segments are path params.
## Appointment
- `GET /appointments` — Returns a paginated list of Appointments
- `POST /appointments` — Creates an Appointment
_required:_ `summary,start_at`
_body:_ all_day, appointment_duration, appointment_type_id, customer_id, description, do_not_email, email_customer, end_at, location, start_at, summary, ticket_id, user_id, user_ids
- `DELETE /appointments/{id}` — Deletes an Appointment by ID
- `GET /appointments/{id}` — Retrieves an Appointment by ID
- `PUT /appointments/{id}` — Updates an existing Appointment by ID
_required:_ `start_at`
_body:_ all_day, appointment_duration, appointment_type_id, customer_id, description, email_customer, end_at, location, start_at, summary, ticket_id, user_id, user_ids
## Appointment Type
- `GET /appointment_types` — Returns a paginated list of Appointment Types
- `POST /appointment_types` — Creates an Appointment Type
_required:_ `name`
_body:_ email_instructions, location_hard_code, location_type, name
- `DELETE /appointment_types/{id}` — Deletes an Appointment Type by ID
- `GET /appointment_types/{id}` — Retrieves an Appointment Type by ID
- `PUT /appointment_types/{id}` — Updates an existing Appointment Type by ID
_body:_ email_instructions, location_hard_code, location_type, name
## Asset
- `GET /customer_assets` — Returns a paginated list of Assets
- `POST /customer_assets` — Creates an Asset
_required:_ `name`
_body:_ asset_serial, asset_type_id, asset_type_name, customer_id, name, properties
- `GET /customer_assets/chat_information_by_ids` — Retrieves Assets chat informations by IDs
- `GET /customer_assets/{id}` — Retrieves an Asset by ID
- `PUT /customer_assets/{id}` — Updates an existing Asset by ID
_required:_ `name`
_body:_ asset_serial, asset_type_id, asset_type_name, customer_id, name, policy_folder_id, properties
- `GET /customer_assets/{id}/installed_applications` — Retrieves installed applications for an Asset
- `GET /customer_assets/{id}/patches` — Retrieves Windows patch data for an Asset
## Call
- `GET /callerid` — Get Caller ID
## Canned Response
- `GET /canned_responses` — Returns a list of Canned Responses with a query
- `POST /canned_responses` — Creates a new Canned Response
_required:_ `title,body`
_body:_ body, canned_response_category_id, subject, title
- `GET /canned_responses/settings` — Returns the settings for Canned Responses
- `DELETE /canned_responses/{id}` — Deletes a Canned Response
- `PATCH /canned_responses/{id}` — Updates a Canned Response
_body:_ body, canned_response_category_id, subject, title
## Contact
- `GET /contacts` — Returns a paginated list of Contacts
- `POST /contacts` — Creates a Contact
_required:_ `customer_id`
_body:_ address1, address2, city, customer_id, email, enable_portal_user, extension, mobile, name, notes, phone, state, zip
- `DELETE /contacts/{id}` — Deletes a Contact
- `GET /contacts/{id}` — Retrieves a Contact by ID
- `PUT /contacts/{id}` — Updates an existing Contact
_required:_ `name`
_body:_ address1, address2, city, customer_id, email, enable_portal_user, extension, mobile, name, notes, phone, state, title, zip
## Contract
- `GET /contracts` — Returns a paginated list of Contracts
- `POST /contracts` — Creates a Contract
_required:_ `customer_id`
_body:_ apply_to_all, contract_amount, customer_id, description, end_date, likelihood, name, primary_contact, sla_id, start_date, status
- `DELETE /contracts/{id}` — Deletes a Contract by ID
- `GET /contracts/{id}` — Retrieves a Contract by ID
- `PUT /contracts/{id}` — Updates an existing Contract by ID
_required:_ `customer_id`
_body:_ apply_to_all, contract_amount, customer_id, description, end_date, likelihood, name, primary_contact, sla_id, start_date, status
## Customer
- `GET /customers` — Returns a paginated list of customers
- `POST /customers` — Creates a Customer
- `GET /customers/autocomplete` — Returns a paginated list of customers for autocomplete query
- `GET /customers/latest` — Returns latest Customer
- `DELETE /customers/{id}` — Deletes a Customer by ID
- `GET /customers/{id}` — Retrieves a Customer by ID
- `PUT /customers/{id}` — Updates an existing Customer by ID
## Estimate
- `GET /estimates` — Returns a paginated list of Estimates
- `POST /estimates` — Creates an Estimate
_body:_ created_at, customer_id, date, line_items, location_id, name, note, number, status, ticket_id, updated_at
- `DELETE /estimates/{id}` — Deletes an Estimate by ID
- `GET /estimates/{id}` — Retrieves an Estimate by ID or number
- `PUT /estimates/{id}` — Updates an existing Estimate by ID
_body:_ customer_id, date, location_id, name, note, number, status, ticket_id
- `POST /estimates/{id}/convert_to_invoice` — Convert an Estimate to an Invoice
- `POST /estimates/{id}/email` — Sends an Estimate to a Customer
- `POST /estimates/{id}/line_items` — Adds a Line Item to an Estimate
- `DELETE /estimates/{id}/line_items/{line_item_id}` — Deletes a Line Item
- `PUT /estimates/{id}/line_items/{line_item_id}` — Updates a Line Item
- `POST /estimates/{id}/print` — Queues a print job for an Estimate
## Invoice
- `GET /invoices` — Returns a paginated list of Invoices
- `POST /invoices` — Creates an Invoice
_required:_ `customer_id,number,date`
_body:_ balance_due, contact_id, created_at, customer_business_then_name, customer_id, date, due_date, hardwarecost, id, is_paid, line_items, location_id, note, number, pdf_url, po_number, subtotal, tax, tech_marked_paid, ticket_id, total, updated_at, verified_paid
- `DELETE /invoices/{id}` — Deletes an invoice by ID
- `GET /invoices/{id}` — Retrieves an Invoice by ID or Number
- `PUT /invoices/{id}` — Updates an existing invoice by ID
_body:_ contact_id, created_at, customer_business_then_name, customer_id, date, due_date, hardwarecost, location_id, note, number, pdf_url, po_number, subtotal, tax, ticket_id, total, updated_at
- `POST /invoices/{id}/email` — Sends invoice to customer
- `POST /invoices/{id}/print` — Queues a print job for an invoice
- `GET /invoices/{id}/ticket` — Returns the associated ticket for an invoice
## Invoice/Line item
- `POST /invoices/{id}/line_items` — Creates a new line item
- `DELETE /invoices/{id}/line_items/{line_item_id}` — Deletes an a line item of an invoice by ID
- `PUT /invoices/{id}/line_items/{line_item_id}` — Updates an a line item of an invoice by ID
## Item
- `GET /items` — Returns a paginated list of Part Orders
## Lead
- `GET /leads` — Returns a paginated list of Leads
- `POST /leads` — Creates a Lead
_body:_ address, appointment_time, appointment_type_id, business_name, city, contact_id, converted, customer_id, customer_purchase_id, disabled, email, first_name, from_check_in, hidden_notes, last_name, likelihood, location_id, mailbox_id, message_read, mobile, opportunity_amount_dollars, opportunity_start_date, phone, properties, signature_data, signature_date, signature_name, state, status, ticket_description, ticket_id, ticket_problem_type, ticket_properties, ticket_subject, ticket_type_id, user_id, zip
- `GET /leads/{id}` — Retrieves a Lead by ID
- `PUT /leads/{id}` — Updates an existing Lead by ID
_body:_ address, appointment_time, appointment_type_id, business_name, city, contact_id, converted, customer_id, customer_purchase_id, disabled, email, first_name, from_check_in, hidden_notes, last_name, likelihood, location_id, mailbox_id, message_read, mobile, opportunity_amount_dollars, opportunity_start_date, phone, properties, signature_data, signature_date, signature_name, state, status, ticket_description, ticket_id, ticket_problem_type, ticket_properties, ticket_subject, ticket_type_id, user_id, zip
## Line Item
- `GET /line_items` — Returns a paginated list of Line Items
## New Ticket Form
- `GET /new_ticket_forms` — Returns a paginated list of Ticket Forms
- `GET /new_ticket_forms/{id}` — Retrieves a Ticket Form
- `POST /new_ticket_forms/{id}/process_form` — Creates a new Ticket for a Ticket Form
_body:_ appointments, customer_details, ticket_details
## Payment
- `GET /payments` — Returns a paginated list of Payments
- `POST /payments` — Creates a Payment
_body:_ address_city, address_street, address_zip, amount_cents, apply_payments, credit_card_number, customer_id, cvv, date_month, date_year, firstname, invoice_id, invoice_number, lastname, payment_method, ref_num, register_id, signature_data, signature_date, signature_name
- `GET /payments/{id}` — Retrieves a Payment by ID
## Payment Method
- `GET /payment_methods` — Returns a paginated list of Payment Methods
## Payment Profile
- `GET /customers/{customer_id}/payment_profiles` — Returns a paginated list of Payment Profiles
- `POST /customers/{customer_id}/payment_profiles` — Creates a Payment Profile
_body:_ customer_external_id, expiration, last_four, payment_profile_id
- `DELETE /customers/{customer_id}/payment_profiles/{id}` — Deletes a Payment Profile
- `GET /customers/{customer_id}/payment_profiles/{id}` — Retrieves a Payment Profile by ID
- `PUT /customers/{customer_id}/payment_profiles/{id}` — Updates a Payment Profile
_body:_ expiration, last_four
## Phone
- `GET /customers/{customer_id}/phones` — Returns a paginated list of Phones
- `POST /customers/{customer_id}/phones` — Creates a Phone
- `DELETE /customers/{customer_id}/phones/{id}` — Deletes a Phone by ID
- `PUT /customers/{customer_id}/phones/{id}` — Updates an existing Phone by ID
## Policy Folder
- `GET /policy_folders` — Returns a paginated list of Policy Folders
- `POST /policy_folders` — Creates a Policy Folder
_required:_ `customer_id,name,parent_id`
_body:_ customer_id, name, parent_id
- `DELETE /policy_folders/{id}` — Deletes a Policy Folder by ID
- `GET /policy_folders/{id}` — Retrieves a Policy Folder by ID
- `PUT /policy_folders/{id}` — Updates an existing Policy Folder by ID
_body:_ name, parent_id, partial_policy_id
## Portal User
- `GET /portal_users` — Returns a paginated list of Portal Users
- `POST /portal_users` — Creates a Portal User
- `POST /portal_users/create_invitation` — Creates an Invitation for a Portal User
_body:_ id
- `DELETE /portal_users/{id}` — Deletes a Portal User by ID
- `PUT /portal_users/{id}` — Updates an existing Portal User by ID
## Product
- `GET /products` — Returns a paginated list of Products
- `POST /products` — Creates a Product
_required:_ `name,description`
_body:_ category_ids, condition, description, desired_stock_level, disabled, discount_percent, maintain_stock, name, notes, physical_location, price_cost, price_retail, price_wholesale, product_category, product_skus_attributes, qb_item_id, quantity, reorder_at, serialized, sort_order, tax_rate_id, taxable, upc_code, vendor_ids, warranty, warranty_template_id
- `GET /products/barcode` — Returns a Product by Barcode
- `GET /products/categories` — Returns a paginated list of Product Categories
- `GET /products/{id}` — Retrieves a Product by ID
- `PUT /products/{id}` — Updates an existing Product by ID
_required:_ `name,description`
_body:_ category_ids, condition, description, desired_stock_level, disabled, discount_percent, maintain_stock, name, notes, physical_location, price_cost, price_retail, price_wholesale, product_category, product_skus_attributes, qb_item_id, quantity, reorder_at, serialized, sort_order, tax_rate_id, taxable, upc_code, vendor_ids, warranty, warranty_template_id
- `POST /products/{id}/add_images` — Creates a Product Image
- `DELETE /products/{id}/delete_image` — Deletes a Product Image
- `PUT /products/{id}/location_quantities` — Updates a Location Quantity
_body:_ location_quantity_id, quantity
## Product Serial
- `GET /products/{product_id}/product_serials` — Returns a paginated list of Product_serials
- `POST /products/{product_id}/product_serials` — Creates a Product Serial
_body:_ condition, price_cost_cents, price_retail_cents, serial_number
- `POST /products/{product_id}/product_serials/attach_to_line_item` — Adds Product Serials to a Line Item
_body:_ line_item_id, product_serial_ids, record_type
- `PUT /products/{product_id}/product_serials/{id}` — Updates an existing Product Serial by ID
_body:_ condition, notes, price_cost_cents, price_retail_cents, serial_number
## Product Sku
- `GET /products/{product_id}/product_skus` — Returns list of Product Skus
- `POST /products/{product_id}/product_skus` — Creates a Product Sku
_body:_ value, vendor_id
- `PUT /products/{product_id}/product_skus/{id}` — Updates an existing Product Sku by ID
_body:_ value, vendor_id
## Purchase Order
- `GET /purchase_orders` — Returns a paginated list of Purchase Orders
- `POST /purchase_orders` — Creates a Purchase Order
_body:_ delivery_tracking, discount_percent, due_date, expected_date, general_notes, location_id, order_date, other_cents, paid_date, shipping_cents, shipping_notes, user_id, vendor_id
- `GET /purchase_orders/{id}` — Retrieves a Purchase Order by ID
- `POST /purchase_orders/{id}/create_po_line_item` — Adds a Product to a Purchase Order
_body:_ product_id, quantity
- `POST /purchase_orders/{id}/receive` — receive purchase_order
_body:_ line_item_id
## RMM Alert
- `GET /rmm_alerts` — Returns a paginated list of RMM Alerts
- `POST /rmm_alerts` — Creates an RMM Alert
_body:_ rmm_alert
- `DELETE /rmm_alerts/{id}` — Deletes/Clears an RMM Alert by ID
- `GET /rmm_alerts/{id}` — Retrieves an RMM Alert by ID
- `POST /rmm_alerts/{id}/mute` — Mutes an RMM Alert by ID
## Schedule
- `GET /schedules` — Returns a paginated list of Invoice Schedules
- `POST /schedules` — Creates an Invoice Schedule
- `DELETE /schedules/{id}` — Deletes a Schedule by ID
- `GET /schedules/{id}` — Retrieves a Schedule by ID
- `PUT /schedules/{id}` — Updates an existing Invoice Schedule by ID
- `POST /schedules/{id}/add_line_item` — Adds a Line Item to an Invoice Schedule
- `PUT /schedules/{id}/line_items/{schedule_line_item_id}` — Updates a Line Item
- `POST /schedules/{id}/remove_line_item` — Removes a Line Item from an Invoice Schedule
## Search
- `GET /search` — Search all the things
## Setting
- `GET /settings` — Returns a list of Account Settings
- `GET /settings/printing` — Returns Printing Settings
- `GET /settings/tabs` — Returns Tabs Settings
## Ticket
- `GET /ticket_comments` — Returns a paginated flat list of comments across multiple tickets
- `GET /tickets` — Returns a paginated list of Tickets
- `POST /tickets` — Creates a Ticket
- `GET /tickets/settings` — Returns Tickets Settings
- `DELETE /tickets/{id}` — Deletes a Ticket by ID
- `GET /tickets/{id}` — Retrieves a Ticket by ID
- `PUT /tickets/{id}` — Updates an existing Ticket by ID
- `POST /tickets/{id}/add_line_item` — Creates a Ticket Line Item
_body:_ description, name, price_cost, price_retail, product_id, quantity, taxable, upc_code
- `POST /tickets/{id}/attach_file_url` — Attach a file to a Ticket
- `POST /tickets/{id}/charge_timer_entry` — Charges a Ticket Timer
- `POST /tickets/{id}/comment` — Adds a Comment to a Ticket
_body:_ body, do_not_email, hidden, sms_body, subject, tech
- `GET /tickets/{id}/comments` — Returns Comments for a Ticket
- `POST /tickets/{id}/delete_attachment` — Deletes a Ticket Attachment
_body:_ attachment_id
- `POST /tickets/{id}/delete_timer_entry` — Deletes a Ticket Timer
- `POST /tickets/{id}/print` — Prints a Ticket by ID
- `POST /tickets/{id}/remove_line_item` — Deletes a Ticket Line Item
_body:_ ticket_line_item_id
- `POST /tickets/{id}/timer_entry` — Create a Ticket Timer for a Ticket
_body:_ duration_minutes, end_at, notes, product_id, start_at, user_id
- `PUT /tickets/{id}/update_line_item` — Updates an existing Ticket Line Item
_body:_ description, name, price_cost, price_retail, product_id, quantity, taxable, ticket_line_item_id, upc_code
- `PUT /tickets/{id}/update_timer_entry` — Updates an existing Ticket Timer
_body:_ duration_minutes, notes, product_id, start_at, timer_entry_id, user_id
## Ticket Blueprint
- `GET /ticket_blueprints` — Returns a paginated list of Ticket Blueprints
- `POST /ticket_blueprints/{id}/apply` — Creates tickets from a Blueprint
_required:_ `customer_id`
_body:_ customer_id
## Ticket Timer
- `GET /ticket_timers` — Returns a paginated list of Ticket Timers
- `PATCH ticket_timers/{id}` — Update the billable property of a Ticket Timer
## Timelog
- `GET /timelogs` — Returns a paginated list of Timelogs
- `PUT /timelogs` — Updates a Timelog
_body:_ in_at, in_note, lunch, out_at, out_note
- `GET /timelogs/last` — Returns last Timelog
## User
- `GET /me` — Returns the current user
- `POST /otp_login` — Authorize a User with One Time Password
_body:_ code
- `GET /users` — Returns a paginated list of Users
- `GET /users/{id}` — Retrieves an existing User by ID
## User Device
- `POST /user_devices` — Creates a User Device
_body:_ device_name, device_uuid, model, registration_token_gcm, screen_size, system_name
- `GET /user_devices/{id}` — Retrieves an existing User Device by UUID
- `PUT /user_devices/{id}` — Updates an existing User Device by UUID
_body:_ registration_token_gcm
## Vendor
- `GET /vendors` — Returns a paginated list of Vendors
- `POST /vendors` — Creates a Vendor
- `GET /vendors/{id}` — Retrieves a Vendor Page
- `PUT /vendors/{id}` — Updates an existing Vendor page by ID
## Wiki Page
- `GET /wiki_pages` — Returns a paginated list of Wiki Pages
- `POST /wiki_pages` — Creates a Wiki Page
_body:_ asset_id, body, customer_id, name, slug, visibility
- `DELETE /wiki_pages/{id}` — Deletes a Wiki Page by ID
- `GET /wiki_pages/{id}` — Retrieves a Wiki Page
- `PUT /wiki_pages/{id}` — Updates an existing Wiki Page by ID
_body:_ asset_id, body, customer_id, name, slug, visibility
## Worksheet Result
- `GET /tickets/{ticket_id}/worksheet_results` — Returns a paginated list of Worksheet Results
- `POST /tickets/{ticket_id}/worksheet_results` — Creates Worksheet Result
_body:_ title, worksheet_template_id
- `DELETE /tickets/{ticket_id}/worksheet_results/{id}` — Deletes a Worksheet Result
- `GET /tickets/{ticket_id}/worksheet_results/{id}` — Retrieves a Worksheet Result by ID
- `PUT /tickets/{ticket_id}/worksheet_results/{id}` — Updates a Worksheet Result
_body:_ answers, complete, public, required, title, user_id, worksheet_template_id

View File

@@ -19,6 +19,8 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
<!-- Append entries below this line -->
2026-07-10 | Howard-Home | syncro/assets | [correction] assumed asset writes (POST/PUT create-update), policy_folder_id move, patches, and installed_applications were not doable via the public API; correct is the endpoints exist per the live OpenAPI spec and the prior 'policy_folder_id silently ignored' was a token missing the 'Assets - Policy Change' scope, not an API limitation [ctx: ref=reference-syncro-rmm-api-gui-only endpoint=PUT_/customer_assets/{id}]
2026-07-10 | GURU-BEAST-ROG | guruscan | detached full-chain wrapper died mid-RKill on TPM-PC (marker never written, task->Ready, rkill.log completed fine as orphan) — RKill's process sweep almost certainly killed its own hidden SYSTEM powershell parent; relaunched with -SkipScanners RKill [ctx: host=TPM-PC client=mineralogical-record chain-design-issue]
2026-07-10 | GURU-5070 | packetdial | HTTP 404 GET https://pbx.packetdial.com/ns-api/v2/domains/vwp.91912.service/users/100: {"code":404,"message":"Resource not found."} [ctx: cmd=user] (x2)