# 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