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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user