Skip to content

Resources

Resources are the primary interface for interacting with the Sonny's Data API. Each resource is accessed as a property on the SonnysClient and exposes methods like list(), get(), and resource-specific operations.

with SonnysClient(api_id, api_key) as client:
    customers = client.customers.list()
    customer  = client.customers.get("12345")

Base Classes

BaseResource

BaseResource(client: SonnysClient)

Base class for all API resources.

Stores a reference to the parent :class:SonnysClient so that subclasses can issue HTTP requests via self._client._request().

ListableResource

ListableResource(client: SonnysClient)

Bases: BaseResource

Mixin for resources that support a paginated (or non-paginated) list endpoint.

Subclasses must define the following class attributes:

  • _path: URL path for the list endpoint (e.g., "/customer").
  • _items_key: Key inside data that holds the items array (e.g., "customers").
  • _model: Pydantic model class to validate each item against.
  • _default_limit: Page size for paginated requests (default 100).
  • _paginated: Whether the endpoint supports offset/limit pagination (default True). Set to False for endpoints like /site that return all records in a single response.

list

list(**params: object) -> list[SonnysModel]

Fetch all items from the list endpoint.

For paginated endpoints, automatically pages through all results using offset-based pagination (offset starts at 1 per API spec).

For non-paginated endpoints (_paginated=False), makes a single request and returns all items.

Parameters:

Name Type Description Default
**params object

Extra query parameters forwarded to every request (e.g., first_name="John").

{}

Returns:

Type Description
list[SonnysModel]

A list of validated Pydantic model instances.

GettableResource

GettableResource(client: SonnysClient)

Bases: BaseResource

Mixin for resources that support a detail (get-by-ID) endpoint.

Subclasses must define the following class attributes:

  • _detail_path: URL path template with {id} placeholder (e.g., "/customer/{id}").
  • _detail_model: Pydantic model class to validate the detail response against.

get

get(id: str) -> SonnysModel

Fetch a single resource by its ID.

Parameters:

Name Type Description Default
id str

The resource identifier, substituted into _detail_path.

required

Returns:

Type Description
SonnysModel

A validated Pydantic model instance.


Concrete Resources

Customers

Customers(client: SonnysClient)

Bases: ListableResource, GettableResource

Access the /customer list and detail endpoints.

Provides paginated customer search and individual customer lookup.

  • list() returns :class:~sonnys_data_client.types.CustomerListItem summaries. Supports startDate, endDate, site, and region filters.
  • get(id) returns a full :class:~sonnys_data_client.types.Customer profile including address, contact info, and SMS preferences.

Items

Items(client: SonnysClient)

Bases: ListableResource

Access the /item list endpoint.

Provides paginated item catalog listing. List-only resource with no detail endpoint.

  • list() returns :class:~sonnys_data_client.types.Item records with SKU, name, department, and pricing info. Supports site filter.

Employees

Employees(client: SonnysClient)

Bases: ListableResource, GettableResource

Access the /employee list and detail endpoints.

Provides paginated employee search, individual employee lookup, and time-tracking data.

  • list() returns :class:~sonnys_data_client.types.EmployeeListItem summaries. Supports startDate and endDate filters.
  • get(id) returns a full :class:~sonnys_data_client.types.Employee record with contact info and employment dates.
  • get_clock_entries(id) fetches :class:~sonnys_data_client.types.ClockEntry time-tracking records for a specific employee.

get_clock_entries

get_clock_entries(
    employee_id: int | str,
    *,
    start_date: str | None = None,
    end_date: str | None = None,
) -> list[ClockEntry]

Fetch clock entries for an employee.

The API returns a nested data.weeks[] structure where each week contains a clockEntries[] array. This method flattens them into a single list.

Parameters:

Name Type Description Default
employee_id int | str

The employee identifier.

required
start_date str | None

Optional start date filter (passed as startDate).

None
end_date str | None

Optional end date filter (passed as endDate).

None

Returns:

Type Description
list[ClockEntry]

A flat list of validated :class:ClockEntry instances.

Sites

Sites(client: SonnysClient)

Bases: ListableResource

Access the /site/list endpoint.

Provides a complete site listing. Non-paginated resource that returns all sites in a single call. No detail endpoint.

  • list() returns :class:~sonnys_data_client.types.Site records with site ID, code, name, and timezone.

Giftcards

Giftcards(client: SonnysClient)

Bases: ListableResource

Access the /giftcard-liablilty list endpoint.

Provides paginated gift card liability listing. List-only resource with no detail endpoint.

  • list() returns :class:~sonnys_data_client.types.GiftcardListItem records with balance and usage info.
Note

The API path contains a typo (giftcard-liablilty) which is intentional to match the actual Sonny's API endpoint.

Washbooks

Washbooks(client: SonnysClient)

Bases: ListableResource, GettableResource

Access the /washbook/account list and detail endpoints.

Provides paginated washbook account search and individual account lookup with full detail.

  • list() returns :class:~sonnys_data_client.types.WashbookListItem summaries. Supports date and site filters.
  • get(id) returns a full :class:~sonnys_data_client.types.Washbook record with customer, vehicle, and tag details.

RecurringAccounts

RecurringAccounts(client: SonnysClient)

Bases: ListableResource, GettableResource

Access the /recurring/account list, detail, and custom endpoints.

The most feature-rich resource, providing paginated account search, individual account lookup, and specialized reporting endpoints.

  • list() returns :class:~sonnys_data_client.types.RecurringListItem summaries with status and billing site info.
  • get(id) returns a full :class:~sonnys_data_client.types.Recurring record with billing history, tags, vehicles, and customer details.
  • list_status_changes() fetches status transition history.
  • list_modifications() fetches account modification audit logs.
  • list_details() fetches all accounts with full detail in bulk.

list_status_changes

list_status_changes(
    **params: object,
) -> list[RecurringStatusChange]

Fetch all recurring account status changes.

Returns:

Type Description
list[RecurringStatusChange]

A flat list of :class:RecurringStatusChange instances.

list_modifications

list_modifications(
    **params: object,
) -> list[RecurringModification]

Fetch all recurring account modifications.

Returns:

Type Description
list[RecurringModification]

A flat list of :class:RecurringModification instances.

list_details

list_details(**params: object) -> list[Recurring]

Fetch all recurring accounts with full detail.

Unlike :meth:list which returns summary :class:RecurringListItem objects, this method returns full :class:Recurring detail objects.

Returns:

Type Description
list[Recurring]

A flat list of :class:Recurring instances.

Transactions

Transactions(client: SonnysClient)

Bases: ListableResource, GettableResource

Access the /transaction list, detail, and by-type endpoints.

The most complex resource, providing paginated transaction search, individual transaction lookup, type-filtered listing, and batch job support for large exports.

  • list() returns :class:~sonnys_data_client.types.TransactionListItem summaries. Supports startDate, endDate, site, region filters.
  • get(id) returns a full :class:~sonnys_data_client.types.Transaction record with line items, tenders, and discounts.
  • list_by_type(type) filters transactions by type (wash, recurring, etc.).
  • list_v2() uses the enriched v2 endpoint with customer and status fields.
  • load_job() submits asynchronous batch jobs for large date ranges.

list

list(**params: object) -> list[TransactionListItem]

Fetch all transactions, converting date strings to timestamps.

Accepts startDate / endDate as ISO-8601 strings (e.g. "2026-01-15") or Unix timestamps.

Returns:

Type Description
list[TransactionListItem]

A flat list of :class:TransactionListItem instances.

list_by_type

list_by_type(
    item_type: str, **params: object
) -> list[TransactionListItem]

Fetch all transactions of a specific type.

Valid types include: wash, prepaid-wash, recurring, washbook, giftcard, merchandise, house-account. The API validates the type parameter.

Parameters:

Name Type Description Default
item_type str

The transaction type to filter by.

required
**params object

Extra query parameters forwarded to every request (e.g., startDate, endDate, site, region).

{}

Returns:

Type Description
list[TransactionListItem]

A flat list of :class:TransactionListItem instances.

list_v2

list_v2(**params: object) -> list[TransactionV2ListItem]

Fetch all transactions using the v2 endpoint.

The v2 endpoint returns enriched list items with customer_id, is_recurring_plan_sale, is_recurring_plan_redemption, and transaction_status fields.

Note: The API caches v2 responses for 10 minutes per reporting criteria.

Returns:

Type Description
list[TransactionV2ListItem]

A flat list of :class:TransactionV2ListItem instances.

load_job

load_job(
    *,
    poll_interval: float = 2.0,
    timeout: float = 300.0,
    **params: object,
) -> list[TransactionJobItem]

Submit batch jobs and auto-paginate through all results.

Pagination happens at the job submission level: each call to /transaction/load-job with a different offset fetches one page. The method submits as many jobs as needed to retrieve all records.

Note: The API caches job data for 20 minutes and limits the date range to a maximum of 24 hours.

Parameters:

Name Type Description Default
poll_interval float

Seconds between poll attempts (default 2.0).

2.0
timeout float

Max seconds to wait for each job (default 300.0).

300.0
**params object

Query parameters (startDate, endDate, site, limit, offset, etc.).

{}

Returns:

Type Description
list[TransactionJobItem]

A list of :class:TransactionJobItem instances.

Raises:

Type Description
APIError

If any job status is "fail".

APITimeoutError

If any job does not complete within timeout.

StatsResource

StatsResource(client: SonnysClient)

Bases: BaseResource

Access computed business analytics and KPIs.

Unlike other resources that wrap REST endpoints directly, StatsResource computes analytics by fetching raw data and aggregating it locally. Individual stat methods (total sales, total washes, conversion rate, etc.) will be added in Phases 21-25.

All stat methods accept a date range and delegate to :meth:_resolve_dates for consistent parsing and validation.

retail_wash_count

retail_wash_count(
    start: str | datetime, end: str | datetime
) -> int

Count retail wash transactions for a date range.

A retail wash is a type=wash transaction (v1) that is neither a recurring plan sale nor a recurring redemption (v2 flags).

Parameters:

Name Type Description Default
start str | datetime

Range start as an ISO-8601 string (e.g. "2026-01-01") or :class:~datetime.datetime.

required
end str | datetime

Range end as an ISO-8601 string or :class:~datetime.datetime.

required

Returns:

Type Description
int

The number of retail wash transactions in the date range.

Raises:

Type Description
ValueError

If start is after end, or if a string cannot be parsed as a valid ISO-8601 date/datetime.

Example::

count = client.stats.retail_wash_count("2026-01-01", "2026-01-31")
print(f"Retail washes: {count}")

new_memberships_sold

new_memberships_sold(
    start: str | datetime, end: str | datetime
) -> int

Count new membership sales for a date range.

Fetches enriched v2 transactions and identifies those flagged as is_recurring_plan_sale, then verifies each via the v1 detail endpoint to exclude plan upgrades/switches. Only transactions where the v1 is_recurring_sale flag is True are counted.

Parameters:

Name Type Description Default
start str | datetime

Range start as an ISO-8601 string (e.g. "2026-01-01") or :class:~datetime.datetime.

required
end str | datetime

Range end as an ISO-8601 string or :class:~datetime.datetime.

required

Returns:

Type Description
int

The number of genuine new membership sales in the date range.

Raises:

Type Description
ValueError

If start is after end, or if a string cannot be parsed as a valid ISO-8601 date/datetime.

Example::

count = client.stats.new_memberships_sold("2026-01-01", "2026-01-31")
print(f"New memberships sold: {count}")

total_sales

total_sales(
    start: str | datetime, end: str | datetime
) -> SalesResult

Compute revenue breakdown for a date range.

Fetches all transactions via the enriched v2 endpoint and categorizes them into three buckets: recurring plan sales, recurring redemptions, and retail. Returns a :class:~sonnys_data_client.types.SalesResult with per-bucket breakdowns.

The total and count fields exclude membership redemptions (is_recurring_plan_redemption=True), since redemptions are $0-value usage events that do not generate revenue. Redemption data is still available in the recurring_redemptions and recurring_redemptions_count breakdown fields.

.. note::

The total may be slightly higher than the Sonny's Back Office Transaction Total due to customer overpayments. The API includes overpaid amounts in transaction totals, while Back Office tracks them as a separate line item. The typical difference is <1%. Correcting for this would require fetching full transaction details for every transaction to compute the overpaid delta, which is not practical given the API rate limit.

Parameters:

Name Type Description Default
start str | datetime

Range start as an ISO-8601 string (e.g. "2026-01-01") or :class:~datetime.datetime.

required
end str | datetime

Range end as an ISO-8601 string or :class:~datetime.datetime.

required

Returns:

Name Type Description
A SalesResult

class:~sonnys_data_client.types.SalesResult containing

SalesResult

the grand total (excluding redemptions), transaction count,

SalesResult

and per-category breakdowns.

Raises:

Type Description
ValueError

If start is after end, or if a string cannot be parsed as a valid ISO-8601 date/datetime.

Example::

result = client.stats.total_sales("2026-01-01", "2026-01-31")
print(f"Total: ${result.total:.2f}")
print(f"Memberships: ${result.recurring_plan_sales:.2f}")

total_washes

total_washes(
    start: str | datetime, end: str | datetime
) -> WashResult

Compute wash volume breakdown for a date range.

Fetches v2 transactions (for membership flags), v1 type=wash (to identify car washes), and v1 type=recurring (to identify recharges). Classification priority: redemption > plan sale > wash > recharge > unknown.

eligible_wash_count is derived as total - member_wash_count - free_wash_count.

Parameters:

Name Type Description Default
start str | datetime

Range start as an ISO-8601 string (e.g. "2026-01-01") or :class:~datetime.datetime.

required
end str | datetime

Range end as an ISO-8601 string or :class:~datetime.datetime.

required

Returns:

Name Type Description
A WashResult

class:~sonnys_data_client.types.WashResult containing the

WashResult

total wash count and per-category breakdowns.

Raises:

Type Description
ValueError

If start is after end, or if a string cannot be parsed as a valid ISO-8601 date/datetime.

Example::

result = client.stats.total_washes("2026-01-01", "2026-01-31")
print(f"Total washes: {result.total}")
print(f"Retail: {result.retail_wash_count}")
print(f"Member: {result.member_wash_count}")
print(f"Eligible: {result.eligible_wash_count}")
print(f"Free: {result.free_wash_count}")

conversion_rate

conversion_rate(
    start: str | datetime, end: str | datetime
) -> ConversionResult

Compute the membership conversion rate for a date range.

Measures how effectively a site converts eligible wash customers into membership sign-ups. The rate is computed as new_memberships / eligible_washes.

Eligible washes are derived from the total wash count: total_washes - member_washes - free_washes. This includes retail washes with total > 0, plan sale washes, and unknown non-negative transaction types. When there are zero eligible washes the rate is 0.0 (division-by-zero safe).

Parameters:

Name Type Description Default
start str | datetime

Range start as an ISO-8601 string (e.g. "2026-01-01") or :class:~datetime.datetime.

required
end str | datetime

Range end as an ISO-8601 string or :class:~datetime.datetime.

required

Returns:

Name Type Description
A ConversionResult

class:~sonnys_data_client.types.ConversionResult containing

ConversionResult

the conversion rate and component counts.

Raises:

Type Description
ValueError

If start is after end, or if a string cannot be parsed as a valid ISO-8601 date/datetime.

Example::

result = client.stats.conversion_rate("2026-01-01", "2026-01-31")
print(f"Conversion rate: {result.rate:.1%}")
print(f"Memberships: {result.new_memberships}")
print(f"Eligible washes: {result.eligible_washes}")

total_labor_cost

total_labor_cost(
    start: str | datetime, end: str | datetime
) -> LaborCostResult

Compute labor cost breakdown for a date range.

Fetches all clock entries via :meth:_fetch_all_clock_entries and aggregates regular and overtime costs in a single pass. Each entry's cost is computed as rate * hours for the corresponding pay type.

Parameters:

Name Type Description Default
start str | datetime

Range start as an ISO-8601 string (e.g. "2026-01-01") or :class:~datetime.datetime.

required
end str | datetime

Range end as an ISO-8601 string or :class:~datetime.datetime.

required

Returns:

Name Type Description
A LaborCostResult

class:~sonnys_data_client.types.LaborCostResult containing

LaborCostResult

the total cost, regular/overtime breakdowns, hours, and entry

LaborCostResult

count.

Raises:

Type Description
ValueError

If start is after end, or if a string cannot be parsed as a valid ISO-8601 date/datetime.

Example::

result = client.stats.total_labor_cost("2026-01-01", "2026-01-31")
print(f"Total: ${result.total_cost:.2f}")
print(f"Regular: ${result.regular_cost:.2f} ({result.regular_hours:.1f}h)")
print(f"Overtime: ${result.overtime_cost:.2f} ({result.overtime_hours:.1f}h)")

cost_per_car

cost_per_car(
    start: str | datetime, end: str | datetime
) -> CostPerCarResult

Compute labor cost per car for a date range.

Divides total labor cost by total wash volume to measure labor efficiency. A value of 4.25 means the site spent $4.25 in labor for each car washed. When there are zero washes the result is 0.0 (division-by-zero safe).

Parameters:

Name Type Description Default
start str | datetime

Range start as an ISO-8601 string (e.g. "2026-01-01") or :class:~datetime.datetime.

required
end str | datetime

Range end as an ISO-8601 string or :class:~datetime.datetime.

required

Returns:

Name Type Description
A CostPerCarResult

class:~sonnys_data_client.types.CostPerCarResult containing

CostPerCarResult

the cost per car, total labor cost, and total wash count.

Raises:

Type Description
ValueError

If start is after end, or if a string cannot be parsed as a valid ISO-8601 date/datetime.

Example::

result = client.stats.cost_per_car("2026-01-01", "2026-01-31")
print(f"Cost per car: ${result.cost_per_car:.2f}")
print(f"Labor cost: ${result.total_labor_cost:.2f}")
print(f"Total washes: {result.total_washes}")

report

report(
    start: str | datetime, end: str | datetime
) -> StatsReport

Compute all KPIs for a date range in a single call.

Fetches v2 transactions, v1 type=wash, v1 type=recurring, clock entries for all employees, and verifies each plan sale candidate via get() to exclude plan upgrades/switches. Makes 4 bulk API calls (3 transaction + 1 employee list) plus N_employees x ceil(days/14) clock-entry calls and ~N detail calls (one per v2 plan sale candidate, typically ~15/day), then computes every KPI locally.

The sales.total field excludes membership redemptions to align with the Sonny's Back Office Transaction Total. See :meth:total_sales for details.

Parameters:

Name Type Description Default
start str | datetime

Range start as an ISO-8601 string (e.g. "2026-01-01") or :class:~datetime.datetime.

required
end str | datetime

Range end as an ISO-8601 string or :class:~datetime.datetime.

required

Returns:

Name Type Description
A StatsReport

class:~sonnys_data_client.types.StatsReport containing

StatsReport

sales, washes, new_memberships, conversion,

StatsReport

labor, cost_per_car, period_start, and

StatsReport

period_end.

Raises:

Type Description
ValueError

If start is after end, or if a string cannot be parsed as a valid ISO-8601 date/datetime.

Example::

rpt = client.stats.report("2026-01-01", "2026-01-31")
print(f"Revenue: ${rpt.sales.total:.2f}")
print(f"Washes: {rpt.washes.total}")
print(f"New members: {rpt.new_memberships}")
print(f"Conversion: {rpt.conversion.rate:.1%}")
print(f"Labor cost: ${rpt.labor.total_cost:.2f}")
print(f"Cost per car: ${rpt.cost_per_car.cost_per_car:.2f}")

BackOfficeResource

BackOfficeResource(client: SonnysClient)

Bases: BaseResource

Scraper for the BackOffice employee timesheets report.

Requires backoffice_username and backoffice_password at :class:SonnysClient construction. The api_id doubles as the BackOffice subdomain (e.g. "washu"https://washu.sonnyscontrols.com).

timeclock

timeclock(
    start: str | date | datetime,
    end: str | date | datetime,
    *,
    site_id: int | None = None,
) -> BackOfficeTimeclockResult

Scrape the employee-timesheets report for a date range.

Returns per-shift punch-in/out detail grouped by employee, plus a period grand total parsed from the report's footer row.

The client-level site_code is not applied here. The BackOffice report is returned for all sites and every shift carries its own site_code field. Pass an explicit numeric site_id (the BackOffice URL parameter) to narrow at fetch time, or filter result.employees[*].shifts locally.

Parameters:

Name Type Description Default
start str | date | datetime

Start date (inclusive) as YYYY-MM-DD string, :class:~datetime.date, or :class:~datetime.datetime.

required
end str | date | datetime

End date (inclusive), same types.

required
site_id int | None

Optional numeric BackOffice site id for URL-level filtering. When None the report includes all sites.

None

Returns:

Name Type Description
A BackOfficeTimeclockResult

class:BackOfficeTimeclockResult with the full parsed

BackOfficeTimeclockResult

report.

Raises:

Type Description
BackOfficeCredentialsError

backoffice_username or backoffice_password was not supplied.

BackOfficeLoginError

BackOffice authentication failed.

BackOfficeScrapeError

The page HTML did not match the expected structure.