Skip to content

Recurring Accounts

API

The Recurring Accounts resource provides access to recurring (membership) car wash accounts. This is the richest standard resource in the client, offering five methods for listing accounts, fetching full details, tracking status changes, and reviewing modification history. Each account includes billing information, plan details, associated tags, vehicles, and customer data.

Backed by the Data API — uses your api_id / api_key credentials.

Choosing the Right Method

The recurring resource offers five methods. Pick the one that matches your use case to avoid fetching more data than you need:

Method Returns Use When Pagination
list() RecurringListItem Quick summaries, filtering by date/site Auto
get() Recurring Full detail for a single account N/A
list_status_changes() RecurringStatusChange Tracking cancellations, reactivations Auto
list_modifications() RecurringModification Audit trail, plan changes, account edits Auto
list_details() Recurring Bulk export with full detail (heavier) Auto

Start with list() for lightweight summaries and filtering by date range or site. Use get() when you need a single-account deep dive with nested billing history, tags, vehicles, and customer data. Reach for list_status_changes() when analyzing churn and retention trends, and list_modifications() when you need an audit trail of plan changes or account edits. Reserve list_details() for cases where you need the full nested data (tags, vehicles, billing history) in bulk -- it returns the same rich Recurring objects as get() but for every account, so it is significantly heavier.

Methods

list(**params) -> list[RecurringListItem] API

Fetch all recurring accounts. Returns a list of RecurringListItem objects with summary fields. The client automatically paginates through all pages of results.

accounts = client.recurring.list()

You can pass optional query parameters to filter results:

accounts = client.recurring.list(
    startDate="2025-01-01",
    endDate="2025-01-31",
)

get(account_id) -> Recurring API

Fetch full details for a single recurring account by its ID. Returns a Recurring object with nested customer, tags, vehicles, billing history, and status history.

account = client.recurring.get("12345")

list_status_changes(**params) -> list[RecurringStatusChange] API

Fetch all recurring account status changes. Returns a list of RecurringStatusChange objects tracking when accounts changed status (e.g., active to cancelled).

changes = client.recurring.list_status_changes()

list_modifications(**params) -> list[RecurringModification] API

Fetch all recurring account modifications. Returns a list of RecurringModification objects -- each includes the full account detail plus a list of modification entries.

modifications = client.recurring.list_modifications()

list_details(**params) -> list[Recurring] API

Fetch all recurring accounts with full detail. Returns full Recurring objects (the same model as get()) for every account, rather than the summary RecurringListItem objects returned by list().

details = client.recurring.list_details()

Heavier API call

list_details() returns full Recurring objects for every account. This results in significantly more data per request than list(). Use list() when you only need summary fields, and reserve list_details() for when you need the complete account data in bulk.

Examples

List recurring accounts

from sonnys_data_client import SonnysClient

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    accounts = client.recurring.list()

    for acct in accounts:
        print(
            f"Account {acct.id}: {acct.status_name} "
            f"(status={acct.status}, site={acct.billing_site_code})"
        )

Get account detail

The detail endpoint returns billing info, plan details, tags, vehicles, customer data, and full status and billing history:

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    acct = client.recurring.get("12345")

    # Plan and billing info
    print(f"Plan: {acct.plan_name}")
    print(f"Billing amount: ${acct.billing_amount:.2f}")
    print(f"Next bill date: {acct.next_bill_date}")
    print(f"On trial: {acct.is_on_trial}")
    print(f"Suspended: {acct.is_suspended}")

    # Customer
    print(f"Customer: {acct.customer.first_name} {acct.customer.last_name}")

    # Tags and vehicles
    for tag in acct.tags:
        print(f"Tag #{tag.number} - {'active' if tag.enabled else 'disabled'}")
    for vehicle in acct.vehicles:
        print(f"Vehicle {vehicle.id}: {vehicle.plate or 'No plate'}")

    # Status history
    for status in acct.recurring_statuses:
        print(f"Status: {status.status} on {status.date}")

    # Billing history
    for billing in acct.recurring_billings:
        print(f"Billed ${billing.amount_charged:.2f} on {billing.date}")

Track status changes

The list_status_changes() method returns a flat list of every status transition across all accounts:

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    changes = client.recurring.list_status_changes(
        startDate="2025-06-01",
        endDate="2025-06-30",
    )

    for change in changes:
        print(
            f"Account {change.recurring_id}: "
            f"{change.old_status} -> {change.new_status} "
            f"on {change.status_date} "
            f"(by {change.employee_name} at {change.site_code})"
        )

Review modifications

The list_modifications() method returns full account objects extended with a modifications list:

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    modified = client.recurring.list_modifications(
        startDate="2025-06-01",
        endDate="2025-06-30",
    )

    for acct in modified:
        print(f"Account {acct.id} ({acct.plan_name}):")
        for mod in acct.modifications:
            comment = mod.comment or "No comment"
            print(f"  {mod.date}: {mod.name} - {comment}")

Compare list vs list_details

Use list() for lightweight summaries and list_details() when you need the full account data:

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    # Lightweight: returns RecurringListItem objects
    summaries = client.recurring.list()
    print(f"Found {len(summaries)} accounts (summary)")

    # Full detail: returns Recurring objects (same as get())
    details = client.recurring.list_details()
    print(f"Found {len(details)} accounts (full detail)")

    # Full detail includes nested data not available in summaries
    for acct in details:
        print(
            f"Account {acct.id}: {acct.plan_name}, "
            f"{len(acct.tags)} tags, {len(acct.vehicles)} vehicles"
        )

Models

RecurringListItem

Returned by list(). Contains summary fields for each recurring account.

Field Type Description
id str Unique account identifier
name str \| None Account name
balance float \| None Current account balance
sign_up_date str Date the account was created
cancel_date str \| None Date the account was cancelled
billing_site_id int ID of the billing site
customer_id str \| None Associated customer identifier
status int Numeric status code
status_name str Human-readable status name
billing_site_code str Site code of the billing site

Recurring

Returned by get() and list_details(). Contains full account details with nested models.

Field Type Description
id str Unique account identifier
is_on_trial bool Whether the account is on trial
trial_amount float Trial billing amount
billing_site_code str Site code of the billing site
creation_site_code str Site code where account was created
next_bill_date str Next scheduled billing date
last_bill_date str \| None Most recent billing date
billing_amount float \| None Current billing amount
is_suspended bool Whether the account is suspended
suspended_until str \| None Suspension end date
current_recurring_status_name str Current status name
plan_name str Name of the wash plan
additional_tag_price float \| None Price for additional tags
customer WashbookCustomer Nested customer object
tags list[WashbookTag] List of RFID tags
vehicles list[WashbookVehicle] List of vehicles
recurring_statuses list[RecurringStatus] Status history (see below)
recurring_billings list[RecurringBilling] Billing history (see below)

RecurringStatus

Nested inside the Recurring model. Represents a single status history entry.

Field Type Description
status str Status name
date str Date of the status

RecurringBilling

Nested inside the Recurring model. Represents a single billing history entry.

Field Type Description
amount_charged float Amount billed
date str Billing date
last_four_cc str Last four digits of credit card
credit_card_expiration_date str \| None Card expiration date

RecurringStatusChange

Returned by list_status_changes(). Represents a single status transition.

Field Type Description
washbook_account_id str Associated washbook account ID
recurring_id str Recurring account identifier
old_status str Previous status
new_status str New status
status_date str Date of the status change
employee_name str Employee who made the change
site_code str Site where the change occurred

RecurringModification

Returned by list_modifications(). Extends the Recurring model with a modifications list.

This model inherits all fields from Recurring (see above) plus:

Field Type Description
modifications list[RecurringModificationEntry] List of modifications

RecurringModificationEntry

Nested inside RecurringModification.

Field Type Description
name str Modification name
date str Date of the modification
comment str \| None Optional comment

Advanced Patterns

Churn Analysis

Use list_status_changes() to analyze account churn over a date range. Each status change includes the old and new status, the date, and the employee and site responsible:

from collections import Counter
from sonnys_data_client import SonnysClient

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    changes = client.recurring.list_status_changes(
        startDate="2025-06-01",
        endDate="2025-06-30",
    )

    # Count transitions by type
    transitions = Counter(
        f"{c.old_status}{c.new_status}" for c in changes
    )
    for transition, count in transitions.most_common():
        print(f"{transition}: {count}")

    # Find cancellations
    cancellations = [c for c in changes if c.new_status.lower() == "cancelled"]
    print(f"\n{len(cancellations)} cancellations this month")
    for c in cancellations:
        print(f"  Account {c.recurring_id} at {c.site_code} on {c.status_date}")

Billing History Report

Use list_details() to build a billing summary across all accounts. Each Recurring object includes a recurring_billings list with individual charge records:

with SonnysClient(api_id="your-api-id", api_key="your-api-key") as client:
    accounts = client.recurring.list_details(
        startDate="2025-06-01",
        endDate="2025-06-30",
    )

    total_revenue = 0.0
    for acct in accounts:
        acct_total = sum(b.amount_charged for b in acct.recurring_billings)
        total_revenue += acct_total
        if acct_total > 0:
            print(
                f"Account {acct.id} ({acct.plan_name}): "
                f"${acct_total:.2f} over {len(acct.recurring_billings)} billings"
            )

    print(f"\nTotal recurring revenue: ${total_revenue:.2f}")

Use list_details() only when needed

list_details() returns full Recurring objects for every account, including nested tags, vehicles, and billing history. This is significantly heavier than list(). Use it only when you need billing or status history data in bulk.

Shared nested types

The Recurring model reuses WashbookCustomer, WashbookTag, and WashbookVehicle from the Washbooks resource. See that guide for field-level documentation of those nested types.

Auto-pagination

All list methods (list(), list_status_changes(), list_modifications(), and list_details()) automatically fetch all pages of results. You do not need to handle pagination manually.

See also

For patterns combining recurring accounts with transaction data, see the Transactions guide.