Reference/API 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.

8 endpoints3 support an Idempotency-KeyOpenAPI 3.0

Before you start

Every request needs a 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.

Booking status lifecycle

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}.

statusmeaning
PENDINGJust created. Awaiting payment (if any is required) or manual confirmation.
CONFIRMEDPayment succeeded, or the booking needed none — the guest is locked in.
INPROGRESSThe rental/tour/package is currently underway.
COMPLETEDFinished — the bike was returned, or the tour/package ended.
CANCELLEDCancelled from PENDING, CONFIRMED, or INPROGRESS. Any refund due is issued automatically.
1

GET/api/v1/bookings

bookings:read

Returns 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}.

Query parameters

  • 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.

Example request

const response = await fetch('https://your-rideloop-domain/api/v1/bookings?limit=…', {
method: 'GET',
headers: {
Authorization: 'Bearer rlk_...',
},
})
const data = await response.json()

Example response

{
"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 }
}
2

POST/api/v1/bookings

bookings:writeIdempotency-Key supported

Creates 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.

Request body

  • 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

A retried request with the same key returns the original result rather than creating a second one. Without it, a network timeout on a slow connection can double-submit this call.

Example request

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()

Example response

{
"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/…"
}
3

GET/api/v1/bookings/{reference}

bookings:read

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.

Example request

const response = await fetch('https://your-rideloop-domain/api/v1/bookings/{reference}', {
method: 'GET',
headers: {
Authorization: 'Bearer rlk_...',
},
})
const data = await response.json()

Example response

{
"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"
}
4

PATCH/api/v1/bookings/{reference}

bookings:writeIdempotency-Key supported

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.

Request body

  • statusrequired — "CONFIRMED", "CANCELLED", or "COMPLETED".

Always send an idempotency key

A retried request with the same key returns the original result rather than creating a second one. Without it, a network timeout on a slow connection can double-submit this call.

Example request

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()

Example response

{ "ok": true, "refundedMinor": 5000 }
5

POST/api/v1/bookings/{reference}/payment-links

payments:writeIdempotency-Key supported

Creates 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.

Request body

  • amountMinor — Amount to collect, in minor units. Defaults to the full outstanding balance; must not exceed it.

Always send an idempotency key

A retried request with the same key returns the original result rather than creating a second one. Without it, a network timeout on a slow connection can double-submit this call.

Example request

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()

Example response

{ "url": "https://checkout.stripe.com/…" }
6

GET/api/v1/bookings/{reference}/payments

bookings:read

Returns 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.

Example request

const response = await fetch('https://your-rideloop-domain/api/v1/bookings/{reference}/payments', {
method: 'GET',
headers: {
Authorization: 'Bearer rlk_...',
},
})
const data = await response.json()

Example response

{
"payments": [
{
"id": "…",
"amountMinor": 5000,
"currency": "GBP",
"type": "CHARGE",
"status": "SUCCEEDED",
"purpose": "DEPOSIT",
"createdAt": "2026-07-01T00:00:00.000Z"
}
]
}
7

GET/api/v1/fleet

fleet:read

Returns 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.

Example request

const response = await fetch('https://your-rideloop-domain/api/v1/fleet', {
method: 'GET',
headers: {
Authorization: 'Bearer rlk_...',
},
})
const data = await response.json()

Example response

{
"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 }]
}
8

GET/api/v1/availability

fleet:read

Checks 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.

Query parameters

  • 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.

Example request

const response = await fetch('https://your-rideloop-domain/api/v1/availability?productType=…', {
method: 'GET',
headers: {
Authorization: 'Bearer rlk_...',
},
})
const data = await response.json()

Example response

{
"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"]
}