Quick start
No code? Use the timetable widget or the booking button. Building an app? A studio owner creates an API key in Dashboard → Integrations, then:
curl https://www.reservd.co.uk/api/v1/classes \
-H "Authorization: Bearer rsv_live_…"{
"data": [
{
"id": "8c1d…",
"object": "class",
"name": "Reformer foundations",
"starts_at": "2026-10-01T08:30:00+00:00",
"ends_at": "2026-10-01T09:20:00+00:00",
"capacity": 8,
"places_taken": 6,
"places_left": 2,
"waitlist": 0,
"instructor": "Ella",
"status": "scheduled",
"drop_in_price_pence": 2400
}
]
}Timetable widget
Paste this into any website. It shows the next 7 days with places left and a Book button, and resizes itself.
<div data-reservd="your-studio"></div>
<script src="https://www.reservd.co.uk/embed.js" async></script>Options on the div: data-theme="dark", data-accent="ff8a5c" (your brand colour, no #), data-days="14". Bookings open in a new tab on the studio's Reservd page, so sign-in works on every browser.
Booking button
<a href="https://www.reservd.co.uk/s/your-studio" data-reservd-button>Book a class</a>
<script src="https://www.reservd.co.uk/button.js" async></script>Add data-theme="light" for dark backgrounds. For Instagram and link-in-bio pages, just use the link https://www.reservd.co.uk/s/your-studio.
Calendar feeds
Subscribe in Google, Apple or Outlook Calendar. Feeds refresh on their own.
/cal/studio/your-studio.ics | The whole timetable. |
/cal/studio/your-studio.ics?instructor=Ella | One instructor's classes, with how full they are. |
/cal/me/<private token>.ics | A member's own bookings across every studio. Members find their link on My bookings. |
API keys
Send the key in the Authorization header as Bearer rsv_live_…. Each key belongs to one studio and can only see that studio. Keep keys on your server: don't put them in a website or mobile app where people can read them. Revoke a key any time in Dashboard → Integrations.
Requests and responses are JSON. Times are ISO 8601 in UTC; show them in the studio's timezone. Money is in pence. Lists return { "data": [...], "has_more": true } and take limit (up to 200) and offset.
Endpoints
| Request | What it does |
|---|---|
GET/api/v1/studio | Your studio's details and booking rules. |
GET/api/v1/classes?from=&to= | The timetable with places left. 14 days from now by default, up to 92 days per request. |
GET/api/v1/classes/{id} | One class. |
GET/api/v1/class-types | Class types (the things on your timetable). |
GET/api/v1/products | Packs, memberships and drop-in products. |
GET/api/v1/members?email=&q=&limit=&offset= | Find members by exact email, or search name and email with q. |
POST/api/v1/members | Add a member. Body: full_name, email, phone (optional). |
GET/api/v1/members/{id} | One member. |
PATCH/api/v1/members/{id} | Change full_name or phone. |
GET/api/v1/members/{id}/passes | A member's packs and memberships, with credits left. |
GET/api/v1/bookings?class_id=&member_id=&status= | Bookings, newest first. |
POST/api/v1/bookings | Book a member in. Body: class_id, member_id, pass_id (optional). Full classes put them on the waitlist. |
GET/api/v1/bookings/{id} | One booking. |
DELETE/api/v1/bookings/{id}?apply_rules=true | Cancel. Without apply_rules it's a staff cancel with no late fee. |
POST/api/v1/bookings/{id}/check-in | Check someone in. DELETE on the same address undoes it. |
GET/api/v1/events?since=&type= | The event log, if you'd rather poll than use webhooks. |
Book someone in
curl -X POST https://www.reservd.co.uk/api/v1/bookings \
-H "Authorization: Bearer rsv_live_…" \
-H "Content-Type: application/json" \
-d '{ "class_id": "8c1d…", "member_id": "41fa…" }'The response is the booking. Its status is booked, or waitlisted if the class was full. Booking statuses: booked, waitlisted, cancelled, late_cancelled, attended, no_show.
Errors
HTTP 409
{ "error": { "code": "already_booked", "message": "You're already booked on that class." } }Codes include missing_key, invalid_key (401), invalid_id, missing_field (400), member_not_found, booking_not_found, class_not_found (404), already_booked, no_credits_left, weekly_limit_reached, not_cancellable (409).
Webhooks
Add an https address in Dashboard → Integrations. We send a POST with a JSON body for each event you choose. Reply with any 2xx within 10 seconds. If you don't, we retry after 1, 5 and 30 minutes, then 2, 6, 12 and 24 hours.
POST /your-endpoint
Reservd-Event: booking.created
Reservd-Signature: t=1790280000,v1=5f2b…
{
"id": "e7a0…",
"type": "booking.created",
"created_at": "2026-09-25T18:02:11Z",
"studio_id": "0b6c…",
"data": {
"booking": { "id": "…", "status": "booked", "payment_status": "due", "amount_pence": 1400 },
"class": { "id": "…", "name": "Vinyasa flow", "starts_at": "…" },
"member": { "id": "…", "full_name": "Maya R", "email": "maya@example.com" }
}
}| Event | When |
|---|---|
booking.created | A place is booked |
booking.waitlisted | Someone joins a waitlist |
booking.promoted | Someone moves off the waitlist |
booking.cancelled | A booking is cancelled |
booking.checked_in | Someone checks in |
booking.no_show | Someone doesn't turn up |
booking.paid | A booking is paid |
member.created | A member joins |
member.updated | A member's details change |
payment.succeeded | A payment goes through |
payment.failed | A payment fails |
payment.refunded | A payment is refunded |
pass.created | A pack or membership starts |
pass.updated | A pack or membership changes |
class.cancelled | A class is cancelled |
Checking signatures
Every request is signed with your endpoint's secret (whsec_…). Check it before trusting the body, and reject anything older than five minutes.
import crypto from "node:crypto";
function isFromReservd(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return v1?.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}Use the raw request body, before any JSON parsing. The same event can arrive more than once, so use its id to ignore repeats.