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>
This commit is contained in:
2026-04-30 18:44:12 -07:00
parent e000b8c3e8
commit 006eff35d5
2 changed files with 186 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
# 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