Appearance
Integrations
The Integrations settings page connects third-party marketing and booking tools to your brand.
Getting there
- Where: Settings → Integrations.
- Permission: the
brandsresource.
Available integrations
Each integration shows a status badge: Not connected, Connected, Error, or Disabled.
- Klaviyo: sync opted-in customers to Klaviyo profiles.
- Mailchimp: sync opted-in customers into a Mailchimp audience.
- ClassPass: publish availability and accept partner bookings through the server-to-server partner API.
Syncing customers
Klaviyo and Mailchimp cards include Sync customers after the integration is connected and enabled. The sync sends active customers whose email marketing opt-in is enabled. Archived customers and opted-out customers are skipped.
After a sync completes, the card shows the last successful sync time. If the provider rejects the request, the integration status changes to Error and the card shows the provider error so you can fix credentials or settings before retrying.
ClassPass partner API
Enable the ClassPass integration and set the Partner API key before sharing the API with ClassPass. Requests use Authorization: Bearer <partnerApiKey>.
The partner API is server-to-server and mirrors the inventory flow ClassPass expects from booking-system integrations: publish sellable schedule inventory, accept reservation transactions, and accept cancellation transactions.
Authentication and setup
- The integration must be Enabled.
- Partner API key is the bearer token ClassPass sends.
- Partner ID is returned with availability responses.
- Single-location brands can use the default Venue ID.
- Multi-location brands should map each CRM location to a ClassPass venue ID. Unmapped locations are not published to ClassPass, which prevents sessions from appearing under the wrong venue.
Availability
http
GET /api/classpass/{brandId}/availability?from=YYYY-MM-DD&to=YYYY-MM-DD
Authorization: Bearer <partnerApiKey>Optional query parameters:
classIdfilters to one session record, including both recurring and one-off sessions.locationIdfilters to one CRM location.
The response includes the ClassPass partner ID and an ordered sessions array. Each session includes:
venueId,scheduleId,classId,className,classType, andcategorydate,startTime,endTime, anddurationMinuteslocationId,locationName,instructorId, andinstructorNamecapacity,confirmedCount,waitlistCount,availableSpots, andbookingClosed
Only published recurring and one-off sessions are returned. Private reservations reduce available spots to zero so ClassPass cannot book a slot that has already been claimed as a whole-space private booking.
bookingClosed turns true the moment a session starts, not when it ends, and the booking endpoint refuses those sessions with the same rule. A session already under way cannot be joined.
Create booking
http
POST /api/classpass/{brandId}/bookings
Authorization: Bearer <partnerApiKey>
Content-Type: application/json
{
"externalBookingId": "classpass-booking-123",
"scheduleId": "11111111-1111-4111-8111-111111111111",
"reservedFor": "2026-06-18",
"customer": {
"email": "guest@example.com",
"firstName": "ClassPass",
"lastName": "Guest",
"phone": "+15555550123"
},
"notes": "Optional partner note"
}The booking endpoint is idempotent by externalBookingId. A first successful booking returns 201 with created: true; a replay of the same ClassPass booking returns 200 with created: false and the existing reservation.
Sauna CRM creates or reuses the customer by email, books the reservation with source: "classpass", and stores the external booking link for future cancellation.
Common booking errors:
404when the schedule does not exist for this brand.400when the schedule does not occur onreservedForor booking is closed.409when the session is full, already privately booked, or the same customer is already booked.
Cancel booking
http
POST /api/classpass/{brandId}/bookings/{externalBookingId}/cancel
Authorization: Bearer <partnerApiKey>Cancellation finds the reservation by ClassPass external booking ID. Active ClassPass bookings are cancelled without refunding customer credits, because ClassPass handles member credits and partner payment settlement outside Sauna CRM.
The endpoint returns 404 if the external booking ID is unknown. Repeating a cancellation for an already-cancelled booking returns the current reservation state.
Outbound availability webhook
The partner API above lets ClassPass pull availability. The outbound webhook pushes a notification the moment something changes on the Sauna CRM side, a session edit, a schedule change, or a non-ClassPass booking or cancellation that frees or fills a seat, so ClassPass can re-pull the affected scope instead of polling. ClassPass's own bookings and cancellations are intentionally not notified back: the partner already knows about its own actions.
Setup
In the ClassPass card, set:
- Webhook URL: the HTTPS endpoint ClassPass gives you to receive events. Saving rejects anything that is not
https://, that embeds credentials, or that points straight at a private, loopback, or link-local address. The host must also be publicly resolvable: because DNS can change between a save and a send, that is re-checked on every delivery, and a host resolving to a private address fails the delivery rather than the save. - Webhook secret: the shared secret used to sign every delivery. Store the same value on the receiving side to verify signatures.
Delivery starts automatically once the integration is Enabled and both a Webhook URL and Webhook secret are saved. The card shows a live delivery summary (queued, delivered, and failed counts over the last 7 days, the last successful delivery time, and a sample of recent failures). If deliveries fail repeatedly with no successes, the integration is flipped to Error so the misconfiguration is visible; re-saving the integration clears it.
Event types
Every delivery carries one of these type values:
session.availability_changed: a single session (schedule + date) changed its fill or private-booking state. Scope:scheduleId+reservedFor.class.changed: a session's attributes or publish state changed, or it was deleted. Scope:classId.schedule.changed: a schedule's timing or recurrence changed, or it was created or deleted. Scope:scheduleId.
Payload
The body is a JSON "re-sync this scope" nudge that also carries the current snapshot. removed: true means the scope no longer exists (unpublished or deleted) and the listing should be dropped.
json
{
"eventId": "8f3c…",
"type": "session.availability_changed",
"brandId": "…",
"partnerId": "partner-123",
"occurredAt": "2026-07-17T18:04:11.000Z",
"snapshotAt": "2026-07-17T18:04:42.128Z",
"scope": { "scheduleId": "…", "reservedFor": "2026-07-19" },
"removed": false,
"session": {
"scheduleId": "…",
"classId": "…",
"className": "Vinyasa",
"reservedFor": "2026-07-19",
"venueId": "venue-1",
"capacity": 20,
"confirmedCount": 12,
"availableSpots": 8,
"bookingClosed": false
}
}Depending on type, the snapshot is under session, class, or schedule. Treat the snapshot as authoritative as of snapshotAt, if you receive deliveries out of order, keep the one with the newest snapshotAt. Because the snapshot is read at delivery time, a retried or coalesced event always reflects current state rather than a stale value.
Headers and signature verification
Each POST carries:
X-ClassPass-Signature: sha256=<hex>X-ClassPass-Timestamp: <unix-seconds>X-ClassPass-Event-Id: <eventId>(stable across retries, use it to dedupe)X-ClassPass-Event-Type: <type>
The signature is HMAC-SHA256(secret, "<timestamp>.<rawBody>"), hex-encoded, where <rawBody> is the exact bytes of the request body and <timestamp> is the value of the X-ClassPass-Timestamp header. The timestamp is bound into the MAC (not just sent as a header) so an old body cannot be replayed under a fresh timestamp.
To verify:
- Read the raw request body before JSON parsing.
- Recompute
HMAC-SHA256(secret, timestampHeader + "." + rawBody). - Compare against the header value (strip the
sha256=prefix) using a constant-time comparison. - Reject timestamps outside an acceptable freshness window (for example, more than five minutes of skew) to bound replay.
js
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, headers, secret) {
const timestamp = headers["x-classpass-timestamp"];
const received = (headers["x-classpass-signature"] || "").replace(
/^sha256=/,
"",
);
const expected = createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(received);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Delivery semantics
- At-least-once. A delivery can arrive more than once; dedupe on
eventIdand prefer the newestsnapshotAt. - Retries. Failed deliveries (network error, timeout, non-2xx, or a 3xx redirect, which is never followed) are retried with exponential backoff and eventually dead-lettered after several attempts.
- Coalescing. A burst of changes to the same scope collapses into a single pending notification, so you receive one re-sync nudge rather than many.
- A
2xxresponse acknowledges receipt. Any other status is treated as a failure and retried, so respond2xxas soon as you have durably accepted the event.
Klaviyo fields
- Private API key: required for syncing profiles.
- List ID: required for customer subscription sync.
- Revision: optional API revision. Leave blank to use the current default.
- Webhook secret / URL: reserved for webhook setup.
Mailchimp fields
- API key: required for syncing audience members.
- Server prefix: the data-center prefix from the API key or Mailchimp URL, such as
us21. - Audience ID: required list ID for the audience.
- Legacy webhook secret: temporary compatibility for existing webhook URLs or headers that carry a shared secret.
- Webhook signing secret: enable signature verification when creating the Mailchimp webhook, then paste the one-time signing secret here. Signed deliveries must include Mailchimp's timestamped
X-Mailchimp-Signatureheader.
To migrate without downtime, leave the legacy secret saved while you create the signed webhook. Paste the signing secret and save the integration. Once the signing secret is set, signed authentication is required and legacy authentication is rejected. You can then remove the old webhook.
Tips
- Use Sync customers after importing customers, changing opt-in status, or updating provider credentials.
- Emails sent directly from the dashboard are managed on the Notifications page, not in Integrations.