- 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>
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.yaml → credentials.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 notificationtech— 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_retail— only price field that saves;price,retail_price,rate,price_centsall silently ignored and leave line at $0.00
Labor product IDs:
1190473— Labor - Remote Business (standard remote work)26118— Labor - Onsite Business26184— Labor - Emergency or After Hours Business9269129— Labor - Prepaid Project Labor9269124— Labor - Internal Labor26117— Fee - Travel Time68055— 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:
- Get ticket details
- Ask: "How many minutes should I bill, and what labor type? (remote / onsite / emergency / project / internal)"
- Draft the comment body and show it to the user for review before posting
- Post comment:
POST /tickets/{id}/comment - Add billable line item:
POST /tickets/{id}/add_line_itemwith quantity in decimal hours,price_retail,name,description - Create invoice:
POST /invoiceswith{"ticket_id": N, "customer_id": N, "category": "Standard"} - Verify invoice:
GET /invoices/{id}to confirm line items transferred - 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.