Files
claudetools/.claude/memory/syncro_invoice_verification_pattern.md
Mike Swanson 006eff35d5 docs: Syncro invoice verification pattern (lesson from false alarm)
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>
2026-04-30 18:44:12 -07:00

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_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.

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

    // 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.

  • 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