Live-tested the remaining write resources on Howard Test. Recorded: - wrapped response shapes (.lead.id/.vendor.id/.product.id/.purchase_order.id/ .po_line_item.id/.worksheet_result.id) added to Verified Response Shapes. - create_po_line_item requires product maintain_stock:true. - worksheet templates live in GET /tickets/settings (.worksheet_templates[]), no standalone endpoint; need a valid worksheet_template_id. - PUT /products requires name+description; leads/vendors/products/POs have NO DELETE (global test records persist -> GUI cleanup). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.8 KiB
Syncro Skill — Live Test Findings (running log)
Verified behaviors/gotchas discovered while testing /syncro against the live tenant on the
Howard Test sandbox customer (customer_id 36118743) + RMM-TEST-MACHINE. Newest on top.
Each entry is a rule the skill must not re-learn. Critical ones are also folded into
syncro.md (operating manual) and api-reference.md (breadth reference).
2026-07-10 — Phase 2 prep: RMM installer URL (structure decoded)
The Syncro RMM installer URL is a base64 token that partly encodes API-derivable IDs.
A working install link is https://rmm.syncromsp.com/dl/msi/<b64> where <b64> base64-decodes
to v1-{customer_id}-{A}-{B}-{policy_folder_id}. For Howard Test:
djEt... → v1-36118743-1819842526-48753-5092561 (customer_id 36118743, policy_folder_id
5092561 — both pulled from the API; A=1819842526, B=48753 are NOT on the customer or
policy-folder objects). Host is rmm.syncromsp.com (Kabuto/RMM), separate from the PSA API.
- RESOLVED (4-client sample):
B=48753is CONSTANT (our Syncro account id).AVARIES per client (1819842526 / 1610167344 / 1552910968 / 1520216784) and is notkabuto_customer_idnor anywhere in the customer/policy-folder objects — an unguessable per-client security token (prevents URL forgery from sequential customer_ids). Therefore the URL is NOT constructible — it must be harvested per client and vaulted as a secret.customer_id+policy_folder_idARE API-readable, butAis the blocker. Vaulted so far: cascades-tucson, grabb-durando, reliant, howard-test atclients/<slug>/syncro-agent-installer.sops.yaml(credentials.installer_url).deploy-agentreads from there. - Installer is an MSI (
/dl/msi/), so silent deploy usesmsiexec /i <file>.msi /qn /norestart(NOT the.exe --consolepath). Syncro also offers a.exe(--console --allow-force-reboot); the download link determines which. Verify the exact MSI silent behavior on RMM-TEST-MACHINE before any customer push. - The token embeds an enrollment secret → it is a credential: vault it (per client), never
commit plaintext.
policy_folder_idcomes fromGET /policy_folders?customer_id=N.
2026-07-10 — Phase 1b (breadth: leads / vendors / products / PO / worksheets)
Response shapes — these are all WRAPPED (unlike contacts/contracts): POST /leads →
{"lead":{...}} (.lead.id); POST /vendors → {"vendor":{...}}; POST/PUT /products →
{"product":{...}}; POST /purchase_orders → {"purchase_order":{...}};
POST /purchase_orders/{id}/create_po_line_item → {"po_line_item":{...}};
POST /tickets/{id}/worksheet_results → {"worksheet_result":{...}};
DELETE .../worksheet_results/{id} → {"message":"N: We deleted <title>."}.
PO line items require the product to have maintain_stock:true. create_po_line_item
against a non-stock product returns {"errors":"...ensure that the item ... is set to 'Maintain Stock'."}. Flip the product (PUT /products/{id} maintain_stock:true) first.
Worksheet templates are NOT a standalone endpoint — they live in GET /tickets/settings →
.worksheet_templates[] ({id,title,account_id}). You need a valid worksheet_template_id
to create a worksheet result (e.g. "Virus Removal" = 11341).
PUT /products/{id} requires name AND description (same as create) — 422 without.
No DELETE endpoint for leads, vendors, products, purchase_orders — test records in those
GLOBAL catalogs PERSIST (can't be removed via API; archive/hide in the GUI). Mark test items
[TEST] and set products disabled:true. Created this run (need GUI cleanup): lead 43492446,
vendor 809317, product 23639304 (disabled), PO 2101221 (+ line 8918508).
Minor: POST /leads business_name didn't persist (came back null); first_name/
last_name/email/customer_id did. Re-check the correct field for a lead's company name.
2026-07-10 — Phase 1 (PSA CRUD)
Emailing an invoice/estimate returns 200 "Email sent" even when it delivered to NOBODY.
POST /invoices/{id}/email and POST /estimates/{id}/email take no recipient param —
Syncro picks the recipient from the record server-side. If customer.email is null (and no
contact is a flagged recipient), the API still returns {"message":"Email sent"} HTTP 200 but
nothing is sent. NEVER report an email as delivered based on the 200 alone. Ensure the
customer has a valid email (or a recipient contact) BEFORE emailing, and confirm real
delivery out-of-band when it matters.
customer.email is UNIQUE across the tenant. PUT /customers/{id} with an email already
used elsewhere returns {"success":false,"message":["Email has already been taken"]} and does
NOT set it. For a test customer that must reach a real person whose address is already in
Syncro, use plus-addressing (user+tag@domain) — unique in Syncro, still routes to the inbox
IF the mail tenant has plus-addressing enabled (M365: off by default in some tenants — verify).
PUT /customers/{id} uses two different response shapes — always check for the error shape.
Success → {"customer":{...}}. Validation failure → {"success":false,"message":[...]} with
the field unchanged. Parsing only .customer.email hides the failure (looks like a silent
no-op). Read the raw body / check .success == false on customer writes.
Contacts return a FLAT object, not wrapped. POST /contacts and PUT /contacts/{id}
return {"id":N,"name":...,"email":...} directly — NOT {"contact":{...}}. Parse .id, not
.contact.id. (The wrapped-parse returns null and looks like a failure when the write
succeeded — a blind retry then DUPLICATES the contact. GET-verify, never retry.)
Contact recipient flags (primary, receives_invoices, receives_reports) are NOT settable
via the API — PUT /contacts/{id} accepts them in the body but silently ignores them (stay
null). To designate an invoice-recipient contact, set it in the Syncro GUI.
POST /contracts write succeeds but the response does not echo the created object (fields
parse null). GET /contracts and match to confirm creation — never retry on the null.
GET /contracts?customer_id=N does NOT filter server-side — it returns ALL contracts.
Filter client-side by .customer_id.
Reinforces the standing rule: null/blank fields in a Syncro write response are NOT proof of failure. Syncro has no idempotency, so a blind retry duplicates. Always GET the resource to confirm true state before any follow-up action.
Verified-working this phase (no gotcha, for coverage record)
POST/GET customer; POST contact; POST ticket (+ number); POST ticket comment (hidden);
POST add_line_item; POST invoice (from ticket); POST estimate + line_items + PUT recalc;
POST appointment; POST schedule (paused). All parse under the shapes documented in syncro.md.