Cragside API
Cragside is a scheduling platform for climbing gyms. This API exposes read access
to a gym's bookings, members, classes and payments. All responses are JSON, all
timestamps are UTC ISO 8601 (2026-06-11T14:02:31Z), and all list
endpoints share the same pagination and incremental-sync semantics.
This instance serves the demo dataset of Cragside Boulderhalle (Freiburg). The records are fictional; the API behaviour is real.
Authentication
Every /v1/* request needs a bearer token in the
Authorization header. This demo instance ships with the read-only
sandbox token cragside_read_demo_7f3kqp (operators can override it
with the CRAGSIDE_READ_TOKEN environment variable).
curl -H "Authorization: Bearer cragside_read_demo_7f3kqp" \
"$BASE/v1/bookings?limit=2"
A missing or wrong token returns 401:
{"error": "unauthorized", "hint": "Authorization: Bearer <read token>"}
Pagination & incremental sync
All four list endpoints accept the same query parameters:
| Param | Type | Description |
|---|---|---|
limit | integer | Rows per page. Default 100, maximum 500. |
updated_since | ISO 8601 | Only rows with updated_at strictly greater than this timestamp. Pass it on the first request of a sync; the cursor carries it forward. |
cursor | string | Opaque pagination token from meta.next_cursor. Pass it back verbatim; it pins the snapshot, the offset and the updated_since filter, so a paging loop is stable even while new rows arrive. |
Rows are sorted by (updated_at, id) ascending. Every response has the shape:
{
"data": [ ... ],
"meta": { "count": 100, "next_cursor": "eyJvIjoxMDAs..." }
}
meta.count is the number of rows in this page.
meta.next_cursor is present when more rows exist and null
on the last page.
Incremental sync recipe
- Full load:
GET /v1/bookings?limit=500, follownext_cursoruntil it isnull. - Remember the largest
updated_atyou saw. - Periodically:
GET /v1/bookings?updated_since=<that timestamp>, again followingnext_cursor.
updated_at changes whenever a row changes (a booking is cancelled or
checked in, a payment settles or is refunded, a membership freezes), so an
updated_since poll returns both newly created rows and
recently updated old rows — upsert on the primary key.
curl -H "Authorization: Bearer cragside_read_demo_7f3kqp" \
"$BASE/v1/bookings?updated_since=2026-06-11T06:00:00Z&limit=500"
GET /v1/bookings
One row per booked session: open-gym slots (class_id is
null, roughly 60%) and class reservations.
| Field | Type | Example |
|---|---|---|
booking_id | string, primary key | "bk_104233" |
member_id | string | "mem_0412" |
member_email | string | "lena.krause73@web.de" |
class_id | string or null | "cls_17" |
room | string | "boulder-1" — one of boulder-1, boulder-2, lead-wall, training-room, spire |
starts_at | ISO 8601 | "2026-06-11T18:30:00Z" |
party_size | integer 1–4 | 2 |
status | string | "confirmed" — one of confirmed, cancelled, checked-in, waitlist |
created_at | ISO 8601 | "2026-06-11T17:55:12Z" |
updated_at | ISO 8601, cursor field | "2026-06-11T18:34:08Z" |
curl -H "Authorization: Bearer cragside_read_demo_7f3kqp" "$BASE/v1/bookings?limit=1"
{
"data": [
{
"booking_id": "bk_100001",
"member_id": "mem_0683",
"member_email": "paula.winkler@gmx.de",
"class_id": null,
"room": "boulder-2",
"starts_at": "2026-03-01T08:20:00Z",
"party_size": 1,
"status": "confirmed",
"created_at": "2026-03-01T08:11:42Z",
"updated_at": "2026-03-01T08:11:42Z"
}
],
"meta": { "count": 1, "next_cursor": "eyJvIjoxLCJhIjoi..." }
}
GET /v1/members
| Field | Type | Example |
|---|---|---|
member_id | string, primary key | "mem_0042" |
name | string | "Jonas Weber" |
email | string | "jonas.weber14@gmail.com" |
plan | string | "monthly" — one of monthly, annual, punch-card, student |
joined_on | date | "2024-11-03" |
status | string | "active" — one of active, frozen, cancelled |
updated_at | ISO 8601, cursor field | "2026-03-01T00:00:00Z" |
GET /v1/classes
The weekly class schedule.
| Field | Type | Example |
|---|---|---|
class_id | string, primary key | "cls_07" |
name | string | "Lead Climbing 101" |
room | string | "lead-wall" |
weekday | string | "Thu" — Mon through Sun |
time | string HH:MM | "18:30" |
capacity | integer | 12 |
instructor | string | "Katja" |
updated_at | ISO 8601, cursor field | "2026-03-01T00:00:00Z" |
GET /v1/payments
Day passes, class fees and other point-of-sale charges. Most bookings produce a payment a few minutes after they are created.
| Field | Type | Example |
|---|---|---|
payment_id | string, primary key | "pay_203117" |
member_id | string | "mem_0412" |
amount | string, decimal euros | "12.50" |
method | string | "card" — one of card, sepa, cash, app |
status | string | "settled" — one of settled, pending, refunded |
created_at | ISO 8601 | "2026-06-11T18:01:09Z" |
updated_at | ISO 8601, cursor field | "2026-06-11T18:42:55Z" |
Other endpoints
| Endpoint | Auth | Description |
|---|---|---|
GET / | none | API root: name, version, endpoint list, docs URL. |
GET /docs | none | This page. |
GET /home | none | Product landing page with a demo "Add a booking" button. |
POST /demo/add-booking | none | Creates one demo booking with created_at = updated_at = now and returns it. Handy for verifying that an incremental sync picks up new rows. Kept in memory only. |
GET /healthz | none | {"ok": true} |
Rate notes
- Read endpoints: no hard limit; please stay under ~5 requests/second sustained.
POST /demo/add-booking: 10 requests per minute per IP, then429 {"error": "rate_limited"}.- Use
limit=500andnext_cursorfor bulk loads instead of many small pages.
Cragside · Scheduling for climbing gyms · demo dataset, fictional records.