Reference/API Reference
Every endpoint this API exposes, generated straight from the same OpenAPI spec you can download and import into Postman or any client generator — this page and /developers/openapi.json can never drift apart.
Before you start
Bearer API key scoped to your operator account, gated by the scopes each key was granted when you created it — see Authentication & scopes for how to generate one.Every booking moves through these statuses in order, left to right, except CANCELLED — which is reachable from any status before COMPLETED. Read a booking’s current status with GET /bookings/{reference} or drive it yourself with PATCH /bookings/{reference}.
| status | meaning |
|---|---|
| PENDING | Just created. Awaiting payment (if any is required) or manual confirmation. |
| CONFIRMED | Payment succeeded, or the booking needed none — the guest is locked in. |
| INPROGRESS | The rental/tour/package is currently underway. |
| COMPLETED | Finished — the bike was returned, or the tour/package ended. |
| CANCELLED | Cancelled from PENDING, CONFIRMED, or INPROGRESS. Any refund due is issued automatically. |
/api/v1/bookingsReturns a page of your bookings, newest first — the same records behind the operator portal’s Bookings list. Use this to poll for changes, reconcile your own records, or back a custom dashboard; if you just need to react to changes as they happen, a webhook subscription (see Webhooks) is cheaper than polling and lower-latency. Filter by status, or fetch a single booking directly via GET /bookings/{reference}.
limit — Max rows to return (default 20, max 100).offset — Rows to skip, for pagination (default 0, max 10,000).status — Filter by booking status, e.g. CONFIRMED.const response = await fetch('https://your-rideloop-domain/api/v1/bookings?limit=…', {method: 'GET',headers: {Authorization: 'Bearer rlk_...',},})const data = await response.json()
{"bookings": [{"id": "…","reference": "RB-2026-000123","status": "CONFIRMED","itemName": "Mountain Bike","startDate": "2026-08-01T00:00:00.000Z","endDate": "2026-08-03T00:00:00.000Z","guestName": "Jane Doe","guestEmail": "jane@example.com","estimatedTotalMinor": 10000,"amountPaidMinor": 5000,"outstandingMinor": 5000,"paymentOption": "DEPOSIT","paymentStatus": "PARTIALLY_PAID","depositMinor": 5000,"taxMinor": 0,"whatsappOptedIn": true,"createdVia": "STOREFRONT","createdAt": "2026-07-01T00:00:00.000Z","idVerificationStatus": "NOT_REQUIRED","rescheduleOfferStatus": "NOT_APPLICABLE"}],"pagination": { "limit": 20, "offset": 0, "totalCount": 1 }}
/api/v1/bookingsCreates a booking — either a bike-category rental (categoryId + size) or a guided tour/holiday package booking (itemType + itemId). Runs the exact same pricing, availability, waiver/terms, and payment pipeline as the public storefront. When the operator has an active waiver or terms template, this request must supply acceptance the same way a guest would — there is no relaxed path for API callers. Every new booking starts life as PENDING and fires a booking.created webhook immediately; for paymentOption "DEPOSIT"/"FULL", it only moves to CONFIRMED (and fires booking.confirmed + payment.received) once the guest actually completes the returned checkoutUrl — see How payments work.
categoryId — Bike category to book — selects a bike-category booking. Provide this OR itemType/itemId, not both.size — Bike frame size — required when categoryId is set.itemType — "tour" or "package" — selects a guided-tour/holiday-package booking.itemId — The tour or package id — required when itemType is set.partySize — Number of guests — required for a tour booking.ratePlanType — "FLEXIBLE" or "NON_REFUNDABLE" — required for a package booking.startDaterequired — ISO date the rental/tour/package starts.endDate — ISO date it ends — required for a bike-category or package booking, omitted for a single-day tour.guestNamerequired — The guest's name.guestEmailrequired — The guest's email.guestPhone — The guest's phone number.message — A free-text note from the guest.paymentOption — "ENQUIRE" (no charge), "DEPOSIT", or "FULL". Defaults to "ENQUIRE".couponCode — A campaign coupon code to redeem, if applicable.addOnIds — Array of add-on ids to attach (bike-category bookings only).bikeRentalCategoryId — For a tour/package booking, also rent one of the operator’s own bikes alongside it.bikeRentalSize — Bike frame size — required when bikeRentalCategoryId is set.locationId — A pickup/dropoff location from the operator's own catalog.whatsappOptedIn — Whether the guest actively consented to WhatsApp booking reminders (default false — SMS is used instead when unset).waiverAccepted — Must be `true` when the operator has an active waiver template.waiverSignerName — Required alongside waiverAccepted.waiverSignerIp — The guest's own IP address, if you have it — recorded as consent evidence. Never inferred from this request's own headers, since that would record YOUR server's IP, not the guest's.termsAccepted — Must be `true` when the operator has an active terms & conditions template.Always send an idempotency key
const response = await fetch('https://your-rideloop-domain/api/v1/bookings', {method: 'POST',headers: {Authorization: 'Bearer rlk_...','Content-Type': 'application/json',},body: JSON.stringify({"categoryId": "…","size": "M","startDate": "2026-08-01","endDate": "2026-08-03","guestName": "Jane Doe","guestEmail": "jane@example.com","paymentOption": "DEPOSIT","whatsappOptedIn": true}),})const data = await response.json()
{"id": "…","reference": "RB-2026-000124","status": "PENDING","itemName": "Mountain Bike","startDate": "2026-08-01T00:00:00.000Z","endDate": "2026-08-03T00:00:00.000Z","guestName": "Jane Doe","guestEmail": "jane@example.com","estimatedTotalMinor": 10000,"amountPaidMinor": 0,"outstandingMinor": 10000,"paymentOption": "DEPOSIT","paymentStatus": "AWAITING_PAYMENT","depositMinor": 5000,"taxMinor": 0,"whatsappOptedIn": true,"createdVia": "API","createdAt": "2026-07-01T00:00:00.000Z","idVerificationStatus": "NOT_REQUIRED","rescheduleOfferStatus": "NOT_APPLICABLE","checkoutUrl": "https://checkout.stripe.com/…"}
/api/v1/bookings/{reference}Fetches a single booking by its reference — the same shape returned by GET /bookings and pushed in every booking-related webhook payload, so you never need to reconcile two different representations. A booking moves through PENDING → CONFIRMED → INPROGRESS → COMPLETED as it’s paid, picked up, and returned; CANCELLED is reachable from any of the first three. Use PATCH on this same path to drive that transition yourself.
const response = await fetch('https://your-rideloop-domain/api/v1/bookings/{reference}', {method: 'GET',headers: {Authorization: 'Bearer rlk_...',},})const data = await response.json()
{"id": "…","reference": "RB-2026-000123","status": "CONFIRMED","itemName": "Mountain Bike","startDate": "2026-08-01T00:00:00.000Z","endDate": "2026-08-03T00:00:00.000Z","guestName": "Jane Doe","guestEmail": "jane@example.com","estimatedTotalMinor": 10000,"amountPaidMinor": 5000,"outstandingMinor": 5000,"paymentOption": "DEPOSIT","paymentStatus": "PARTIALLY_PAID","depositMinor": 5000,"taxMinor": 0,"whatsappOptedIn": true,"createdVia": "API","createdAt": "2026-07-01T00:00:00.000Z","idVerificationStatus": "NOT_REQUIRED","rescheduleOfferStatus": "NOT_APPLICABLE"}
/api/v1/bookings/{reference}Transitions a booking’s status. Setting status to "CANCELLED" runs the same cancellation pipeline as the operator portal (computes and issues any refund due, voids an open security-deposit hold) and fires a booking.cancelled webhook, plus payment.refunded if money came back — every other target is a plain status change, firing the matching booking.confirmed/booking.completed event. Only the transitions already valid for the booking’s current status are accepted (e.g. a COMPLETED booking can’t be reopened) — see GET /bookings/{reference} for the full status lifecycle.
statusrequired — "CONFIRMED", "CANCELLED", or "COMPLETED".Always send an idempotency key
const response = await fetch('https://your-rideloop-domain/api/v1/bookings/{reference}', {method: 'PATCH',headers: {Authorization: 'Bearer rlk_...','Content-Type': 'application/json',},body: JSON.stringify({ "status": "CANCELLED" }),})const data = await response.json()
{ "ok": true, "refundedMinor": 5000 }
/api/v1/bookings/{reference}/payment-linksCreates a Stripe Checkout link for whatever a booking still owes, and emails it to the guest. Defaults to the full outstanding balance. A more sensitive action than booking creation — money moves against an EXISTING booking — so it needs its own payments:write scope. Once the guest pays, a payment.received webhook fires and the booking's amountPaidMinor/outstandingMinor update — poll GET /bookings/{reference}/payments if you'd rather check than wait for the webhook.
amountMinor — Amount to collect, in minor units. Defaults to the full outstanding balance; must not exceed it.Always send an idempotency key
const response = await fetch('https://your-rideloop-domain/api/v1/bookings/{reference}/payment-links', {method: 'POST',headers: {Authorization: 'Bearer rlk_...','Content-Type': 'application/json',},body: JSON.stringify({ "amountMinor": 5000 }),})const data = await response.json()
{ "url": "https://checkout.stripe.com/…" }
/api/v1/bookings/{reference}/paymentsReturns every payment recorded against a booking, newest first — deposits, balance charges, and refunds alike, each with its own status. Use it to reconcile what a guest has actually paid against the booking’s estimatedTotalMinor/amountPaidMinor/outstandingMinor, or to look up a specific charge after receiving a payment.received, payment.failed, or payment.refunded webhook.
const response = await fetch('https://your-rideloop-domain/api/v1/bookings/{reference}/payments', {method: 'GET',headers: {Authorization: 'Bearer rlk_...',},})const data = await response.json()
{"payments": [{"id": "…","amountMinor": 5000,"currency": "GBP","type": "CHARGE","status": "SUCCEEDED","purpose": "DEPOSIT","createdAt": "2026-07-01T00:00:00.000Z"}]}
/api/v1/fleetReturns your published bike categories, guided tours, and holiday packages — the same catalog a guest sees on your storefront, minus anything still in draft. Each entry’s id is what you pass back as categoryId or itemId when creating a booking with POST /bookings, so this is typically the first call an integration makes: list the fleet, then book against one of the ids it returns.
const response = await fetch('https://your-rideloop-domain/api/v1/fleet', {method: 'GET',headers: {Authorization: 'Bearer rlk_...',},})const data = await response.json()
{"categories": [{ "type": "category", "id": "…", "slug": "mountain-bike", "name": "Mountain Bike", "shortDescription": "…" }],"tours": [{ "type": "tour", "id": "…", "slug": "alps-loop", "name": "Alps Loop", "subtitle": "…", "pricePerPersonMinor": 5000 }],"packages": [{ "type": "package", "id": "…", "slug": "tuscany-week", "name": "Tuscany Week", "subtitle": "…", "priceFromMinor": 80000 }]}
/api/v1/availabilityChecks whether a bike category, guided tour, or holiday package is available on real dates and returns its real price — never invented stock. When it isn’t available, you still get a 200 back with `available: false` and a real `alternatives` list of dates that ARE free, so your own UI can offer a next step instead of a dead end. Use GET /fleet first to get the real categoryId/tourId/packageId (and, for a package, this endpoint’s own variantOptions/accommodationOptions on a first call) — this endpoint always takes real ids, never a fuzzy name match. Also returns waiverRequired/termsRequired (collect these before calling POST /bookings whenever either is true) and the active add-ons catalog, each already priced for this exact stay (totalMinorForStay) — never recompute that yourself. When an automatic discount campaign applies, campaignName/discountMinor/discountedPriceMinor are included; otherwise they’re omitted entirely, so check for their presence rather than treating a missing discount as zero.
productType — Required. One of "bike", "tour", "package" — which required params below apply.categoryId — Bike only, required. From GET /fleet’s categories[].id.size — Bike only, required. One of the real RentalBikeCategory frame sizes, e.g. "M".startDate — Bike/package, required. YYYY-MM-DD.endDate — Bike: required. Package: required ONLY when the package has no fixed itinerary (a 400 names this when it applies) — an itinerary-based package's real end date is always derived from its own night count and this is ignored if passed. YYYY-MM-DD.tourId — Tour only, required. From GET /fleet’s tours[].id.date — Tour only, required. YYYY-MM-DD.partySize — Tour: required. Package: optional — only affects the result when the package’s own capacity is tracked per-person rather than per-booking.packageId — Package only, required. From GET /fleet’s packages[].id.ratePlanType — Package only, optional. "FLEXIBLE" (default) or "NON_REFUNDABLE".accommodationId — Package only, optional. One of a prior call’s own accommodationOptions[].id for this package.variantId — Package only, optional unless the package has variants (then required). One of a prior call’s own variantOptions[].id — omit to get an error naming the real options.const response = await fetch('https://your-rideloop-domain/api/v1/availability?productType=…', {method: 'GET',headers: {Authorization: 'Bearer rlk_...',},})const data = await response.json()
{"ok": true,"available": true,"productType": "bike","productId": "…","categoryName": "Mountain Bike","requestedStartDate": "2026-08-01","requestedEndDate": "2026-08-03","priceMinor": 6000,"securityDepositMinor": 500,"rentalDays": 3,"addOns": [{ "name": "Helmet", "pricingType": "FLAT", "priceMinor": 500, "totalMinorForStay": 500 }],"waiverRequired": false,"termsRequired": true,"hasElectricAssist": false,"categoryType": "MOUNTAIN","sizes": ["S", "M", "L"]}