Files
claudetools/.claude/memory/feedback_syncro_api.md
Mike Swanson 0c000109dc chore(memory): consolidate scattered feedback/project/reference files
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.
2026-06-01 16:25:45 -07:00

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.
type
feedback

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}/tickets before 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 (and product_id for catalog items). Missing name/description → 422.
  • Response is flat — parse .id directly (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}.