Created memory entry documenting correct way to verify ticket-invoice linkage in Syncro API after 2026-04-30 incident where faulty verification script falsely claimed 31 tickets had no invoices (actually 29 had invoices properly, 2 were correctly Non-Billable). Key lessons: - List endpoint does NOT return ticket_id or line_items - Must query individual invoices for full data - Invoice numbers are strings, not integers - Use ticket ID (internal), not ticket number (user-visible) Added to memory index for future GrepAI semantic search. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
186 lines
5.7 KiB
Markdown
186 lines
5.7 KiB
Markdown
# Syncro API: Correct Invoice Verification Pattern
|
|
|
|
**Created:** 2026-04-30
|
|
**Category:** API Integration, Billing Verification
|
|
**Keywords:** syncro, invoice, verification, ticket linkage, billing, false positive
|
|
|
|
## Problem
|
|
|
|
The Syncro API's invoice list endpoint (`/api/v1/invoices?customer_id=X`) **does not return line items or ticket linkage information**. This causes false negatives when trying to verify if a ticket has an attached invoice.
|
|
|
|
## Incorrect Approach (DO NOT USE)
|
|
|
|
```python
|
|
# WRONG - This will always fail to find invoice linkage
|
|
inv_result = requests.get(
|
|
f'https://computerguru.syncromsp.com/api/v1/invoices?customer_id={customer_id}',
|
|
headers={'Authorization': token}
|
|
)
|
|
invoices = inv_result.json().get('invoices', [])
|
|
|
|
# This will NOT work - line_items are not in the list response
|
|
for inv in invoices:
|
|
for item in inv.get('line_items', []): # line_items will be empty or missing
|
|
if item.get('ticket_id') == ticket_id:
|
|
# This code path never executes
|
|
```
|
|
|
|
**Why this fails:**
|
|
- List endpoint returns minimal invoice data
|
|
- No `line_items` array
|
|
- No `ticket_id` field
|
|
- Returns only: number, total, status, created_at, customer_id
|
|
|
|
## Correct Approach (USE THIS)
|
|
|
|
Invoice linkage is stored at the **invoice level, not in line items**. You must query each invoice individually to get the `ticket_id` field.
|
|
|
|
```python
|
|
import requests, json
|
|
|
|
SYNCRO_TOKEN = "your_token_here"
|
|
SYNCRO_BASE = "https://computerguru.syncromsp.com/api/v1"
|
|
|
|
def find_invoice_for_ticket(ticket_id, customer_id):
|
|
"""
|
|
Correctly verify if a ticket has an attached invoice.
|
|
|
|
Args:
|
|
ticket_id: Syncro ticket ID (integer from ticket object, not ticket number)
|
|
customer_id: Syncro customer ID
|
|
|
|
Returns:
|
|
Invoice number (string) if found, None otherwise
|
|
"""
|
|
# Step 1: Get list of invoice numbers for customer
|
|
list_resp = requests.get(
|
|
f'{SYNCRO_BASE}/invoices?customer_id={customer_id}',
|
|
headers={'Authorization': SYNCRO_TOKEN}
|
|
)
|
|
invoices = list_resp.json().get('invoices', [])
|
|
|
|
# Step 2: Query each invoice individually to check ticket_id
|
|
for inv in invoices:
|
|
inv_number = inv.get('number') # Note: this is a STRING
|
|
|
|
# Get full invoice details
|
|
detail_resp = requests.get(
|
|
f'{SYNCRO_BASE}/invoices/{inv_number}',
|
|
headers={'Authorization': SYNCRO_TOKEN}
|
|
)
|
|
|
|
detail_data = detail_resp.json()
|
|
if 'invoice' in detail_data:
|
|
full_invoice = detail_data['invoice']
|
|
|
|
# The ticket_id is at the invoice level, not in line items
|
|
if full_invoice.get('ticket_id') == ticket_id:
|
|
return inv_number
|
|
|
|
return None
|
|
|
|
# Usage example
|
|
ticket_data = requests.get(
|
|
f'{SYNCRO_BASE}/tickets?number=32223',
|
|
headers={'Authorization': SYNCRO_TOKEN}
|
|
).json()
|
|
|
|
ticket = ticket_data['tickets'][0]
|
|
ticket_id = ticket['id'] # Use ID, not number
|
|
customer_id = ticket['customer_id']
|
|
|
|
invoice_num = find_invoice_for_ticket(ticket_id, customer_id)
|
|
if invoice_num:
|
|
print(f"Ticket has invoice #{invoice_num}")
|
|
else:
|
|
print("No invoice found")
|
|
```
|
|
|
|
## Critical Details
|
|
|
|
1. **Invoice numbers are strings, not integers**
|
|
- `inv.get('number')` returns `"67469"` not `67469`
|
|
- Use string comparison: `if str(num) == '67469'`
|
|
|
|
2. **Use ticket ID, not ticket number**
|
|
- Ticket number: `32223` (user-visible)
|
|
- Ticket ID: `109554336` (internal, used for linkage)
|
|
- Invoice `ticket_id` field contains the internal ID
|
|
|
|
3. **Invoice endpoint structure**
|
|
- List: `/api/v1/invoices?customer_id=X` → minimal data
|
|
- Detail: `/api/v1/invoices/{invoice_number}` → full data including `ticket_id`
|
|
|
|
4. **Response structure difference**
|
|
```json
|
|
// List endpoint response
|
|
{
|
|
"invoices": [
|
|
{
|
|
"number": "67469",
|
|
"total": "75.0",
|
|
"status": null,
|
|
"created_at": "2026-04-28T16:36:22.421-07:00"
|
|
// NO ticket_id here
|
|
// NO line_items here
|
|
}
|
|
]
|
|
}
|
|
|
|
// Detail endpoint response (/invoices/67469)
|
|
{
|
|
"invoice": {
|
|
"number": "67469",
|
|
"total": "75.0",
|
|
"status": null,
|
|
"created_at": "2026-04-28T16:36:22.421-07:00",
|
|
"ticket_id": 109554336, // <-- THIS is what you need
|
|
"line_items": [...] // <-- These are also here
|
|
}
|
|
}
|
|
```
|
|
|
|
## Real-World Impact
|
|
|
|
**Case Study (2026-04-30):**
|
|
- Analyzed 31 tickets with zero time entries
|
|
- Used incorrect list-endpoint approach
|
|
- Falsely concluded ALL 31 had no invoices
|
|
- Created false "CRITICAL billing gap" alarm
|
|
- Reality: 29 had proper invoices, 2 were correctly Non-Billable
|
|
- 93.5% success rate misidentified as 0% success
|
|
|
|
**User caught the error immediately:** "That kittle ticket DOES have an invoice attached, perhaps your search isn't working properly?"
|
|
|
|
**Impact:** Nearly triggered unnecessary remediation work, lost credibility, wasted time
|
|
|
|
## Rate Limiting
|
|
|
|
When verifying multiple tickets:
|
|
- List endpoint: 1 call per customer
|
|
- Detail endpoint: 1 call per invoice
|
|
- Add `time.sleep(0.1)` between detail calls
|
|
- For 30 tickets with ~20 invoices each = ~600 API calls
|
|
- Budget 60+ seconds for full verification
|
|
|
|
## When to Use This
|
|
|
|
Use this pattern when:
|
|
- Verifying if a ticket has been invoiced
|
|
- Auditing billing completeness
|
|
- Reconciling tickets marked "Invoiced" status
|
|
- Investigating billing workflow issues
|
|
|
|
**DO NOT assume** invoice linkage exists without checking the detail endpoint.
|
|
|
|
## Related Files
|
|
|
|
- Session log with error analysis: `session-logs/2026-04-30-session.md`
|
|
- Syncro API credentials: `vault:msp-tools/syncro.sops.yaml`
|
|
- Syncro base URL: `https://computerguru.syncromsp.com/api/v1`
|
|
|
|
## See Also
|
|
|
|
- `.claude/CLAUDE.md` — Data integrity rule: "Never use placeholder/fake data"
|
|
- This pattern applies to any API where list ≠ detail responses
|