From 1f56098652ae2ad28827222449d93b3dd8b0553c Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Fri, 10 Jul 2026 15:45:26 -0700 Subject: [PATCH] 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) --- .claude/commands/syncro.md | 176 ++++++- .claude/memory/MEMORY.md | 2 +- .../reference_syncro_rmm_api_gui_only.md | 40 +- .claude/standards/syncro/api-reference.md | 435 ++++++++++++++++++ errorlog.md | 2 + 5 files changed, 629 insertions(+), 26 deletions(-) create mode 100644 .claude/standards/syncro/api-reference.md diff --git a/.claude/commands/syncro.md b/.claude/commands/syncro.md index 09158610..73c3b752 100644 --- a/.claude/commands/syncro.md +++ b/.claude/commands/syncro.md @@ -18,6 +18,12 @@ Create, update, close, comment on, and bill tickets in Syncro PSA. /syncro estimate Create ticket + linked estimate with line items and private purchase notes /syncro schedules List recurring invoice schedules for a customer /syncro schedule View a schedule's template and line items +/syncro assets List a customer's RMM assets (read) +/syncro asset Asset detail + patches + installed applications +/syncro asset-update Rename / fix serial / edit an asset (preview-gated) +/syncro move-asset Move asset to a policy folder (needs token scope - see below) +/syncro rmm-alerts [] List live RMM alerts; mute/clear by id +/syncro deploy-agent 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=]` (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 @- </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--.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//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 = '' + $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=` + 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 diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md index fa4b6dca..2dee6462 100644 --- a/.claude/memory/MEMORY.md +++ b/.claude/memory/MEMORY.md @@ -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. diff --git a/.claude/memory/reference_syncro_rmm_api_gui_only.md b/.claude/memory/reference_syncro_rmm_api_gui_only.md index 59d7a5bf..3289639f 100644 --- a/.claude/memory/reference_syncro_rmm_api_gui_only.md +++ b/.claude/memory/reference_syncro_rmm_api_gui_only.md @@ -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. diff --git a/.claude/standards/syncro/api-reference.md b/.claude/standards/syncro/api-reference.md new file mode 100644 index 00000000..14f11989 --- /dev/null +++ b/.claude/standards/syncro/api-reference.md @@ -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=` **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 `< *"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}/?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}/?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}//${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}//${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 diff --git a/errorlog.md b/errorlog.md index 905f47ee..954dab6c 100644 --- a/errorlog.md +++ b/errorlog.md @@ -19,6 +19,8 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure · +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)