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>
24 KiB
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.mdis 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 bysource .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/*.jsonon 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)
- Preview + explicit confirmation before any write — show the full payload.
- 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.
[TEST]prefix on any test article; test against ACG-internalcustomer_id 15353550.- Bot alert after every successful write (
post-bot-alert.sh) — same as the core. - 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_idrequires Assets - Policy Change. Updatingpolicy_folder_idwith 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.
# 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 AppointmentsPOST /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_idsDELETE /appointments/{id}— Deletes an Appointment by IDGET /appointments/{id}— Retrieves an Appointment by IDPUT /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 TypesPOST /appointment_types— Creates an Appointment Type
required:name
body: email_instructions, location_hard_code, location_type, nameDELETE /appointment_types/{id}— Deletes an Appointment Type by IDGET /appointment_types/{id}— Retrieves an Appointment Type by IDPUT /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 AssetsPOST /customer_assets— Creates an Asset
required:name
body: asset_serial, asset_type_id, asset_type_name, customer_id, name, propertiesGET /customer_assets/chat_information_by_ids— Retrieves Assets chat informations by IDsGET /customer_assets/{id}— Retrieves an Asset by IDPUT /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, propertiesGET /customer_assets/{id}/installed_applications— Retrieves installed applications for an AssetGET /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 queryPOST /canned_responses— Creates a new Canned Response
required:title,body
body: body, canned_response_category_id, subject, titleGET /canned_responses/settings— Returns the settings for Canned ResponsesDELETE /canned_responses/{id}— Deletes a Canned ResponsePATCH /canned_responses/{id}— Updates a Canned Response
body: body, canned_response_category_id, subject, title
Contact
GET /contacts— Returns a paginated list of ContactsPOST /contacts— Creates a Contact
required:customer_id
body: address1, address2, city, customer_id, email, enable_portal_user, extension, mobile, name, notes, phone, state, zipDELETE /contacts/{id}— Deletes a ContactGET /contacts/{id}— Retrieves a Contact by IDPUT /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 ContractsPOST /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, statusDELETE /contracts/{id}— Deletes a Contract by IDGET /contracts/{id}— Retrieves a Contract by IDPUT /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 customersPOST /customers— Creates a CustomerGET /customers/autocomplete— Returns a paginated list of customers for autocomplete queryGET /customers/latest— Returns latest CustomerDELETE /customers/{id}— Deletes a Customer by IDGET /customers/{id}— Retrieves a Customer by IDPUT /customers/{id}— Updates an existing Customer by ID
Estimate
GET /estimates— Returns a paginated list of EstimatesPOST /estimates— Creates an Estimate
body: created_at, customer_id, date, line_items, location_id, name, note, number, status, ticket_id, updated_atDELETE /estimates/{id}— Deletes an Estimate by IDGET /estimates/{id}— Retrieves an Estimate by ID or numberPUT /estimates/{id}— Updates an existing Estimate by ID
body: customer_id, date, location_id, name, note, number, status, ticket_idPOST /estimates/{id}/convert_to_invoice— Convert an Estimate to an InvoicePOST /estimates/{id}/email— Sends an Estimate to a CustomerPOST /estimates/{id}/line_items— Adds a Line Item to an EstimateDELETE /estimates/{id}/line_items/{line_item_id}— Deletes a Line ItemPUT /estimates/{id}/line_items/{line_item_id}— Updates a Line ItemPOST /estimates/{id}/print— Queues a print job for an Estimate
Invoice
GET /invoices— Returns a paginated list of InvoicesPOST /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_paidDELETE /invoices/{id}— Deletes an invoice by IDGET /invoices/{id}— Retrieves an Invoice by ID or NumberPUT /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_atPOST /invoices/{id}/email— Sends invoice to customerPOST /invoices/{id}/print— Queues a print job for an invoiceGET /invoices/{id}/ticket— Returns the associated ticket for an invoice
Invoice/Line item
POST /invoices/{id}/line_items— Creates a new line itemDELETE /invoices/{id}/line_items/{line_item_id}— Deletes an a line item of an invoice by IDPUT /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 LeadsPOST /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, zipGET /leads/{id}— Retrieves a Lead by IDPUT /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 FormsGET /new_ticket_forms/{id}— Retrieves a Ticket FormPOST /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 PaymentsPOST /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_nameGET /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 ProfilesPOST /customers/{customer_id}/payment_profiles— Creates a Payment Profile
body: customer_external_id, expiration, last_four, payment_profile_idDELETE /customers/{customer_id}/payment_profiles/{id}— Deletes a Payment ProfileGET /customers/{customer_id}/payment_profiles/{id}— Retrieves a Payment Profile by IDPUT /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 PhonesPOST /customers/{customer_id}/phones— Creates a PhoneDELETE /customers/{customer_id}/phones/{id}— Deletes a Phone by IDPUT /customers/{customer_id}/phones/{id}— Updates an existing Phone by ID
Policy Folder
GET /policy_folders— Returns a paginated list of Policy FoldersPOST /policy_folders— Creates a Policy Folder
required:customer_id,name,parent_id
body: customer_id, name, parent_idDELETE /policy_folders/{id}— Deletes a Policy Folder by IDGET /policy_folders/{id}— Retrieves a Policy Folder by IDPUT /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 UsersPOST /portal_users— Creates a Portal UserPOST /portal_users/create_invitation— Creates an Invitation for a Portal User
body: idDELETE /portal_users/{id}— Deletes a Portal User by IDPUT /portal_users/{id}— Updates an existing Portal User by ID
Product
GET /products— Returns a paginated list of ProductsPOST /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_idGET /products/barcode— Returns a Product by BarcodeGET /products/categories— Returns a paginated list of Product CategoriesGET /products/{id}— Retrieves a Product by IDPUT /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_idPOST /products/{id}/add_images— Creates a Product ImageDELETE /products/{id}/delete_image— Deletes a Product ImagePUT /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_serialsPOST /products/{product_id}/product_serials— Creates a Product Serial
body: condition, price_cost_cents, price_retail_cents, serial_numberPOST /products/{product_id}/product_serials/attach_to_line_item— Adds Product Serials to a Line Item
body: line_item_id, product_serial_ids, record_typePUT /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 SkusPOST /products/{product_id}/product_skus— Creates a Product Sku
body: value, vendor_idPUT /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 OrdersPOST /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_idGET /purchase_orders/{id}— Retrieves a Purchase Order by IDPOST /purchase_orders/{id}/create_po_line_item— Adds a Product to a Purchase Order
body: product_id, quantityPOST /purchase_orders/{id}/receive— receive purchase_order
body: line_item_id
RMM Alert
GET /rmm_alerts— Returns a paginated list of RMM AlertsPOST /rmm_alerts— Creates an RMM Alert
body: rmm_alertDELETE /rmm_alerts/{id}— Deletes/Clears an RMM Alert by IDGET /rmm_alerts/{id}— Retrieves an RMM Alert by IDPOST /rmm_alerts/{id}/mute— Mutes an RMM Alert by ID
Schedule
GET /schedules— Returns a paginated list of Invoice SchedulesPOST /schedules— Creates an Invoice ScheduleDELETE /schedules/{id}— Deletes a Schedule by IDGET /schedules/{id}— Retrieves a Schedule by IDPUT /schedules/{id}— Updates an existing Invoice Schedule by IDPOST /schedules/{id}/add_line_item— Adds a Line Item to an Invoice SchedulePUT /schedules/{id}/line_items/{schedule_line_item_id}— Updates a Line ItemPOST /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 SettingsGET /settings/printing— Returns Printing SettingsGET /settings/tabs— Returns Tabs Settings
Ticket
GET /ticket_comments— Returns a paginated flat list of comments across multiple ticketsGET /tickets— Returns a paginated list of TicketsPOST /tickets— Creates a TicketGET /tickets/settings— Returns Tickets SettingsDELETE /tickets/{id}— Deletes a Ticket by IDGET /tickets/{id}— Retrieves a Ticket by IDPUT /tickets/{id}— Updates an existing Ticket by IDPOST /tickets/{id}/add_line_item— Creates a Ticket Line Item
body: description, name, price_cost, price_retail, product_id, quantity, taxable, upc_codePOST /tickets/{id}/attach_file_url— Attach a file to a TicketPOST /tickets/{id}/charge_timer_entry— Charges a Ticket TimerPOST /tickets/{id}/comment— Adds a Comment to a Ticket
body: body, do_not_email, hidden, sms_body, subject, techGET /tickets/{id}/comments— Returns Comments for a TicketPOST /tickets/{id}/delete_attachment— Deletes a Ticket Attachment
body: attachment_idPOST /tickets/{id}/delete_timer_entry— Deletes a Ticket TimerPOST /tickets/{id}/print— Prints a Ticket by IDPOST /tickets/{id}/remove_line_item— Deletes a Ticket Line Item
body: ticket_line_item_idPOST /tickets/{id}/timer_entry— Create a Ticket Timer for a Ticket
body: duration_minutes, end_at, notes, product_id, start_at, user_idPUT /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_codePUT /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 BlueprintsPOST /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 TimersPATCH ticket_timers/{id}— Update the billable property of a Ticket Timer
Timelog
GET /timelogs— Returns a paginated list of TimelogsPUT /timelogs— Updates a Timelog
body: in_at, in_note, lunch, out_at, out_noteGET /timelogs/last— Returns last Timelog
User
GET /me— Returns the current userPOST /otp_login— Authorize a User with One Time Password
body: codeGET /users— Returns a paginated list of UsersGET /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_nameGET /user_devices/{id}— Retrieves an existing User Device by UUIDPUT /user_devices/{id}— Updates an existing User Device by UUID
body: registration_token_gcm
Vendor
GET /vendors— Returns a paginated list of VendorsPOST /vendors— Creates a VendorGET /vendors/{id}— Retrieves a Vendor PagePUT /vendors/{id}— Updates an existing Vendor page by ID
Wiki Page
GET /wiki_pages— Returns a paginated list of Wiki PagesPOST /wiki_pages— Creates a Wiki Page
body: asset_id, body, customer_id, name, slug, visibilityDELETE /wiki_pages/{id}— Deletes a Wiki Page by IDGET /wiki_pages/{id}— Retrieves a Wiki PagePUT /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 ResultsPOST /tickets/{ticket_id}/worksheet_results— Creates Worksheet Result
body: title, worksheet_template_idDELETE /tickets/{ticket_id}/worksheet_results/{id}— Deletes a Worksheet ResultGET /tickets/{ticket_id}/worksheet_results/{id}— Retrieves a Worksheet Result by IDPUT /tickets/{ticket_id}/worksheet_results/{id}— Updates a Worksheet Result
body: answers, complete, public, required, title, user_id, worksheet_template_id