Files
claudetools/.claude/commands/syncro.md
Mike Swanson 9143eb6262 Session log: desertrat.com Mailprotector SBR repair + Syncro API corrections
- Added desertrat.com to /etc/mailprotector_domains on Websvr (outbound SBR now active)
- Created Mailprotector bulk user import CSV (38 desertrat.com accounts/forwarders)
- Created Syncro ticket #32181 + invoice #67437 for Furrier (30 min remote, $81.53)
- Corrected syncro.md skill doc: add_line_item for billing, remove_line_item to delete,
  charge_timer_entry to convert timers, comment DELETE impossible via API
- Created clients/furrier/ with session log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 12:24:15 -07:00

10 KiB

/syncro — Syncro PSA ticket management

Create, update, close, comment on, and bill tickets in Syncro PSA.

Usage

/syncro                                  Show open tickets summary
/syncro ticket <number>                  View ticket details + comments
/syncro create <customer> <subject>      Create new ticket
/syncro update <number> <status>         Update ticket status
/syncro close <number>                   Close/resolve a ticket
/syncro comment <number> <text>          Add a comment to a ticket
/syncro bill <number>                    Add billable time and create invoice
/syncro search <query>                   Search tickets by subject/customer
/syncro customers <query>                Search customers

API Configuration

Base URL: https://computerguru.syncromsp.com/api/v1 API Key: SOPS vault msp-tools/syncro.sops.yamlcredentials.credential Rate limit: 180 requests/minute per IP Docs: https://api-docs.syncromsp.com/

Implementation

When invoked, use the Syncro REST API via curl. All requests include ?api_key=<key> as query parameter (NOT in header — Syncro uses query param auth).

Get API key

API_KEY=$(bash D:/vault/scripts/vault.sh get-field msp-tools/syncro.sops.yaml credentials.credential)
BASE="https://computerguru.syncromsp.com/api/v1"

If vault.sh get-field fails (yq not installed), fall back to:

API_KEY=$(sops -d D:/vault/msp-tools/syncro.sops.yaml | py -c "import sys,yaml; print(yaml.safe_load(sys.stdin)['credentials']['credential'])")

Endpoints reference

Tickets

Operation Method Endpoint Body
List tickets GET /tickets?status=<status>&per_page=25
Get ticket GET /tickets/<id>
Create ticket POST /tickets {"customer_id": N, "subject": "...", "problem_type": "...", "status": "New"}
Update ticket PUT /tickets/<id> {"status": "In Progress", "priority": "..."}
Delete ticket DELETE /tickets/<id>

Ticket statuses: New, In Progress, Waiting on Customer, Waiting on Vendor, Scheduled, Resolved, Invoiced, Closed

Ticket fields (create/update):

  • customer_id (required for create)
  • subject (required for create)
  • problem_type (string, free-form)
  • status (string, one of the statuses above)
  • priority (string)
  • due_date (ISO date)
  • user_id (assign to tech)
  • contact_id (customer contact)
  • ticket_type_id (ticket category)

Comments

Operation Method Endpoint Body
Add comment POST /tickets/<id>/comment {"subject": "Update", "body": "...", "hidden": false, "do_not_email": false}

Comment fields (verified):

  • subject — required; comment header (e.g., "Update", "Resolution", "Internal Note")
  • body — required; comment text (HTML supported)
  • hidden — bool; if true, internal-only (customer can't see)
  • do_not_email — bool; if true, suppresses customer email notification
  • tech — string; overrides the authenticated user's name shown on the comment

Silently ignored (do not use): product_id, minutes_spent, bill_time_now — accepted but not saved. Verified 2026-04-21.

CRITICAL — duplicate prevention: The server has no idempotency. One POST = one comment, always. Duplicates are caused by calling the endpoint twice (retry after a perceived timeout, double tool invocation, etc.). Never retry a POST /comment without first GET /tickets/{id} to confirm the comment did not already land. The Idempotency-Key header is silently ignored.

Comments cannot be deleted via API. No DELETE endpoint exists in the Syncro API for comments — confirmed against official swagger spec. Duplicate comments require manual removal in the GUI.

Do NOT wrap body in {"comment": {...}} — returns 422 "Body can't be blank". POST flat JSON directly.

Customers

Operation Method Endpoint
List/search GET /customers?query=<search>&per_page=25
Get customer GET /customers/<id>
Create customer POST /customers

Billable Line Items

Two verified ways to add billable time. Both produce ticket line items that transfer to invoices.

Option A — Direct line item (simpler):

Operation Method Endpoint
Add line item POST /tickets/<id>/add_line_item
Remove line item POST /tickets/<id>/remove_line_item
Update line item PUT /tickets/<id>/update_line_item
# Add
curl -s -X POST "${BASE}/tickets/${ID}/add_line_item?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"product_id": 1190473, "quantity": 0.5, "price_retail": 150.00, "name": "Labor - Remote Business", "description": "Work description"}'

# Remove
curl -s -X POST "${BASE}/tickets/${ID}/remove_line_item?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"ticket_line_item_id": 12345}'
# Returns: {"success": true, "message": ""}

Option B — Timer then charge (for time-tracking workflows):

# 1. Create timer entry
curl -s -X POST "${BASE}/tickets/${ID}/timer_entry?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"start_at": "ISO8601", "end_at": "ISO8601", "notes": "...", "billable": true, "product_id": 1190473}'

# 2. Charge timer — sets recorded:true and creates linked line item
curl -s -X POST "${BASE}/tickets/${ID}/charge_timer_entry?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"timer_entry_id": N}'

# Delete timer (if needed)
curl -s -X POST "${BASE}/tickets/${ID}/delete_timer_entry?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"timer_entry_id": N}'
# Returns: {"success": true}

add_line_item required fields:

  • name — required (422 if missing)
  • description — required (422 if missing)
  • product_id — labor product ID (see list below)
  • quantity — decimal hours (0.5 = 30 min, 1.0 = 1 hour)
  • price_retailonly price field that saves; price, retail_price, rate, price_cents all silently ignored and leave line at $0.00

Labor product IDs:

  • 1190473 — Labor - Remote Business (standard remote work)
  • 26118 — Labor - Onsite Business
  • 26184 — Labor - Emergency or After Hours Business
  • 9269129 — Labor - Prepaid Project Labor
  • 9269124 — Labor - Internal Labor
  • 26117 — Fee - Travel Time
  • 68055 — Labor - Website Labor

Timer Entries (time tracking reference)

Operation Method Endpoint
Add timer POST /tickets/<id>/timer_entry
Charge timer → line item POST /tickets/<id>/charge_timer_entry
Update timer PUT /tickets/<id>/update_timer_entry
Delete timer POST /tickets/<id>/delete_timer_entry
List timers GET /ticket_timers?ticket_id=<id>

Invoices

Operation Method Endpoint Body
List invoices GET /invoices?per_page=25
Get invoice GET /invoices/<id>
Create from ticket POST /invoices {"ticket_id": N, "customer_id": N, "category": "Standard"}
Delete invoice DELETE /invoices/<id>

"Make Invoice" flow: POST /invoices pulls all add_line_item entries from the ticket into the invoice. Timer entries are NOT included.

Note: The POST /invoices response body does not include line_items — do GET /invoices/{id} to verify line items transferred correctly.

Display formatting

When showing ticket lists, format as:

#32164  New        Jerry Burger           Own cloud thing again
#32163  New        LeeAnn Parkinson       Remote - Jim cant access his email
#32162  Invoiced   Len's Auto Brokerage   Server upgrade

When showing ticket detail, include:

  • Ticket number, subject, status, priority
  • Customer name + contact
  • Created date, due date, last updated
  • Assigned tech
  • Comments (most recent first, truncated to last 5)
  • Line items / billing status

Billing workflow

ALWAYS ask the user for minutes and labor type before logging any time. Never assume a default. ALWAYS show a preview of the comment to the user before posting. Wait for confirmation.

When /syncro bill <number> is called:

  1. Get ticket details
  2. Ask: "How many minutes should I bill, and what labor type? (remote / onsite / emergency / project / internal)"
  3. Draft the comment body and show it to the user for review before posting
  4. Post comment: POST /tickets/{id}/comment
  5. Add billable line item: POST /tickets/{id}/add_line_item with quantity in decimal hours, price_retail, name, description
  6. Create invoice: POST /invoices with {"ticket_id": N, "customer_id": N, "category": "Standard"}
  7. Verify invoice: GET /invoices/{id} to confirm line items transferred
  8. Update ticket status to Invoiced

Correct pattern:

# Step 1: Post comment
curl -s -X POST "${BASE}/tickets/${ID}/comment?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"subject": "Resolution", "body": "...", "hidden": false, "do_not_email": false}'

# Step 2: Add billable line item (convert minutes to decimal hours)
# 60 min = 1.0, 30 min = 0.5, 45 min = 0.75, etc.
curl -s -X POST "${BASE}/tickets/${ID}/add_line_item?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"product_id": 1190473, "quantity": 1.0, "price_retail": 150.00, "name": "Labor - Remote Business", "description": "..."}'

# Step 3: Create invoice
curl -s -X POST "${BASE}/invoices?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"ticket_id": '"${ID}"', "customer_id": '"${CUST}"', "category": "Standard"}'

# Step 4: Verify line items transferred
curl -s "${BASE}/invoices/${INVOICE_ID}?api_key=${API_KEY}" | jq '.invoice.line_items'

# Step 5: Mark ticket Invoiced
curl -s -X PUT "${BASE}/tickets/${ID}?api_key=${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"status": "Invoiced"}'

--labor maps to product IDs: remote → 1190473, onsite → 26118, emergency → 26184, project → 9269129, internal → 9269124, travel → 26117, website → 68055

Error handling

  • 401: API key invalid or expired
  • 404: ticket/customer/invoice not found
  • 422: validation error (show the error message from response body)
  • 429: rate limited (wait 60s and retry)

Integration with session logs

When closing a ticket (/syncro close), offer to create a session log entry in clients/<customer>/session-logs/ documenting what was resolved. Pull the ticket subject, comments, and resolution into a structured log.