Troubleshooting

This guide helps you resolve common issues when using ServicePytan with the ServiceTitan API.

Authentication Issues

“Invalid Client Credentials” Error

Problem: Authentication fails with invalid credentials error.

Solutions:

  1. Verify Credentials:

    import servicepytan
    
    # Check your configuration
    conn = servicepytan.auth.servicepytan_connect(config_file="./config.json")
    print("Client ID:", conn.get('SERVICETITAN_CLIENT_ID', 'Not set'))
    print("Tenant ID:", conn.get('SERVICETITAN_TENANT_ID', 'Not set'))
    # Never print secrets in production!
    
  2. Check Credential Sources:

    • Ensure CLIENT_ID and CLIENT_SECRET match in ServiceTitan

    • Verify APP_KEY and TENANT_ID are correct

    • Check if API application is active in ServiceTitan

  3. Validate Environment:

    # Ensure you're using the correct environment
    conn = servicepytan.auth.servicepytan_connect(
        api_environment="production"  # or "integration"
    )
    

“Application Not Found” Error

Problem: API returns application not found error.

Solutions:

  1. Verify APP_ID and APP_KEY in Developer Portal

  2. Ensure API application is published and active

  3. Check tenant association in ServiceTitan settings

Environment Variable Issues

Problem: Environment variables not loading correctly.

Solutions:

  1. Check Variable Names:

    # Verify environment variables are set
    echo $SERVICETITAN_CLIENT_ID
    echo $SERVICETITAN_TENANT_ID
    
  2. Use .env File:

    # Create .env file in project root
    cat > .env << EOF
    SERVICETITAN_CLIENT_ID=your_client_id
    SERVICETITAN_CLIENT_SECRET=your_client_secret
    SERVICETITAN_APP_KEY=your_app_key
    SERVICETITAN_TENANT_ID=your_tenant_id
    EOF
    
  3. Manual Loading:

    from dotenv import load_dotenv
    import os
    
    load_dotenv()
    print("Variables loaded:", bool(os.getenv('SERVICETITAN_CLIENT_ID')))
    

API Request Issues

“Rate Limit Exceeded” Error

Problem: API returns 429 rate limit errors.

Solutions:

  1. Implement Retry Logic:

    import time
    import servicepytan
    
    def safe_api_call(endpoint, options, max_retries=3):
        for attempt in range(max_retries):
            try:
                return endpoint.get_all(options)
            except requests.HTTPError as e:
                if e.response.status_code == 429:
                    wait_time = 2 ** attempt  # Exponential backoff
                    print(f"Rate limited, waiting {wait_time} seconds...")
                    time.sleep(wait_time)
                else:
                    raise
        raise Exception("Max retries exceeded")
    
  2. Reduce Request Frequency:

    • Increase page sizes (up to 500 for most endpoints)

    • Add delays between requests

    • Use caching for repeated data

“Request Timeout” Error

Problem: API requests timeout before completing.

Solutions:

  1. Reduce Page Size:

    # Instead of large pages
    options = {"pageSize": 50}  # Smaller, more reliable
    
  2. Add Request Timeouts:

    import requests
    
    # Custom timeout handling
    try:
        data = endpoint.get_all(options)
    except requests.Timeout:
        print("Request timed out, trying smaller batch...")
    
  3. Break Into Smaller Date Ranges:

    # Instead of large date ranges
    jobs = data_service.get_jobs_completed_between(
        "2024-01-01", "2024-01-07"  # Weekly chunks
    )
    

“Permission Denied” Error

Problem: API returns 403 permission denied errors.

Solutions:

  1. Check API Application Permissions:

    • Verify endpoint access in ServiceTitan Developer Portal

    • Ensure required scopes are granted

  2. Validate Tenant Access:

    # Test basic access
    endpoint = servicepytan.Endpoint("settings", "business-units", conn=conn)
    business_units = endpoint.get_all({"pageSize": 1})
    print("Access confirmed:", len(business_units) >= 0)
    
  3. Review Integration Settings:

    • Check if integration is active

    • Verify correct tenant association

Data Issues

Empty Results When Data Expected

Problem: API returns empty results when data should exist.

Solutions:

  1. Check Date Formats:

    # Ensure correct timezone and format
    from datetime import datetime
    import pytz
    
    # Convert to UTC for API
    local_date = datetime(2024, 1, 1)
    utc_date = pytz.timezone('America/New_York').localize(local_date).astimezone(pytz.UTC)
    formatted_date = utc_date.strftime("%Y-%m-%dT%H:%M:%SZ")
    
  2. Verify Filter Parameters:

    # Debug filters step by step
    options = {"pageSize": 10}  # Start simple
    all_jobs = endpoint.get_all(options)
    print(f"Total jobs without filters: {len(all_jobs)}")
    
    # Add filters gradually
    options["jobStatus"] = "Completed"
    filtered_jobs = endpoint.get_all(options)
    print(f"Completed jobs: {len(filtered_jobs)}")
    
  3. Check Environment Data:

    # Ensure you're checking the right environment
    print(f"Using environment: {conn.get('SERVICETITAN_API_ENVIRONMENT')}")
    print(f"API Root: {conn.get('api_root')}")
    

Incorrect Date/Time Data

Problem: Date and time values appear incorrect.

Solutions:

  1. Configure Timezone:

    conn = servicepytan.auth.servicepytan_connect(
        timezone="America/New_York"  # Set your local timezone
    )
    
  2. Parse Dates Correctly:

    from datetime import datetime
    import pytz
    
    # Parse ServiceTitan date strings
    def parse_st_date(date_string, local_tz="America/New_York"):
        utc_date = datetime.fromisoformat(date_string.replace('Z', '+00:00'))
        local_tz = pytz.timezone(local_tz)
        return utc_date.astimezone(local_tz)
    

Performance Issues

Slow API Responses

Problem: API requests take too long to complete.

Solutions:

  1. Optimize Page Sizes:

    # Find optimal page size for your use case
    page_sizes = [50, 100, 200, 500]
    for size in page_sizes:
        start_time = time.time()
        options = {"pageSize": size}
        result = endpoint.get_page(options, page=1)
        duration = time.time() - start_time
        print(f"Page size {size}: {duration:.2f}s for {len(result['data'])} records")
    
  2. Use Specific Filters:

    # More specific filters = faster responses
    options = {
        "pageSize": 200,
        "active": "true",
        "jobStatus": "Completed",
        "completedOnOrAfter": "2024-01-01T00:00:00Z"
    }
    
  3. Parallel Processing:

    from concurrent.futures import ThreadPoolExecutor
    
    def get_jobs_by_status(status):
        endpoint = servicepytan.Endpoint("jpm", "jobs", conn=conn)
        return endpoint.get_all({"jobStatus": status})
    
    statuses = ["Completed", "Scheduled", "InProgress"]
    with ThreadPoolExecutor(max_workers=3) as executor:
        results = list(executor.map(get_jobs_by_status, statuses))
    

Memory Usage Issues

Problem: Application uses too much memory with large datasets.

Solutions:

  1. Process in Chunks:

    def process_jobs_in_chunks(start_date, end_date, chunk_size=1000):
        page = 1
        while True:
            options = {
                "pageSize": chunk_size,
                "page": page,
                "completedOnOrAfter": start_date,
                "completedBefore": end_date
            }
            
            chunk = endpoint.get_page(options, page=page)
            if not chunk['data']:
                break
                
            # Process chunk immediately
            yield chunk['data']
            page += 1
    
    for job_chunk in process_jobs_in_chunks("2024-01-01", "2024-12-31"):
        # Process each chunk
        for job in job_chunk:
            # Do something with job
            pass
    
  2. Use Generators:

    def stream_jobs(options):
        page = 1
        while True:
            page_options = {**options, "page": page}
            result = endpoint.get_page(page_options, page=page)
            
            for job in result['data']:
                yield job
                
            if len(result['data']) < options.get('pageSize', 50):
                break
            page += 1
    
    # Memory-efficient processing
    for job in stream_jobs({"jobStatus": "Completed"}):
        print(job['id'])
    

Reports Issues

“Report Not Found” Error

Problem: Cannot access custom reports.

Solutions:

  1. Verify Report ID:

    from servicepytan.reports import get_report_list
    
    # List available reports
    reports = get_report_list("operations", conn=conn)
    for report in reports['data']:
        print(f"{report['name']}: {report['id']}")
    
  2. Check Report Category:

    from servicepytan.reports import get_report_categories
    
    categories = get_report_categories(conn=conn)
    for category in categories['data']:
        print(f"Category: {category['name']}")
    

Report Parameters Validation

Problem: Report parameters are rejected.

Solutions:

  1. Check Parameter Types:

    report = servicepytan.Report("operations", "report_id", conn=conn)
    
    # Show required parameters
    report.show_param_types()
    
    # Get parameter metadata
    print(report.metadata.get('parameters', []))
    
  2. Validate Dynamic Sets:

    from servicepytan.reports import get_dynamic_set_list
    
    # Get valid values for dynamic parameters
    business_units = get_dynamic_set_list("business-units", conn=conn)
    print("Valid business unit IDs:", [unit[0] for unit in business_units])
    

Development Environment Setup

Module Import Issues

Problem: Cannot import servicepytan modules.

Solutions:

  1. Verify Installation:

    pip list | grep servicepytan
    pip install --upgrade servicepytan
    
  2. Check Python Path:

    import sys
    print("Python path:", sys.path)
    
    try:
        import servicepytan
        print("ServicePytan version:", servicepytan.__version__)
    except ImportError as e:
        print("Import error:", e)
    
  3. Virtual Environment:

    # Create clean environment
    python -m venv venv_servicepytan
    source venv_servicepytan/bin/activate  # Linux/Mac
    # or venv_servicepytan\Scripts\activate  # Windows
    
    pip install servicepytan
    

Dependency Conflicts

Problem: Package dependency conflicts.

Solutions:

  1. Check Dependencies:

    pip check
    pip list --outdated
    
  2. Install Specific Versions:

    # Create requirements.txt with specific versions
    pip freeze > requirements.txt
    
    # Or install specific version
    pip install servicepytan==0.3.2
    

Debugging Tips

Enable Verbose Logging

import logging

# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger('servicepytan')
logger.setLevel(logging.DEBUG)

# Now all API requests will be logged
conn = servicepytan.auth.servicepytan_connect()

Test Connection

def test_servicepytan_connection():
    """Comprehensive connection test"""
    try:
        # Test authentication
        conn = servicepytan.auth.servicepytan_connect()
        print("✅ Configuration loaded")
        
        # Test token retrieval
        token = servicepytan.auth.get_auth_token(conn)
        print("✅ Authentication successful")
        
        # Test API access
        endpoint = servicepytan.Endpoint("settings", "business-units", conn=conn)
        data = endpoint.get_all({"pageSize": 1})
        print("✅ API access confirmed")
        
        # Test DataService
        data_service = servicepytan.DataService(conn=conn)
        print("✅ DataService initialized")
        
        print("🎉 All tests passed!")
        
    except Exception as e:
        print(f"❌ Test failed: {e}")
        import traceback
        traceback.print_exc()

test_servicepytan_connection()

API Response Inspection

def debug_api_response(endpoint, options):
    """Debug API responses step by step"""
    import json
    
    print(f"Testing endpoint: {endpoint.folder}/{endpoint.endpoint}")
    print(f"Options: {json.dumps(options, indent=2)}")
    
    try:
        # Test single page first
        response = endpoint.get_page(options, page=1)
        print(f"✅ Page 1: {len(response.get('data', []))} records")
        print(f"Total pages: {response.get('totalPages', 'unknown')}")
        print(f"Total count: {response.get('totalCount', 'unknown')}")
        
        if response.get('data'):
            print("Sample record keys:", list(response['data'][0].keys()))
            
    except Exception as e:
        print(f"❌ Error: {e}")
        import traceback
        traceback.print_exc()

# Usage
conn = servicepytan.auth.servicepytan_connect()
endpoint = servicepytan.Endpoint("jpm", "jobs", conn=conn)
debug_api_response(endpoint, {"pageSize": 5, "active": "true"})

Getting Help

If you can’t resolve your issue:

  1. Check the GitHub Issues: https://github.com/elliotpalmer/servicepytan/issues

  2. ServiceTitan Developer Docs: https://developer.servicetitan.io/

  3. Create a New Issue: Include:

    • ServicePytan version

    • Python version

    • Complete error message

    • Minimal code example

    • Expected vs actual behavior


Back to Examples | Back to Getting Started