Skip to content

Reporting and BI

Use this guide when your goal is to pull bsport operational data into a reporting dashboard, data warehouse, or analytics tool.

Who this is for

  • Teams building custom management dashboards with KPIs on bookings, attendance, memberships, or revenue.
  • Data analysts loading bsport data into an external warehouse or BI tool (Metabase, Tableau, Looker, and similar).
  • Operators who need a consolidated view across multiple companies in a franchise network.

Alternative: Data as a Service (DaaS)

For enterprise reporting and BI use cases, bsport offers Data as a Service: a dedicated Databricks data warehouse with curated, business-friendly tables (members, classes, bookings, subscriptions, etc.) refreshed daily.

Benefits over the Public API:

  • Curated tables aligned with Back Office naming (not technical API objects)
  • Managed pipeline: no need to build/maintain your own ETL
  • Warehouse-style queries: better for large reporting workloads than repeated API polling
  • Tool freedom: connect Power BI, Tableau, Airtable, or any BI platform directly
  • Faster time-to-value: start analyzing immediately without data modeling

Trade-offs:

  • Batch refresh (~1pm CET daily, with data up to 5am CET) instead of real-time
  • Requires BI/data engineering capability to operate independently
  • Not available for Excel or Google Sheets direct connection

Is DaaS right for you?

Consider DaaS if you have a dedicated BI/data team, need custom analytics beyond standard dashboards, and prefer self-serve exploration over API engineering.

To learn more or request onboarding, contact your account manager.

What you can do

Data type Endpoint
Client list GET /api/v2/management/clients/
Bookings GET /api/v2/management/bookings/
Classes and appointments GET /api/v2/management/classes/, GET /api/v2/management/appointments/
Client passes GET /api/v2/management/client-class-passes/
Client subscriptions GET /api/v2/management/client-subscriptions/
Invoices GET /api/v2/management/invoices/
Invoice items GET /api/v2/management/invoice-items/
Discount code usages GET /api/v2/management/coupon-usages/
Teacher pay rates GET /api/v2/management/teacher-pay-rates/

Filter invoices by client and by issue or creation timestamp, then paginate. Invoice filters take timestamps, not dates: see Date and timestamp filters. Invoice totals are decimal strings in major currency units; nested invoice line amounts are integers in minor currency units. See V2 workflows.

For reporting, a scheduled polling job is the approach with the fewest moving parts. You control the refresh frequency and keep data in your own store.

Scheduled job (daily or hourly)
    -> Public API calls with date filters
    -> Store results in your data warehouse or database
    -> BI tool reads from your data store

Avoid querying the Public API directly from your BI tool at report render time. Cache the data in your own store to avoid rate limit issues and ensure consistent report performance.

import requests
import time
from datetime import date, timedelta

# YOUR_ENV_DOMAIN: public.staging.bsport.io or public.production.bsport.io
BASE_URL = "https://YOUR_ENV_DOMAIN"
HEADERS = {
    "X-Api-Key": "YOUR_API_KEY",
    "X-Client-ID": "YOUR_CLIENT_ID",
    "X-Company-ID": "YOUR_COMPANY_ID",
}

def fetch_bookings(date_from: date, date_to: date) -> list:
    bookings = []
    page = 1
    while True:
        response = requests.get(
            f"{BASE_URL}/api/v2/management/bookings/",
            headers=HEADERS,
            params={
                "page_size": 100,
                "page": page,
                "class_start_after": date_from.isoformat(),
                "class_start_before": f"{date_to.isoformat()}T23:59:59.999999",
            },
        )
        response.raise_for_status()
        data = response.json()
        bookings.extend(data["results"])
        page = data["next_page"]
        if page is None:
            break
        time.sleep(0.3)  # stay within rate limit
    return bookings

# Run daily from a scheduler (cron, Airflow, Celery beat, etc.)
yesterday = date.today() - timedelta(days=1)
for booking in fetch_bookings(date_from=yesterday, date_to=date.today()):
    upsert_into_warehouse(booking)

See Rate limits and bulk data sync for initial load patterns and the specific warning about the client-class-passes endpoint at scale.

Webhook alternative: near-real-time ingestion

Create V2 registrations to push events directly into your warehouse as they happen. This removes the polling lag and reduces API quota usage for high-frequency data like bookings.

Public API event (booking.created, booking.updated, booking.deleted)
    -> your webhook endpoint
    -> upsert into warehouse
    -> BI tool reads from warehouse
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhook/bsport", methods=["POST"])
def handle_bsport_event():
    raw_body = request.get_data()
    verify_bsport_signature(raw_body, request.headers["Bsport-Signature"])

    payload = request.get_json()
    event_type = payload["event_type"]

    if event_type == "booking.deleted":
        delete_booking_from_warehouse(payload["data"]["id"])

    elif event_type in ("booking.created", "booking.updated"):
        upsert_into_warehouse({"type": "booking", **payload["data"]})

    return jsonify({"status": "accepted"}), 200

verify_bsport_signature is defined in Delivery, signatures and retries.

data is the complete V1 Booking resource for create and update events. It uses V1 field names such as session_id and client_pass_id, while the polling job above returns V2 names such as class_id and client_class_pass_id. Map both shapes to the same warehouse columns. A delete event contains only the deleted booking identifier. See the event catalogue for both schemas.

V2 webhook Booking payloads use the same V2 REST shape as the polling examples above. Existing V1 webhook registrations keep their historical contract; use the V1 catalogue when maintaining those integrations.

Combine both approaches

Run a daily polling job to backfill any missed events, and use webhooks for real-time ingestion. The polling job acts as a safety net when your webhook endpoint is unavailable.

Franchise-level reporting

To aggregate data across all companies in a franchise, use a Franchisor API key and loop over each company ID.

import requests
import time
from datetime import date, timedelta

BASE_URL = "https://YOUR_ENV_DOMAIN"
FRANCHISE_ID = "60"
COMPANY_IDS = [1511, 918, 2334]  # companies within the franchise

BASE_HEADERS = {
    "X-Api-Key": "YOUR_FRANCHISOR_API_KEY",
    "X-Client-ID": "YOUR_CLIENT_ID",
    "X-Franchisor-ID": FRANCHISE_ID,
}

def fetch_bookings_for_company(company_id: int, date_from: date, date_to: date) -> list:
    headers = {**BASE_HEADERS, "X-Company-ID": str(company_id)}
    bookings = []
    page = 1
    while True:
        response = requests.get(
            f"{BASE_URL}/api/v2/management/bookings/",
            headers=headers,
            params={
                "page_size": 100,
                "page": page,
                "class_start_after": date_from.isoformat(),
                "class_start_before": f"{date_to.isoformat()}T23:59:59.999999",
            },
        )
        response.raise_for_status()
        data = response.json()
        bookings.extend(data["results"])
        page = data["next_page"]
        if page is None:
            break
        time.sleep(0.3)
    return bookings

yesterday = date.today() - timedelta(days=1)
for company_id in COMPANY_IDS:
    for booking in fetch_bookings_for_company(company_id, yesterday, date.today()):
        upsert_into_warehouse({**booking, "company_id": company_id})

See Headers and tenant context for the full header rules.

Limitations

  • The GET /api/v2/management/client-class-passes/ endpoint can time out on companies with a large number of passes if queried without filters. Always apply a client_ids or date filter.
  • Use expand[]=class_id to include class details in booking results, or retrieve the class by class_id.
  • The API returns no aggregated revenue figures. Compute revenue from invoices and invoice items in your own store.