{"openapi":"3.1.0","info":{"title":"RideLoop Developer API","version":"v1","description":"Read and manage bookings, payments, and fleet listings for your own operator account."},"servers":[{"url":"https://rideloop.co.uk"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key generated from Settings → API Keys, e.g. rlk_…"}}},"paths":{"/api/v1/bookings":{"get":{"operationId":"get_bookings","summary":"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}.","security":[{"bearerAuth":[]}],"x-required-scopes":["bookings:read"],"x-idempotent":false,"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"string"}},{"name":"offset","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"402":{"description":"Subscription inactive (write scopes only)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"403":{"description":"Plan lacks API access, or key lacks the required scope","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}}}},"post":{"operationId":"post_bookings","summary":"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.","security":[{"bearerAuth":[]}],"x-required-scopes":["bookings:write"],"x-idempotent":true,"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"402":{"description":"Subscription inactive (write scopes only)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"403":{"description":"Plan lacks API access, or key lacks the required scope","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"categoryId":{"type":"string","description":"Bike category to book — selects a bike-category booking. Provide this OR itemType/itemId, not both."},"size":{"type":"string","description":"Bike frame size — required when categoryId is set."},"itemType":{"type":"string","description":"\"tour\" or \"package\" — selects a guided-tour/holiday-package booking."},"itemId":{"type":"string","description":"The tour or package id — required when itemType is set."},"partySize":{"type":"string","description":"Number of guests — required for a tour booking."},"ratePlanType":{"type":"string","description":"\"FLEXIBLE\" or \"NON_REFUNDABLE\" — required for a package booking."},"startDate":{"type":"string","description":"ISO date the rental/tour/package starts."},"endDate":{"type":"string","description":"ISO date it ends — required for a bike-category or package booking, omitted for a single-day tour."},"guestName":{"type":"string","description":"The guest's name."},"guestEmail":{"type":"string","description":"The guest's email."},"guestPhone":{"type":"string","description":"The guest's phone number."},"message":{"type":"string","description":"A free-text note from the guest."},"paymentOption":{"type":"string","description":"\"ENQUIRE\" (no charge), \"DEPOSIT\", or \"FULL\". Defaults to \"ENQUIRE\"."},"couponCode":{"type":"string","description":"A campaign coupon code to redeem, if applicable."},"addOnIds":{"type":"string","description":"Array of add-on ids to attach (bike-category bookings only)."},"bikeRentalCategoryId":{"type":"string","description":"For a tour/package booking, also rent one of the operator’s own bikes alongside it."},"bikeRentalSize":{"type":"string","description":"Bike frame size — required when bikeRentalCategoryId is set."},"locationId":{"type":"string","description":"A pickup/dropoff location from the operator's own catalog."},"whatsappOptedIn":{"type":"string","description":"Whether the guest actively consented to WhatsApp booking reminders (default false — SMS is used instead when unset)."},"waiverAccepted":{"type":"string","description":"Must be `true` when the operator has an active waiver template."},"waiverSignerName":{"type":"string","description":"Required alongside waiverAccepted."},"waiverSignerIp":{"type":"string","description":"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":{"type":"string","description":"Must be `true` when the operator has an active terms & conditions template."}},"required":["startDate","guestName","guestEmail"]}}}}}},"/api/v1/bookings/{reference}":{"get":{"operationId":"get_bookings_by","summary":"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.","security":[{"bearerAuth":[]}],"x-required-scopes":["bookings:read"],"x-idempotent":false,"parameters":[{"name":"reference","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"402":{"description":"Subscription inactive (write scopes only)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"403":{"description":"Plan lacks API access, or key lacks the required scope","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}}}},"patch":{"operationId":"patch_bookings_by","summary":"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.","security":[{"bearerAuth":[]}],"x-required-scopes":["bookings:write"],"x-idempotent":true,"parameters":[{"name":"reference","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"402":{"description":"Subscription inactive (write scopes only)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"403":{"description":"Plan lacks API access, or key lacks the required scope","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"\"CONFIRMED\", \"CANCELLED\", or \"COMPLETED\"."}},"required":["status"]}}}}}},"/api/v1/bookings/{reference}/payment-links":{"post":{"operationId":"post_bookings_by_payment_links","summary":"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.","security":[{"bearerAuth":[]}],"x-required-scopes":["payments:write"],"x-idempotent":true,"parameters":[{"name":"reference","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"402":{"description":"Subscription inactive (write scopes only)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"403":{"description":"Plan lacks API access, or key lacks the required scope","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}}},"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"amountMinor":{"type":"string","description":"Amount to collect, in minor units. Defaults to the full outstanding balance; must not exceed it."}},"required":[]}}}}}},"/api/v1/bookings/{reference}/payments":{"get":{"operationId":"get_bookings_by_payments","summary":"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.","security":[{"bearerAuth":[]}],"x-required-scopes":["bookings:read"],"x-idempotent":false,"parameters":[{"name":"reference","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"402":{"description":"Subscription inactive (write scopes only)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"403":{"description":"Plan lacks API access, or key lacks the required scope","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}}}}},"/api/v1/fleet":{"get":{"operationId":"get_fleet","summary":"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.","security":[{"bearerAuth":[]}],"x-required-scopes":["fleet:read"],"x-idempotent":false,"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"402":{"description":"Subscription inactive (write scopes only)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"403":{"description":"Plan lacks API access, or key lacks the required scope","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}}}}},"/api/v1/availability":{"get":{"operationId":"get_availability","summary":"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.","security":[{"bearerAuth":[]}],"x-required-scopes":["fleet:read"],"x-idempotent":false,"parameters":[{"name":"productType","in":"query","required":false,"schema":{"type":"string"}},{"name":"categoryId","in":"query","required":false,"schema":{"type":"string"}},{"name":"size","in":"query","required":false,"schema":{"type":"string"}},{"name":"startDate","in":"query","required":false,"schema":{"type":"string"}},{"name":"endDate","in":"query","required":false,"schema":{"type":"string"}},{"name":"tourId","in":"query","required":false,"schema":{"type":"string"}},{"name":"date","in":"query","required":false,"schema":{"type":"string"}},{"name":"partySize","in":"query","required":false,"schema":{"type":"string"}},{"name":"packageId","in":"query","required":false,"schema":{"type":"string"}},{"name":"ratePlanType","in":"query","required":false,"schema":{"type":"string"}},{"name":"accommodationId","in":"query","required":false,"schema":{"type":"string"}},{"name":"variantId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Success"},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"402":{"description":"Subscription inactive (write scopes only)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"403":{"description":"Plan lacks API access, or key lacks the required scope","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","description":"e.g. invalid_request_error, authentication_error, permission_error, rate_limit_error"},"code":{"type":"string"},"message":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]}}}}}}}}}