sync: auto-sync from GURU-5070 at 2026-05-26 07:25:37

Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-05-26 07:25:37
This commit is contained in:
2026-05-26 07:25:41 -07:00
parent f962cb87d0
commit 7a5c12d2af

View File

@@ -16,6 +16,8 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
/syncro customers <query> Search customers
/syncro move-appointment <customer> Find and reschedule an existing appointment
/syncro estimate <customer> <subject> Create ticket + linked estimate with line items and private purchase notes
/syncro schedules <customer> List recurring invoice schedules for a customer
/syncro schedule <id> View a schedule's template and line items
```
## API Configuration
@@ -51,6 +53,8 @@ Create, update, close, comment on, and bill tickets in Syncro PSA.
**Always pass `"taxable": false` explicitly on labor line items.** Labor products are configured with `taxable: false` in Syncro, but `add_line_item` via API does not inherit the product's taxable setting — it posts the line item as `taxable: true` regardless. Always include `"taxable": false` in the payload to match the product's configured value.
**`DELETE /schedules/{id}` destroys the recurring invoice template immediately — no confirmation, no undo.** Past generated invoices are unaffected but future billing stops. The schedule must be recreated manually with all line items if deleted accidentally. NEVER run destructive HTTP method probes against a live customer schedule — use ACG internal account (customer_id 15353550) for any testing. Incident: Russo Law Firm schedule 224454 deleted during API research 2026-05-26; recreated as 509659.
**Appointment dates — always verify day-of-week before the preview.** Day-of-week math is easy to get wrong. Before including any appointment date in a preview, run a live check and display the full day name alongside the date (e.g. "Saturday 2026-05-23", never just "2026-05-23"). The user confirms the day name at the preview step — if the name is wrong, the date is wrong. Incident: #32312 booked Sunday May 24 instead of Saturday May 23 (2026-05-21). Reported by Winter.
```bash
@@ -455,6 +459,14 @@ Every endpoint's response shape, verified against the live API. Parse exactly as
| Add estimate line item | POST `/estimates/{id}/line_items` | `{"estimate": {...}, "line_item": {"id": N, ...}}` | `.line_item.id` |
| Get estimate | GET `/estimates/{id}` | `{"estimate": {"line_items": [...]}}` | `.estimate.line_items[].price` |
| Delete estimate | DELETE `/estimates/{id}` | `{"message": "N: We deleted # NNNN. "}` | — |
| List schedules | GET `/schedules` | `{"schedules": [...], "meta": {...}}` | `.schedules[].id`, `.meta.total_entries` |
| Get schedule | GET `/schedules/{id}` | `{"schedule": {"lines": [...]}}` | `.schedule.id`, `.schedule.lines[].id` |
| Create schedule | POST `/schedules` | `{"schedule": {...}}` | `.schedule.id` |
| Update schedule | PUT `/schedules/{id}` | `{"schedule": {...}}` | `.schedule.next_run`, `.schedule.paused` |
| Delete schedule | DELETE `/schedules/{id}` | `{"success": "deleted"}` | — **NO CONFIRMATION — immediate** |
| Add schedule line | POST `/schedules/{id}/line_items` | `{"schedule_line_item": {...}}` | `.schedule_line_item.id` |
| Update schedule line | PUT `/schedules/{id}/line_items/{li_id}` | `{"schedule_line_item": {...}}` | `.schedule_line_item.quantity`, `.schedule_line_item.price_retail` |
| Delete schedule line | DELETE `/schedules/{id}/line_items/{li_id}` | `{"success": true}` | — |
**Invoice GET line_items field names differ from ticket line_items:** `item` = product name, `name` = description, `price` = unit rate. Do not use `price_retail` when reading invoice line items.
@@ -592,6 +604,106 @@ curl -s -X DELETE "${BASE}/invoices/${INV_ID}?api_key=${API_KEY}"
POST `/invoices` pulls all current line items from the ticket into the invoice automatically. The POST response includes `.invoice.id` and `.invoice.total` — if either is null, GET `/invoices?customer_id=${CUST_ID}&per_page=5` and find the invoice by `ticket_id` match before taking any other action.
#### Recurring Invoice Schedules
Recurring invoice templates are at `/schedules` — **not** `/recurring_invoices` (404). Generated invoices carry a `schedule_id` field linking back to the template. The `recurring_invoice_id` field on invoices is always null; ignore it.
**Hard rule: never run HTTP method probes against a live customer schedule. Always use an ACG internal test schedule (customer_id 15353550) for destructive-method testing. `DELETE /schedules/{id}` has no confirmation and destroys the template immediately — past generated invoices are unaffected but future billing stops.**
Valid `frequency` values (verified): `Monthly`, `Quarterly`, `Annually`, `Weekly`, `Biweekly`. All other strings return `{"error": ["Frequency must be a valid selection"]}`.
```bash
# List all schedules — 50/page, filterable
curl -s "${BASE}/schedules?api_key=${API_KEY}"
curl -s "${BASE}/schedules?customer_id=${CUST_ID}&api_key=${API_KEY}"
curl -s "${BASE}/schedules?paused=true&api_key=${API_KEY}"
# meta: {total_pages, total_entries, per_page}
# Get one schedule (includes full lines array)
curl -s "${BASE}/schedules/${SCHED_ID}?api_key=${API_KEY}" | jq '
{
id: .schedule.id,
name: .schedule.name,
customer_id: .schedule.customer_id,
frequency: .schedule.frequency,
next_run: .schedule.next_run,
paused: .schedule.paused,
subtotal: .schedule.subtotal,
email_customer: .schedule.email_customer,
lines: [.schedule.lines[] | {id, name, quantity, price_retail, product_id}]
}'
# Create schedule — response: {"schedule": {...}}
# Required: customer_id, name, frequency, next_run
SCHED_RESP=$(curl -s -X POST "${BASE}/schedules?api_key=${API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{
"customer_id": CUST_ID,
"name": "Schedule name",
"frequency": "Monthly",
"next_run": "YYYY-MM-DD",
"email_customer": true,
"paused": false,
"snail_mail": false,
"charge_mop": false,
"invoice_unbilled_ticket_charges": false
}
JSON
)
SCHED_ID=$(echo "$SCHED_RESP" | jq -r '.schedule.id')
# Update schedule — any writable field, response: {"schedule": {...}}
# Updatable: name, frequency, next_run, paused, email_customer, snail_mail,
# charge_mop, invoice_unbilled_ticket_charges
curl -s -X PUT "${BASE}/schedules/${SCHED_ID}?api_key=${API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @- <<JSON
{"next_run": "YYYY-MM-DD", "paused": false}
JSON
# Delete schedule — IMMEDIATE, no confirmation, response: {"success": "deleted"}
# ONLY run this against ACG internal test schedules unless explicitly directed by user
curl -s -X DELETE "${BASE}/schedules/${SCHED_ID}?api_key=${API_KEY}"
# Add line item to schedule — response: {"schedule_line_item": {...}}
# Required: name, description, product_id, quantity, price_retail
# Note: taxable not required but include it; defaults unclear
LI_RESP=$(curl -s -X POST "${BASE}/schedules/${SCHED_ID}/line_items?api_key=${API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @- <<JSON
{
"product_id": ${PRODUCT_ID},
"name": "Product name",
"description": "Line item description",
"quantity": "1.0",
"price_retail": ${RATE},
"taxable": false
}
JSON
)
LI_ID=$(echo "$LI_RESP" | jq -r '.schedule_line_item.id')
# Update schedule line item — response: {"schedule_line_item": {...}}
# Updatable: name, description, quantity, price_retail, taxable
curl -s -X PUT "${BASE}/schedules/${SCHED_ID}/line_items/${LI_ID}?api_key=${API_KEY}" \
-H "Content-Type: application/json" \
--data-binary @- <<JSON
{"quantity": "3.0", "price_retail": ${NEW_RATE}, "name": "Product name", "description": "Updated description"}
JSON
# Delete schedule line item — response: {"success": true}
curl -s -X DELETE "${BASE}/schedules/${SCHED_ID}/line_items/${LI_ID}?api_key=${API_KEY}"
```
**GET single line item (`GET /schedules/{id}/line_items/{li_id}`) returns 404.** Read line items via `GET /schedules/{id}` → `.schedule.lines[]`.
**Schedule object fields:** `id`, `account_id`, `customer_id`, `name`, `frequency`, `next_run`, `paused`, `subtotal`, `email_customer`, `snail_mail`, `charge_mop`, `invoice_unbilled_ticket_charges`, `last_invoice_paid`, `lines[]`
**Schedule line item fields:** `id`, `schedule_id`, `product_id`, `product_category`, `name`, `description`, `quantity`, `price_retail` / `retail_cents`, `price_cost` / `cost_cents`, `taxable`, `position`, `one_time_charge`, `device_ids`, `recurring_type_id` (Pax8 subscription UUID link), `asset_type_id`, `user_id`, plus Syncro Backup-specific fields (`syncro_backup_billing_type`, `syncro_backup_credit_type`, `syncro_backup_bill_all`, `syncro_backup_bill_after_licenses`, `syncro_backup_bill_after_storage`), `vendor_name`, `vendor_product_name`, `bill_all_units`, `bill_units_threshold`, `include_zero_charge`
**Pax8 link field:** `recurring_type_id` on a schedule line item holds the Pax8 subscription UUID. Pax8 sets this when syncing subscriptions and uses it to identify which line to PUT when a subscription changes quantity or price.
#### Estimates
Estimates (quotes) always require a linked ticket — create the ticket first, then the estimate with `ticket_id` set. This enables private notes (purchase links, sourcing details) on the ticket using the standard hidden comment endpoint. Estimates created without a ticket have no notes surface accessible via API. Verified 2026-05-22 against ACG internal account.