CRM and email automation¶
Keep client profiles and segmentation in an external CRM without repeatedly polling the complete client list.
What you can synchronize¶
| Change | Public API event | Payload resource |
|---|---|---|
| Member created | client.created |
Client |
| Member profile updated | client.updated |
Client |
| Tag added to a member | client.tag_added |
{client_id, tag_id} relation |
| Tag removed from a member | client.tag_removed |
{client_id, tag_id} relation |
Subscription events are not part of the current Public API webhook catalogue. For invoice synchronization, use V2 invoice webhooks, which report invoice and line-item mutations. Do not build a new integration against the legacy Back Office billing or invoice payloads.
Architecture¶
Public API webhook
→ verify signature and deduplicate delivery
→ read the Client object from data
→ upsert the CRM contact
Public API webhook data already matches the unexpanded V1 Client retrieve response. The
CRM fields listed below exist in both the V1 and V2 Client, so the same upsert code reads a
webhook payload and a V2 response. An enrichment request is only required when the
integration needs a related resource that is not in that model.
Initial import¶
Import existing clients before activating webhook registrations:
import time
import requests
# 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_all_clients() -> list[dict]:
clients = []
page = 1
while True:
response = requests.get(
f"{BASE_URL}/api/v2/management/clients/",
headers=HEADERS,
params={"page_size": 100, "page": page},
)
response.raise_for_status()
data = response.json()
clients.extend(data["results"])
page = data["next_page"]
if page is None:
return clients
time.sleep(0.3)
Use the Client id as the stable external identifier. Useful CRM fields include email,
firstname, lastname, phone_number, tag_ids, is_archived, is_email_accepted and
is_sms_accepted.
Process ongoing changes¶
Create V2 registrations for the four client events and route them to the same receiver:
from flask import Flask, jsonify, request
app = Flask(__name__)
MEMBER_RESOURCE_EVENTS = {
"client.created",
"client.updated",
}
TAG_RELATION_EVENTS = {
"client.tag_added",
"client.tag_removed",
}
@app.post("/webhook/bsport")
def handle_bsport_event():
raw_body = request.get_data()
verify_bsport_signature(raw_body, request.headers["Bsport-Signature"])
delivery_id = request.headers["X-Bsport-Webhook-Delivery-Id"]
if delivery_was_processed(delivery_id):
return jsonify({"status": "already_processed"}), 200
payload = request.get_json()
event_type = payload["event_type"]
if event_type in MEMBER_RESOURCE_EVENTS:
client = payload["data"]
upsert_crm_contact(external_id=client["id"], client=client)
elif event_type in TAG_RELATION_EVENTS:
relation = payload["data"]
update_crm_tag(
client_id=relation["client_id"],
tag_id=relation["tag_id"],
applied=event_type == "client.tag_added",
)
mark_delivery_processed(delivery_id)
return jsonify({"status": "accepted"}), 200
Signature verification must use the raw request bytes before JSON parsing. See Delivery, signatures and retries for the complete algorithm.
Keep tag mappings readable¶
Client resource payloads contain the current tag_ids list. Relation events instead contain one
numeric client_id and one numeric tag_id. Maintain a mapping between tag identifiers and the
labels used by your CRM; apply or remove exactly the relation named by the event.
Send messages from your CRM¶
V2 can queue a plain-text email or SMS to up to 100 clients per request. The client list is not an exclusive recipient list, and a retry can send duplicates. Read Send an email or SMS before you use it.
Fallback polling¶
The clients endpoint does not currently expose an incremental updated_at filter. A polling
fallback must therefore paginate the complete list and compare it with local state. Prefer
webhooks for ongoing synchronization and keep polling as a lower-frequency reconciliation job.