Webhooks
Webhooks push StudioBase events to your other tools the moment they happen — no manual exports, no polling.
Go to Dashboard > Settings > Integrations > Webhooks to manage your endpoints.
Enabling webhooks
- Click Enable webhooks.
- Click Add endpoint, paste the HTTPS URL your tool gave you, and choose which events to receive (or check All events).
- Copy the signing secret shown after creation — you'll need it to verify deliveries. It's only shown once, but you can re-reveal it later with Reveal secret in the endpoint's Edit dialog.
Disabling webhooks stops new events from being recorded; deliveries already queued will still finish their retry cycles.
Events
| Event | Fires when |
|---|---|
booking.created | A class booking is created (paid checkout or class-pack credit) |
booking.cancelled | A booking is cancelled |
checkin.completed | A client checks in (self check-in, kiosk, or instructor) |
payment.completed | A class checkout payment completes |
refund.issued | A refund is issued for a booking |
ping | You click Send test event |
Booking-related events also include class context — class_name,
class_start_time, and instructor_name — so you can build messages like
"Alex checked in to Morning Yoga Flow at 9:00" without extra lookups.
Every delivery is a JSON POST shaped like:
{
"id": "9b2f…",
"type": "booking.created",
"version": 1,
"created_at": "2026-06-10T14:30:00+00:00",
"studio": { "id": "…", "slug": "your-studio" },
"data": {
"booking_id": "…",
"guest_email": "…",
"class_name": "Morning Yoga Flow",
"class_start_time": "2026-06-12T09:00:00+00:00",
"instructor_name": "Jordan Lee"
}
}
The id is stable across retries — use it to deduplicate if your receiver
sees the same event twice.
Verifying signatures
Deliveries are signed with the Standard Webhooks
scheme — headers webhook-id, webhook-timestamp, and webhook-signature
(v1,<base64 HMAC-SHA256 of "id.timestamp.body">, keyed by your secret).
Use any Standard Webhooks library, or verify manually. Both examples below also enforce a 5-minute replay window by rejecting timestamps older than 300 seconds:
const crypto = require("crypto");
function verify(secret, headers, rawBody) {
// Reject stale timestamps (replay protection).
const ts = Number(headers["webhook-timestamp"]);
if (!Number.isFinite(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const key = Buffer.from(secret.slice("whsec_".length), "base64");
const signedContent = `${headers["webhook-id"]}.${ts}.${rawBody}`;
const expected = crypto.createHmac("sha256", key).update(signedContent).digest();
return headers["webhook-signature"].split(" ").some((part) => {
if (!part.startsWith("v1,")) return false;
const provided = Buffer.from(part.slice(3), "base64");
return provided.length === expected.length && crypto.timingSafeEqual(provided, expected);
});
}
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: str) -> bool:
ts = headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret[len("whsec_"):])
signed = f'{headers["webhook-id"]}.{ts}.{raw_body}'
expected = base64.b64encode(hmac.new(key, signed.encode(), hashlib.sha256).digest()).decode()
return any(
hmac.compare_digest(p[len("v1,"):], expected)
for p in headers["webhook-signature"].split(" ")
if p.startswith("v1,")
)
Delivery and retries
Respond with any 2xx status within 10 seconds. Anything else is retried up to 7 times on a backoff schedule (30s, 2m, 10m, 30m, 2h, 6h, 12h) before the delivery is marked failed.
After 5 deliveries in a row exhaust their retries, the endpoint is automatically disabled. Once your receiver is fixed, re-check Active in the endpoint's Edit dialog — re-activating resets its failure count.
Troubleshooting
Open Deliveries next to any endpoint to see every attempt with its HTTP
status and error. Redeliver queues the delivery for the next worker run (within about a minute);
Send test event fires a ping so you can verify your receiver
end-to-end.
Need Help?
Contact support at support@studiobase.org if you have questions about webhooks.