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>
5.7 KiB
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)
# 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_itemsarray - No
ticket_idfield - 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.
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
-
Invoice numbers are strings, not integers
inv.get('number')returns"67469"not67469- Use string comparison:
if str(num) == '67469'
-
Use ticket ID, not ticket number
- Ticket number:
32223(user-visible) - Ticket ID:
109554336(internal, used for linkage) - Invoice
ticket_idfield contains the internal ID
- Ticket number:
-
Invoice endpoint structure
- List:
/api/v1/invoices?customer_id=X→ minimal data - Detail:
/api/v1/invoices/{invoice_number}→ full data includingticket_id
- List:
-
Response structure difference
// 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