Skip to content

Error Handling

The SDK raises typed exceptions for every failure mode so your code can react precisely to each problem. Every exception inherits from SonnysError, letting you catch broadly or narrow down to specific error types depending on your needs.

Exception Hierarchy

All SDK exceptions follow this inheritance tree. Catching a parent class also catches all of its children.

SonnysError                     # Base for all SDK errors
  APIError                      # Base for API-related errors
    APIConnectionError          # Network failures (DNS, refused, etc.)
      APITimeoutError           # Request timed out before a response
    APIStatusError              # HTTP error responses (4xx / 5xx)
      AuthError                 # 403 -- credential or authorization problem
      RateLimitError            # 429 -- rate limit exceeded
      ValidationError           # 400 / 422 -- bad request parameters
      NotFoundError             # 404 -- resource not found
      ServerError               # 5xx -- server-side failure

Catching Errors

Catch-all

The simplest approach catches SonnysError to handle any SDK failure in one place:

from sonnys_data_client import SonnysClient, SonnysError

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    try:
        transactions = client.transactions.list(
            startDate="2025-06-01",
            endDate="2025-06-30",
        )
    except SonnysError as e:
        print(f"SDK error: {e}")

Catch by category

Separate network problems from HTTP errors to apply different recovery strategies:

from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import (
    APIConnectionError,
    APIStatusError,
    SonnysError,
)

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    try:
        customers = client.customers.list()
    except APIConnectionError:
        print("Network problem -- check your connection")
    except APIStatusError as e:
        print(f"HTTP {e.status_code}: {e.message}")
    except SonnysError as e:
        print(f"Other SDK error: {e}")

Catch specific exceptions

For fine-grained control, catch each exception type individually:

from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import (
    AuthError,
    NotFoundError,
    RateLimitError,
    ServerError,
    ValidationError,
    SonnysError,
)

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    try:
        customer = client.customers.get("12345")
    except AuthError:
        print("Invalid credentials or unauthorized site")
    except NotFoundError:
        print("Customer not found")
    except RateLimitError:
        print("Rate limit exceeded after retries")
    except ValidationError as e:
        print(f"Bad request: {e.message}")
    except ServerError:
        print("Server error -- try again later")
    except SonnysError as e:
        print(f"Unexpected SDK error: {e}")

Tip

Order specific exceptions before general ones -- Python matches the first except clause. Place AuthError before APIStatusError, and APIStatusError before SonnysError.

Error Attributes

All APIStatusError subclasses (AuthError, RateLimitError, ValidationError, NotFoundError, ServerError) expose these attributes:

Attribute Type Description
message str Human-readable error description from the API
status_code int HTTP status code (e.g., 403, 404, 429)
body dict \| None Full parsed JSON response body from the API
error_type str \| None Sonny's API error type string (e.g., "BadClientCredentialsError")

Access these attributes in your except block to log details or branch on the specific error type:

from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import APIStatusError

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    try:
        transactions = client.transactions.list(
            startDate="2025-06-01",
            endDate="2025-06-30",
        )
    except APIStatusError as e:
        print(f"Status:     {e.status_code}")
        print(f"Message:    {e.message}")
        print(f"Error type: {e.error_type}")
        print(f"Body:       {e.body}")

Readable error strings

Printing or logging an APIStatusError directly (e.g., print(e)) produces a formatted string with all key details:

[HTTP 422] (PayloadValidationError) site: Only 1 site can be selected.
API response body: {'error': {'type': 'PayloadValidationError', ...}}

This makes tracebacks and log output immediately actionable without needing to inspect individual attributes.

API error_type values

The error_type attribute contains the raw error type string returned by the Sonny's API. The SDK maps these to exception classes automatically:

API error_type HTTP Status SDK Exception
MissingClientCredentialsError 403 AuthError
BadClientCredentialsError 403 AuthError
MismatchCredentialsError 403 AuthError
NotAuthorizedError 403 AuthError
BadSiteCredentialsError 403 AuthError
NotAuthorizedSiteCredentialsError 403 AuthError
NotAuthorizedSiteArgsError 403 AuthError
RequestRateExceedError 429 RateLimitError
PayloadValidationError 422 ValidationError
InvalidPayloadRequestTimestampError 400 ValidationError
EntityNotFoundError 404 NotFoundError
UnexpectedFailure 400 APIError
ServerUnexpectedFailure 500 ServerError

Per-Exception-Type Guidance

AuthError

When it is raised: The API returns HTTP 403 for any credential or authorization problem.

Common causes:

  • Wrong api_id or api_key values (e.g., copied from the wrong environment or database)
  • site_code not authorized for the given API ID -- each API ID is provisioned for specific sites, and using a site code from a different organization triggers NotAuthorizedSiteCredentialsError
  • Missing credentials -- forgetting to pass api_id or api_key to the constructor
  • API ID/key pair mismatch -- using the API ID from one database (e.g., WashU) with the API key from another (e.g., Icon)
  • Passing site as a query parameter instead of using the site_code constructor argument

Recommended handling: Do not retry -- fix your credentials or site code configuration. Check that your api_id, api_key, and site_code match what was provisioned by Sonny's. Use error_type to pinpoint the exact problem.

from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import AuthError

with SonnysClient(
    api_id="your-api-id",
    api_key="your-api-key",
    site_code="JOLIET",
) as client:
    try:
        sites = client.sites.list()
    except AuthError as e:
        print(f"Auth failed: {e.message}")
        print(f"Error type: {e.error_type}")

        # Branch on the specific auth failure
        if e.error_type == "BadClientCredentialsError":
            print("Check your api_id and api_key values")
        elif e.error_type == "NotAuthorizedSiteCredentialsError":
            print("This site_code is not authorized for your API ID")
        elif e.error_type == "MissingClientCredentialsError":
            print("Credentials were not provided")
        elif e.error_type == "MismatchCredentialsError":
            print("API ID and key do not belong to the same account")

Warning

If you operate multiple databases (e.g., WashU and Icon) with separate credentials, double-check that you are not mixing API IDs and keys across client instances. A MismatchCredentialsError means the ID and key belong to different accounts.

RateLimitError

When it is raised: The API returns HTTP 429 and all built-in retries have been exhausted (default: 3 attempts with exponential backoff).

Common causes:

  • Running an analytics script while a scheduled cron job is also pulling data against the same API ID
  • Bulk-exporting transactions for multiple sites in a tight loop without pausing between sites
  • Multiple SonnysClient instances sharing the same api_id -- each instance has its own rate limiter, but the API enforces a single 20 req/15s limit per API ID
  • Burst traffic from a loop that calls client.transactions.get() for hundreds of individual transaction IDs

Recommended handling: The SDK already retries 429 responses with exponential backoff (see Built-in Retry Behavior). If this exception reaches your code, the situation is severe. Back off for a longer period before retrying, or stagger your scripts.

import time
import logging
from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import RateLimitError

# Enable logging to see when 429 retries happen internally
logging.basicConfig(level=logging.WARNING)

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    try:
        transactions = client.transactions.list(
            startDate="2025-06-01",
            endDate="2025-06-30",
        )
    except RateLimitError as e:
        print(f"Rate limited after retries: {e.message}")
        # All 3 built-in retries failed -- back off for 30 seconds
        time.sleep(30)

Tip

If you regularly hit rate limits, check whether multiple processes share the same API ID. Stagger scheduled jobs by at least 60 seconds, or increase max_retries on the client to allow more backoff time.

ValidationError

When it is raised: The API returns HTTP 400 or 422 for invalid request parameters.

Common causes:

  • Date format "06/01/2025" instead of ISO 8601 ("2025-06-01") -- this is the most common validation error and triggers InvalidPayloadRequestTimestampError. The SDK auto-converts ISO 8601 strings to Unix timestamps, but non-ISO formats like MM/DD/YYYY are not supported.
  • Passing a transaction type string that does not exist to list_by_type() (e.g., "membership" instead of "recurring")
  • Sending endDate earlier than startDate
  • Missing the required site parameter on load_job() when the API ID has access to multiple sites
  • Missing required parameters on endpoints that enforce them

Recommended handling: Do not retry -- fix the request parameters. Use ISO 8601 date strings ("2025-06-01") or Unix timestamps. Check parameter names and allowed values. Use error_type to distinguish between timestamp errors and general payload errors.

from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import ValidationError

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    try:
        # load_job() requires explicit site= when the API ID has multi-site access
        results = client.transactions.load_job(
            startDate="2025-06-15",
            endDate="2025-06-16",
            # Missing site= parameter!
        )
    except ValidationError as e:
        print(f"Validation error: {e.message}")
        print(f"Error type: {e.error_type}")

        if e.error_type == "PayloadValidationError":
            print("Check parameter names and values")
            print(f"Full response: {e.body}")
        elif e.error_type == "InvalidPayloadRequestTimestampError":
            print("Use ISO 8601 date format (YYYY-MM-DD) or Unix timestamps")

Info

The PayloadValidationError type may return multiple error messages in the response body. The SDK joins them into a single message string separated by semicolons. Check e.body for the original "messages" array if you need to inspect each validation failure individually.

NotFoundError

When it is raised: The API returns HTTP 404 when the requested resource does not exist.

Common causes:

  • Transaction ID from a different site than the one configured via site_code -- IDs are scoped per site
  • Customer ID that was valid in one database but does not exist in another (e.g., looking up a WashU customer ID against the Icon database)
  • Deleted recurring account or gift card that no longer exists in the system
  • Typo or truncated ID string

Recommended handling: Verify the ID is correct and belongs to the configured site. In batch workflows where you iterate over a list of IDs, catch NotFoundError and skip or log the missing record rather than aborting the entire loop.

from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import NotFoundError

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    # Gracefully handle missing records in a batch lookup
    customer_ids = ["12345", "99999", "67890"]
    found = []

    for cid in customer_ids:
        try:
            customer = client.customers.get(cid)
            found.append(customer)
        except NotFoundError:
            print(f"Customer {cid} not found, skipping")
            # e.error_type will be "EntityNotFoundError"

    print(f"Found {len(found)} of {len(customer_ids)} customers")

ServerError

When it is raised: The API returns HTTP 5xx indicating a server-side failure.

Common causes:

  • Sonny's API scheduled maintenance window -- the API may return 500 or 503 during planned updates
  • Intermittent 500 errors on large date-range queries or load_job() requests that stress the backend
  • ServerUnexpectedFailure -- an unexpected crash on the API side
  • 502/503 responses from upstream infrastructure (load balancer, gateway)

Recommended handling: Retry after a delay. Server errors are often transient. If they persist beyond a few minutes, check with Sonny's support for known outages. For automated pipelines, implement retry with backoff (see Custom Retry Patterns).

import time
from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import ServerError

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    max_attempts = 3
    for attempt in range(max_attempts):
        try:
            transactions = client.transactions.list(
                startDate="2025-06-01",
                endDate="2025-06-30",
            )
            break  # Success
        except ServerError as e:
            print(f"Server error ({e.status_code}): {e.message}")
            if attempt < max_attempts - 1:
                delay = 5 * (2 ** attempt)
                print(f"Retrying in {delay}s...")
                time.sleep(delay)
            else:
                print("All retries exhausted")
                raise

APIConnectionError

When it is raised: The HTTP request fails before reaching the server -- no response is received.

Common causes:

  • No internet connection on the machine running the script
  • DNS resolution failure for trigonapi.sonnyscontrols.com -- common on new server deployments where DNS is not yet configured
  • Corporate firewall or proxy blocking outbound HTTPS traffic to the Sonny's API domain
  • VPN disconnection mid-request
  • Running inside a Docker container or CI environment without outbound network access

Recommended handling: Check network connectivity. This error does not have status_code or body attributes since no HTTP response was received. For automated pipelines, retry a few times with a delay -- the network issue may be transient.

import time
from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import APIConnectionError

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    try:
        sites = client.sites.list()
    except APIConnectionError as e:
        print(f"Connection failed: {e.message}")
        print("Troubleshooting steps:")
        print("  1. Check internet connectivity")
        print("  2. Verify DNS resolves: nslookup trigonapi.sonnyscontrols.com")
        print("  3. Check firewall allows HTTPS to trigonapi.sonnyscontrols.com")
        print("  4. If behind a proxy, configure requests proxy settings")

APITimeoutError

When it is raised: The HTTP request was sent but no response arrived within the timeout period. Also raised when load_job() polling exceeds the configured timeout.

Common causes:

  • load_job() on a high-volume site with a full day of transactions -- the batch job takes longer than the default 300-second timeout to complete
  • Very large list() or list_v2() queries spanning months of data at a busy site
  • Network congestion or high latency between your server and trigonapi.sonnyscontrols.com
  • API under heavy load from other consumers during peak hours

Recommended handling: For load_job(), increase the timeout parameter or use a shorter date range. For list methods, narrow the date range. Consider splitting multi-month queries into smaller chunks.

from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import APITimeoutError

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    try:
        # Large export that may time out
        results = client.transactions.load_job(
            startDate="2025-06-01",
            endDate="2025-06-02",
            timeout=600.0,  # 10 minutes instead of default 5
        )
    except APITimeoutError:
        print("Job timed out -- try a shorter date range or increase timeout")
        # Fallback: split into smaller chunks
        from datetime import date, timedelta
        start = date(2025, 6, 1)
        end = date(2025, 6, 2)
        midpoint = start + (end - start) / 2
        print(f"Try splitting: {start} to {midpoint} and {midpoint} to {end}")

Note

APITimeoutError is a subclass of APIConnectionError. If you catch APIConnectionError, it will also catch timeouts. Use the specific APITimeoutError class when you need to distinguish timeouts from other connection failures.

Built-in Retry Behavior

The SDK includes two layers of automatic protection against rate limiting.

Pre-request rate limiting

Every request passes through a sliding window rate limiter before it is sent. The limiter enforces a maximum of 20 requests per 15-second window. If you are at capacity, the client automatically sleeps until a slot opens -- no exception is raised.

429 retry with exponential backoff

If the API returns HTTP 429 despite the client-side limiter (e.g., another script shares the same API ID), the SDK retries with exponential backoff:

Attempt Delay
1 1 second
2 2 seconds
3 4 seconds

After max_retries attempts (default 3), the SDK raises RateLimitError.

Note

You can configure max_retries when constructing the client:

client = SonnysClient(
    api_id="your-api-id",
    api_key="your-api-key",
    max_retries=5,  # Allow up to 5 retry attempts for 429s
)

What is NOT retried automatically

The SDK only retries HTTP 429 responses. These errors are not retried:

  • APIConnectionError -- Network failures (no response received)
  • APITimeoutError -- Request timeouts
  • AuthError (403) -- Credential or authorization problems
  • ValidationError (400/422) -- Invalid request parameters
  • NotFoundError (404) -- Resource does not exist
  • ServerError (5xx) -- Server-side failures

If you need retry logic for these errors, implement it yourself using the patterns below.

Custom Retry Patterns

Retry on transient server errors

Server errors (500, 502, 503) are often transient. Wrap your call in a simple retry loop with backoff:

import time
from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import ServerError

def fetch_with_retry(client, retries=3, base_delay=2.0):
    for attempt in range(retries):
        try:
            return client.transactions.list(
                startDate="2025-06-01",
                endDate="2025-06-30",
            )
        except ServerError as e:
            if attempt == retries - 1:
                raise
            delay = base_delay * (2 ** attempt)
            print(f"Server error ({e.status_code}), retrying in {delay}s...")
            time.sleep(delay)

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    transactions = fetch_with_retry(client)

Retry on connection and timeout errors

Network problems can be intermittent. Retry APIConnectionError and APITimeoutError with a short delay:

import time
from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import APIConnectionError, APITimeoutError

def fetch_with_network_retry(client, retries=3, delay=5.0):
    for attempt in range(retries):
        try:
            return client.customers.list()
        except (APIConnectionError, APITimeoutError) as e:
            if attempt == retries - 1:
                raise
            print(f"{type(e).__name__}, retrying in {delay}s...")
            time.sleep(delay)

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    customers = fetch_with_network_retry(client)

Production retry with tenacity

For production systems, use the tenacity library for configurable, decorator-based retry logic:

pip install tenacity
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from sonnys_data_client import SonnysClient
from sonnys_data_client._exceptions import APIConnectionError, APITimeoutError, ServerError

@retry(
    retry=retry_if_exception_type((ServerError, APIConnectionError, APITimeoutError)),
    stop=stop_after_attempt(5),
    wait=wait_exponential(multiplier=1, min=2, max=30),
)
def fetch_transactions(client):
    return client.transactions.list(
        startDate="2025-06-01",
        endDate="2025-06-30",
    )

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    transactions = fetch_transactions(client)

Warning

Never retry AuthError or ValidationError -- these indicate a code or configuration problem, not a transient failure. Retrying them wastes time and rate limit budget without any chance of success.

Logging & Debugging

The SDK logs every request, response, and retry through Python's built-in logging module under the sonnys_data_client logger. Enabling debug logging is the fastest way to understand what the SDK is doing under the hood.

Enabling Debug Logging

Log everything (SDK + all other libraries):

import logging

logging.basicConfig(level=logging.DEBUG)

Log only the SDK (recommended for most debugging):

import logging

logging.getLogger("sonnys_data_client").setLevel(logging.DEBUG)

Add a StreamHandler for scripts that don't already configure logging:

import logging

handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))

sdk_logger = logging.getLogger("sonnys_data_client")
sdk_logger.addHandler(handler)
sdk_logger.setLevel(logging.DEBUG)

What Gets Logged

The SDK emits these log messages at each stage of a request:

Level Message Pattern Meaning
DEBUG Rate limiter: waiting {N}s Pre-request rate limit sleep
DEBUG Request: {METHOD} {PATH} params={PARAMS} Outgoing HTTP request
DEBUG Response: {METHOD} {PATH} status={CODE} elapsed={TIME}s Successful response received
DEBUG Error response: {METHOD} {PATH} status={CODE} body={BODY} HTTP error before exception is raised
WARNING Rate limited (429), retry {N}/{MAX} after {DELAY}s 429 received, backing off before retry
DEBUG Job submit: POST /transaction/load-job params={PARAMS} Batch job submitted
DEBUG Job submitted: hash={HASH} Job hash received from API
DEBUG Job poll #{N}: hash={HASH} Polling for job results
DEBUG Job pending: hash={HASH} status={STATUS} Job still processing
DEBUG Job complete: hash={HASH} records={N} polls={N} Job finished with record count
ERROR Job failed: hash={HASH} Batch job returned failure status

Reading Debug Output

Here is an annotated example of a typical debug session showing a successful request, a 429 retry, and an error response:

2025-06-15 10:00:00,100 DEBUG sonnys_data_client: Rate limiter: waiting 0.450s
# ^ Rate limiter detected we're near the 20 req/15s limit. Sleeping 450ms.

2025-06-15 10:00:00,550 DEBUG sonnys_data_client: Request: GET /sites params=None
# ^ Outgoing request to the /sites endpoint with no query parameters.

2025-06-15 10:00:01,200 DEBUG sonnys_data_client: Response: GET /sites status=200 elapsed=0.650s
# ^ Success! The API responded in 650ms with HTTP 200.

2025-06-15 10:00:01,210 DEBUG sonnys_data_client: Request: GET /transaction params={'startDate': 1717200000, 'endDate': 1718409600, 'limit': 100, 'offset': 1}
# ^ Fetching transactions. Dates were auto-converted from ISO strings to Unix timestamps.

2025-06-15 10:00:01,800 WARNING sonnys_data_client: Rate limited (429), retry 1/3 after 1.0s
# ^ API returned 429. SDK will wait 1 second then retry (attempt 1 of 3).

2025-06-15 10:00:02,810 DEBUG sonnys_data_client: Request: GET /transaction params={'startDate': 1717200000, 'endDate': 1718409600, 'limit': 100, 'offset': 1}
# ^ Retry request sent after the 1-second backoff.

2025-06-15 10:00:03,500 DEBUG sonnys_data_client: Response: GET /transaction status=200 elapsed=0.690s
# ^ Retry succeeded. The API responded with HTTP 200.

And here is an example showing batch job lifecycle logging:

2025-06-15 10:01:00,100 DEBUG sonnys_data_client: Job submit: POST /transaction/load-job params={'startDate': 1717200000, 'endDate': 1717286400, 'site': 'JOLIET', 'limit': 100, 'offset': 1}
# ^ Batch job submitted with date range and site parameters.

2025-06-15 10:01:00,800 DEBUG sonnys_data_client: Job submitted: hash=abc123def456
# ^ API accepted the job and returned a hash for polling.

2025-06-15 10:01:02,810 DEBUG sonnys_data_client: Job poll #1: hash=abc123def456
# ^ First poll attempt (after poll_interval delay).

2025-06-15 10:01:03,200 DEBUG sonnys_data_client: Job pending: hash=abc123def456 status=pending
# ^ Job is still processing. Will poll again after another interval.

2025-06-15 10:01:05,210 DEBUG sonnys_data_client: Job poll #2: hash=abc123def456
# ^ Second poll attempt.

2025-06-15 10:01:05,900 DEBUG sonnys_data_client: Job complete: hash=abc123def456 records=347 polls=2
# ^ Job finished! 347 records returned after 2 poll cycles.

And an error response example:

2025-06-15 10:02:00,100 DEBUG sonnys_data_client: Request: GET /transaction params={'startDate': '06/01/2025'}
# ^ Request with an invalid date format (not ISO-8601 or Unix timestamp).

2025-06-15 10:02:00,700 DEBUG sonnys_data_client: Error response: GET /transaction status=400 body={"error":{"type":"InvalidPayloadRequestTimestampError","message":"Invalid timestamp"}}
# ^ Full error response body logged before the exception is raised.

# The exception traceback will show:
# sonnys_data_client._exceptions.ValidationError: [HTTP 400] (InvalidPayloadRequestTimestampError) Invalid timestamp

Tip

In production, leave the SDK logger at WARNING to only see 429 retries. Set to DEBUG only when troubleshooting.

Common Issues & Troubleshooting

Troubleshooting Table

Symptom Likely Cause Solution
AuthError: MissingClientCredentialsError api_id or api_key not provided or empty Check SonnysClient constructor args
AuthError: BadClientCredentialsError Wrong api_id or api_key Verify credentials with Sonny's support
AuthError: NotAuthorizedSiteCredentialsError site_code not authorized for this API ID Use a site_code linked to your API credentials
RateLimitError after retries exhausted Too many concurrent requests Reduce parallelism or increase max_retries
ValidationError: PayloadValidationError Invalid parameter or missing site on load_job() Check parameter names; pass site= to load_job()
NotFoundError: EntityNotFoundError Resource ID doesn't exist or wrong site Verify ID and site_code match
ServerError intermittent 500s Sonny's API transient issue Add retry logic for 5xx (see Custom Retry Patterns)
APIConnectionError Network/DNS failure Check internet, verify trigonapi.sonnyscontrols.com is reachable
APITimeoutError Request took too long Use smaller date ranges, check network latency
Slow responses but no errors Rate limiter pre-emptively sleeping Enable DEBUG logging to see rate limiter waits
ImportError on exceptions Wrong import path Import from sonnys_data_client directly, not _exceptions

Diagnostic Checklist

When something goes wrong, follow these steps in order:

  1. Enable DEBUG logging -- see Enabling Debug Logging above
  2. Check the error_type attribute for the specific API error string (e.g., "BadClientCredentialsError")
  3. Inspect the body attribute for the full API response JSON -- it often contains additional detail
  4. Verify credentials with a simple client.sites.list() call -- if this fails with AuthError, your credentials are wrong
  5. Check rate limiter state with debug logs -- look for Rate limiter: waiting messages to see if pre-request throttling is slowing you down

Warning

The SDK auto-converts ISO 8601 date strings (e.g., "2025-06-01") to Unix timestamps for the API. However, non-ISO formats like "06/01/2025" are not recognized and will cause an InvalidPayloadRequestTimestampError. Always use YYYY-MM-DD format or pass Unix timestamps directly.

Quick Reference

A scannable cheat sheet mapping HTTP status codes to SDK exceptions, typical causes, and recommended actions.

HTTP Status SDK Exception Typical Cause Recommended Action
-- APIConnectionError Network down, DNS failure, firewall blocking Check connectivity; retry with delay
-- APITimeoutError Slow response, large query, job polling timeout Shorten date range; increase timeout
400 ValidationError Bad date format, invalid parameters Fix request parameters; do not retry
403 AuthError Wrong credentials, unauthorized site code Fix credentials or site_code; do not retry
404 NotFoundError ID does not exist, wrong site, deleted record Verify ID and site; skip in batch loops
422 ValidationError Payload validation failure Check parameter values; do not retry
429 RateLimitError Rate limit exceeded after built-in retries Back off 30s+; stagger concurrent scripts
500 ServerError API crash, maintenance, ServerUnexpectedFailure Retry with backoff; contact support if persistent
502/503 ServerError Gateway/infrastructure error Retry with backoff; usually transient