Bookings
List rooms and equipment, take reservations, and manage bookables, closures and requests.
In Loupe, anything people can book (a group room, a quiet seat, a laptop) is a bookable. A booking is a reservation of a bookable for a time range.
List bookables
curl "https://api.loupe.ch/api/public/bookables?organization.slug=riveville&bookableType=room" \ -H "Accept: application/json"{ "id": "0199…", "title": "Group room 2.04", "slug": "group-room-2-04", "bookableType": "room", "calendarType": "slot", "capacity": 6, "slotSize": 60, "minSlotPerReservation": 1, "maxSlotPerReservation": 3, "maxAdvanceDays": 14, "equipments": ["projector", "whiteboard"], "autoValidate": false, "membersOnly": false, "locationName": "Level 2"}slotSize is in minutes; a reservation spans between minSlotPerReservation and maxSlotPerReservation slots. A bookable with calendarType: "day" is booked by whole days instead. autoValidate tells you whether a reservation is confirmed immediately or waits for the team. equipments uses a fixed list: table, paperboard, projector, whiteboard, wheelchair_access, power_outlet.
One bookable by slug: GET /api/public/bookables/item?organization.slug=riveville&slug=group-room-2-04.
Find what is free
Ask for the bookables available on a day, or over a time range:
curl -X POST https://api.loupe.ch/api/public/bookables/available \ -H "Content-Type: application/json" \ -d '{ "organization": "riveville", "date": "2026-10-17", "bookableType": "room" }'For a range, send from and to instead of date. Add equipment to only get bookables that have it. Closures (holidays, maintenance) of one bookable are listed by GET /api/public/bookables/{id}/closures?from=…&to=….
Create a reservation
curl -X POST https://api.loupe.ch/api/public/bookables/0199…/reservation \ -H "Content-Type: application/json" \ -d '{ "startAt": "2026-10-17 14:00:00", "endAt": "2026-10-17 16:00:00", "email": "camille@example.org", "firstname": "Camille", "lastname": "Martin", "guests": 3 }'Times are local, in the library's time zone, and must match the bookable's slots. The response tells you whether the reservation is confirmed (validated: true) or pending, and carries a token and a reservationLink that the person also receives by e-mail.
Only confirmed reservations hold a slot: a pending request does not block anyone else until the team confirms it.
When the API refuses a reservation it answers 400 with a code in detail and context.reason:
| Code | Meaning |
|---|---|
slot_already_reserved | Someone already holds that time |
slot_closed, outside_opening_hours | The library is closed then |
guests_exceed_capacity | Too many people for the room |
duration_too_short, duration_exceeded | Fewer or more slots than allowed |
lead_time_not_reached | Too close to now; context.availableFrom says when booking opens |
beyond_booking_horizon | Too far ahead; see context.maxAdvanceDays |
below_min_participants | The group is smaller than required |
too_many_active_reservations | The person already has the maximum number of upcoming reservations |
members_only | Reserved for library members, see context.reason |
Reservation creation is rate limited per IP address (10, then 1 per minute). See Rate limiting before you book on behalf of many people.
The visitor's link
The token lets the person, or your site on their behalf, follow the reservation without an account. Treat it like a password.
curl "https://api.loupe.ch/api/public/reservations/view?token=…"curl -X POST "https://api.loupe.ch/api/public/reservations/cancel?token=…"Cancelling answers 204, also when the reservation was already cancelled, and e-mails the person.
Check-in. When a bookable has enableCheckIn, people confirm they came from the room itself: POST /api/public/bookables/{id}/checkin with their email and the device's latitude and longitude. The device must be near the library, and the check-in window opens shortly before the reservation (15 minutes by default) and closes shortly after it starts (10 minutes by default). A reservation not checked in by then is cancelled automatically. Refusals carry out_of_radius, reservation_expired or no_open_reservation.
Manage bookables
These endpoints need an API key whose owner holds the matching permission: bookables.create, bookables.update_own or bookables.update_any, and so on. "Own" covers the bookables that person created.
Create a bookable. It starts unpublished unless you send isPublished: true, which needs the publish permission:
curl -X POST "https://api.loupe.ch/api/manager/bookables?organization.slug=riveville" \ -H "X-Api-Key: $LOUPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Group room 2.04", "slug": "group-room-2-04", "bookableType": "room", "calendarType": "slot", "capacity": 6, "slotSize": 60, "minSlotPerReservation": 1, "maxSlotPerReservation": 3, "leadTimeAmount": 2, "leadTimeUnit": "hours", "maxAdvanceDays": 14, "autoValidate": false, "equipments": ["whiteboard"] }'title and slug are required; the slug must be unique within the organization. slotSize is 15, 30 or 60. The booking rules are all optional:
| Field | Rule |
|---|---|
leadTimeAmount + leadTimeUnit (hours or days) | How long in advance a reservation must be made |
maxAdvanceDays | How far ahead people can book, up to 730 days |
minParticipants | Smallest group, the person booking included |
membersOnly | Only library members can book |
By default a bookable follows the library's opening hours. Override them per weekday in settings.openingHours, for example "monday": { "inherit": false, "ranges": [{ "open": "09:00", "close": "12:00" }] }.
Update or publish with PATCH /api/manager/bookables/{id} and Content-Type: application/merge-patch+json; only the fields you send change. Changing isPublished needs bookables.publish_own or bookables.publish_any. Duplicate with POST /api/manager/bookables/{id}/duplicate: the copy is unpublished, keeps the rules, and has no closures or reservations. Delete with DELETE /api/manager/bookables/{id}: the bookable goes to the Loupe trash for 30 days.
Close slots
Close a bookable for maintenance or an event:
curl -X POST https://api.loupe.ch/api/manager/bookables/0199…/closures/batch \ -H "X-Api-Key: $LOUPE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "create": [{ "startAt": "2026-12-24 00:00:00", "endAt": "2026-12-27 00:00:00" }], "delete": ["0199…"] }'delete reopens closures by their id, as returned by GET /api/manager/bookables/{id}/closures?from=…&to=…. A closure only blocks new reservations: existing ones are kept, so cancel them separately if needed.
Manage reservations
| Method | Path | Use |
|---|---|---|
GET | /api/manager/reservations?organization=riveville&period=upcoming&page=1 | Reservations, 25 per page, as { items, totalItems, page, pageSize } |
GET | /api/manager/reservations/{id}?organization=riveville | One reservation, with its contact details and participants |
POST | /api/manager/reservations/{id}/validate | Confirm a pending reservation, optionally moving it with newBookable; the person is e-mailed |
POST | /api/manager/reservations/{id}/cancel | Cancel with a required {"reason": "…"}; the person is e-mailed |
GET | /api/manager/statistics/bookables?from=…&to=… | Booking statistics for a period |
They need reservations.own (reservations of your own bookables) or reservations.any. Validating answers 400 slot_already_reserved when another confirmed reservation overlaps. Use these two actions rather than patching or deleting a reservation directly: only they check overlaps and notify the person.