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.
Recommended architecture: polling¶
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 aclient_idsor date filter. - Use
expand[]=class_idto include class details in booking results, or retrieve the class byclass_id. - The API returns no aggregated revenue figures. Compute revenue from invoices and invoice items in your own store.