Compressed memory store 104 -> 71 files via four passes: - Syncro: 19 scattered feedback_syncro_* files merged into 3 rule files (api/billing/workflow) + an on-demand feedback_syncro_history.md for incident detail, quotes, and tech/product ID tables. - Four near-duplicate merges: Howard paste-safety, Pluto build server, Howard backend deferral, IX server access (ssh+tailscale). - Per-cluster rule/state/history split applied to GuruConnect (2->1), Dataforth (3->2), Cascades (7->3), GuruRMM (13->3). - New reference_resource_map.md: single auto-loaded cheatsheet for "do I have access to X and how do I connect from this machine?" - MEMORY.md rewritten to match the new layout. Health: broken backlinks 8->7, overlap clusters 12->5, orphans 17->0.
3.9 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| Syncro API plumbing — headers, endpoints, response shapes, idempotency | Technical mechanics for talking to the Syncro API — required Content-Type header, the no-idempotency rule (always GET before retry), response wrappers, the add_line_item endpoint shape, HTML rendering, and the (now historical) timer_entry response shape. |
|
Rules only. Incident detail, verbatim quotes, ticket numbers, and dates live in feedback_syncro_history — read on-demand when judging an edge case. Billing/product rules: feedback_syncro_billing. Workflow rules: feedback_syncro_workflow.
1. Content-Type header is required on every POST/PUT
Always include -H "Content-Type: application/json". Without it, curl sends application/x-www-form-urlencoded and Syncro returns a 400 HTML page (not JSON). Applies to comments, tickets, line items, estimates, updates.
Ticket comment payloads also need the subject field:
{"subject":"...","body":"...","hidden":true,"do_not_email":true}
2. No idempotency — ALWAYS GET before retrying any POST
Syncro has no idempotency on any endpoint. One POST always creates one record, regardless of whether the client saw an error. A jq parse error, curl error, timeout, or weird-looking response does NOT mean the POST failed — verify first.
Verification before retry:
- Comments:
GET /tickets/{id}and search.ticket.comments[] | select(.subject == "…"). Check ALL comments, not just[-3:]. - Tickets:
GET /customers/{id}/ticketsbefore retrying. - Line items:
GET /tickets/{id}→.ticket.line_items[].
Response wrappers — CRITICAL for jq:
POST /tickets→{"ticket": {...}}→ use.ticket.id.POST /comment→{"comment": {...}}→ use.comment.id.
Hardening: Write payload JSON to a temp file (e.g. tmp/syncro_comment.json) before posting. Avoids shell quoting/encoding failures that masquerade as POST failures on requests that actually succeeded.
Comments cannot be deleted via API — duplicates require manual GUI removal.
3. HTML formatting in comments
Use <br> for line breaks. Do NOT use <ul> or <li> — Syncro's renderer collapses them into one line. For bulleted lists:
<p>
- Item one<br>
- Item two<br>
- Item three
</p>
4. add_line_item endpoint
POST /api/v1/tickets/{internal_ticket_id}/add_line_item
- Path uses the internal ticket ID (e.g.
111387456), NOT the ticket number (32339). Wrong-ID variants (/line_item,/line_items,PUT line_items_attributes) all 404. - Required fields:
name,description,quantity,price,taxable(andproduct_idfor catalog items). Missingname/description→ 422. - Response is flat — parse
.iddirectly (no wrapper).
Example:
curl -X POST "$BASE/tickets/111387456/add_line_item?api_key=$KEY" \
-H "Content-Type: application/json" \
-d '{"product_id":1049360,"name":"Labor- Warranty work","description":"…","quantity":1,"price":0.0,"taxable":false}'
For ad-hoc API testing, use the internal ACG account only (customer ID 15353550).
5. Timer response — HISTORICAL / SUPERSEDED
Timers are no longer part of the ACG Syncro billing workflow (as of 2026-05-21 — see feedback_syncro_billing and feedback_syncro_history). Kept here for the rare manual-timer case.
POST /tickets/{id}/timer_entry returns a FLAT object — parse .id, NOT .timer.id // .timer_entry.id (both resolve to null and break charge_timer_entry, which can trigger a retry → duplicate). charge_timer_entry response is also flat: use .ticket_line_item_id.
Authoritative timer list for a ticket: GET /tickets/{id} → .ticket.ticket_timers[]. The standalone /ticket_timers?ticket_id=N returns global history, not filtered.
Cleanup duplicates: POST /tickets/{id}/delete_timer_entry with {"timer_entry_id": N}.