Developers

Build on Reservd.

Put a studio's timetable in your app, book people in, and hear about every booking the moment it happens. The API runs on the same booking engine as the Reservd booking pages, so capacity, waitlists and passes behave exactly the same.

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.icsThe whole timetable.
/cal/studio/your-studio.ics?instructor=EllaOne instructor's classes, with how full they are.
/cal/me/<private token>.icsA 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

RequestWhat it does
GET/api/v1/studioYour 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-typesClass types (the things on your timetable).
GET/api/v1/productsPacks, 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/membersAdd 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}/passesA member's packs and memberships, with credits left.
GET/api/v1/bookings?class_id=&member_id=&status=Bookings, newest first.
POST/api/v1/bookingsBook 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=trueCancel. Without apply_rules it's a staff cancel with no late fee.
POST/api/v1/bookings/{id}/check-inCheck 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" }
  }
}
EventWhen
booking.createdA place is booked
booking.waitlistedSomeone joins a waitlist
booking.promotedSomeone moves off the waitlist
booking.cancelledA booking is cancelled
booking.checked_inSomeone checks in
booking.no_showSomeone doesn't turn up
booking.paidA booking is paid
member.createdA member joins
member.updatedA member's details change
payment.succeededA payment goes through
payment.failedA payment fails
payment.refundedA payment is refunded
pass.createdA pack or membership starts
pass.updatedA pack or membership changes
class.cancelledA 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.