REST API Reference

This reference documents 10 public endpoints under /api/public/{slug}/. All GET endpoints are publicly accessible without authentication. The POST /book endpoint requires an API key — pass it as Authorization: Bearer rsvly_....

Localization

Business-authored content (names, descriptions, staff bios, ticket-tier names) can be translated. By default every endpoint returns the raw translation maps — name_translations, description_translations, category_translations, bio_translations — and leaves it to you to pick a language.

Descriptions come in two forms. description — and its map, description_translations (bio_translations for staff) — is always plain text: display it as text, read it aloud, or hand it to an assistant. Services, staff, events and rentals also carry description_html and description_html_translations: the same description as sanitized HTML (paragraphs and line breaks today; bold, italic, underline, strikethrough, lists and links once a business formats its text), for integrations that want to render formatting. The two maps always list the same languages, and description is exactly the visible text of description_html. Never insert description as HTML; if you render description_html, pass it through your own HTML sanitizer first.

Alternatively, add ?locale= to /services, /staff, /events or /rentals and Reservly resolves them for you. The base fields carry the translated text, and the now-redundant *_translations keys are omitted from the response. Nested collections are resolved too — events[].inventory[] and add_on_rentals[].

The parameter is fully optional and never fails a request. Omit it and the response is byte-for-byte what it has always been, so existing integrations are unaffected. Supply a language the business does not offer — or a malformed value — and you get the business's own default content with a 200, not an error. The languages a business supports are listed as languages on /info.

Two limits worth knowing: /info does not accept locale (it returns no translatable fields), and rental category has no translations in the data model, so it is returned untranslated.

Deprecations

short_description and short_description_translations were removed on 2026-08-20 (FC-42 — announced for 2026-09-19, removed early pre-launch by founder decision; no registered consumers existed). Read description / description_translations.

Pricing quotes

Catalogue prices are list prices. A business's pricing rules (seasonal, day-of-week, flash-sale, early-bird…) can move what a booking actually costs, so /services, /events and /rentals also return quoted_price beside every price: the price of one unit computed by the same pricing engine the booking flow charges with. Each response carries a quote object stating what the quote assumes. Quote from quoted_price, never price — the list price is not what the customer will pay when rules apply.

Services and rentals quote for the business's current date by default; pass ?date=YYYY-MM-DD to quote a specific booking date (a malformed date returns 400). Events always quote for the event's own date and start time. For rentals the quoted number is the rules-adjusted rate (see price_type). Rules that need a booking time, a cart, or a customer identity are decided at booking time — POST /book returns the authoritative total, and quoted_price is null for unpriced (free) items or when quote.applies_pricing_rules is false.

quote.date does not mean the same thing on every endpoint. On /services and /rentals it is the booking date the quote was priced for — your ?date=, or the business's today. On /events it is the purchase date (the business's today), used only for buying-window rules such as flash sales and early-bird offers; each event's booking date is its own date field. Both are calendar dates in the business's timezone. Each response's quote.basis sentence states the same thing in words.

Business Info

GET/api/public/{slug}/info
Returns public business profile information including name, logo, currency, timezone, enabled modules, social links, action buttons, payment providers, and cancellation policy. online_bookings_paused is true while the business has paused online bookings for today (judged in the business's timezone): POST /book then answers 403 with messageCode not_accepting_bookings. Existing bookings and every read endpoint are unaffected, and the business reopens online bookings by itself on the day it chose.

Example Request

curl https://reservly.io/api/public/luxe-salon/info
bash

Example Response

{
  "name": "Luxe Salon",
  "slug": "luxe-salon",
  "logo_url": "https://cdn.reservly.io/logos/luxe-salon.png",
  "currency": "USD",
  "timezone": "America/New_York",
  "default_language": "en",
  "languages": ["en", "es"],
  "modules_enabled": {
    "services": true,
    "events": false,
    "rentals": false
  },
  "social_links": {
    "instagram": "https://instagram.com/luxesalon"
  },
  "action_buttons": [
    {
      "type": "link",
      "label": "Visit Website",
      "icon": "globe",
      "content": "https://luxesalon.com",
      "order": 0
    }
  ],
  "has_payment_providers": true,
  "payment_providers": ["stripe"],
  "payment_modes": { "services": "online", "rentals": "customer_choice", "events": "at_door" },
  "cancellation_policy": [
    { "hours_before": 24, "refund_percent": 100 },
    { "hours_before": 2, "refund_percent": 50 },
    { "hours_before": 0, "refund_percent": 0 }
  ],
  "cooling_off_hours": 24,
  "online_bookings_paused": false
}
json

List Services

GET/api/public/{slug}/services
Returns all active services for the business, sorted by display order. Includes pricing, duration, delivery mode, and translation data.

Parameters

NameTypeRequiredDescription
localestringOptionalResolve name, description and description_html into this language and omit the *_translations maps. See Localization.
datestringOptionalBooking date (YYYY-MM-DD) quoted_price should assume. Defaults to the business's current date. Malformed values return 400. See Pricing quotes.

Example Request

curl https://reservly.io/api/public/luxe-salon/services
bash

Example Response

{
  "services": [
    {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "name": "Haircut & Style",
      "description": "Full haircut with wash and blow-dry, finished with a style. Tell us how you like it and we will match the last cut you loved.",
      "description_html": "<p>Full haircut with wash and blow-dry, finished with a style. Tell us how you like it and we will match the last cut you loved.</p>",
      "duration_minutes": 60,
      "price": 75,
      "quoted_price": 86.25,
      "currency": "USD",
      "image_url": "https://cdn.reservly.io/services/haircut.jpg",
      "allow_multiple": false,
      "has_class_schedule": false,
      "name_translations": { "es": "Corte y Peinado" },
      "description_translations": null,
      "description_html_translations": null,
      "payment_requirement": "full",
      "deposit_value": null
    }
  ],
  "currency": "USD",
  "quote": {
    "date": "2026-08-07",
    "applies_pricing_rules": true,
    "basis": "quoted_price is the engine price of one unit booked on `date` (this business's pricing rules applied). Time-of-day, cart, and customer-specific rules are decided at booking; POST /book returns the authoritative total."
  }
}
json

List Staff

GET/api/public/{slug}/staff
Returns active staff members. Optionally filter by service to get only staff qualified for a specific service.

Parameters

NameTypeRequiredDescription
service_iduuidOptionalFilter staff to only those assigned to this service
localestringOptionalResolve name, category, description and description_html into this language and omit the *_translations maps. Note bio_translations resolves into description, and description_html_translations into description_html. See Localization.

Example Request

curl "https://reservly.io/api/public/luxe-salon/staff?service_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
bash

Example Response

{
  "staff": [
    {
      "id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
      "name": "Maria Garcia",
      "description": "Senior stylist with 10 years experience",
      "description_html": "<p>Senior stylist with 10 years experience</p>",
      "category": "Stylists",
      "image_url": "https://cdn.reservly.io/staff/maria.jpg",
      "bio_translations": { "es": "Estilista senior con 10 anos de experiencia" },
      "description_html_translations": { "es": "<p>Estilista senior con 10 anos de experiencia</p>" }
    }
  ]
}
json

Available Slots

GET/api/public/{slug}/slots
Calculates and returns available booking time slots for a given date and service. Accounts for business hours, staff schedules, existing bookings, blocked time, and schedule overrides. Each slot also carries its calendar date (date, dateOffset — 1 for the after-midnight part of an overnight shift) and, on class slots, remainingCapacity. computed is false (with an empty slots list) when availability could not be read for that date — test computed === false rather than treating an empty list as fully booked.

Parameters

NameTypeRequiredDescription
datestringRequiredDate in YYYY-MM-DD format
service_iduuidRequiredService UUID. Supports comma-separated IDs for multi-service bookings.
staff_iduuidOptionalFilter slots to a specific staff member
is_packagestringOptionalSet to "true" when booking a package (uses any-staff matching)

Example Request

curl "https://reservly.io/api/public/luxe-salon/slots?date=2026-04-15&service_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
bash

Example Response

{
  "slots": [
    {
      "time": "09:00",
      "endTime": "10:00",
      "staffId": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
      "staffName": "Maria Garcia",
      "date": "2026-04-15",
      "dateOffset": 0
    },
    {
      "time": "10:00",
      "endTime": "11:00",
      "staffId": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
      "staffName": "Maria Garcia",
      "date": "2026-04-15",
      "dateOffset": 0
    },
    {
      "time": "10:00",
      "endTime": "11:00",
      "staffId": "b2c3d4e5-a6b7-8901-cdef-234567890abc",
      "staffName": "Alex Kim",
      "date": "2026-04-15",
      "dateOffset": 0
    }
  ],
  "computed": true,
  "timezone": "America/New_York"
}
json

Rate limit: 60 requests / minute per IP

Available Dates (month calendar)

GET/api/public/{slug}/available-dates
Returns, for every day in a window of up to 62 days, how many bookable time slots the service has that day — computed with the same slot calculation /slots uses, so it is meant for colouring a date picker before the customer chooses a day. A day that is present with 0 was checked and has nothing free. Days before the business's today or past its advance-booking limit are left out of days entirely (not checked), and a window wholly in the past answers { "days": {}, "computed": true }. computed: false means the window could not be checked at all — treat it as unknown, not as fully booked. Errors are JSON with one error sentence: 400 for a missing or invalid parameter (for example "Window too large (max 62 days)."), 404 "Business not found", and 404 "Service not found: <id>" for a service id this business does not have. Responses may be cached for 10 seconds.

Parameters

NameTypeRequiredDescription
startstringRequiredFirst day of the window, YYYY-MM-DD (a real calendar date — 2026-02-30 is refused).
endstringRequiredLast day of the window, YYYY-MM-DD, on or after start. At most 62 days including both ends.
service_iduuidRequiredService UUID. Comma-separated for a multi-service booking, up to 20.
staff_iduuidOptionalCount only this staff member's slots.
variantsstringOptionalService variants as comma-separated serviceId:variantId UUID pairs.
is_packagestringOptionalSet to "true" when booking a package.
staffing_modestringOptionalFor a package: "per_service" lets each service have its own staff member; anything else means one provider for the whole package.

Example Request

curl "https://reservly.io/api/public/luxe-salon/available-dates?start=2026-10-01&end=2026-10-05&service_id=a1b2c3d4-e5f6-7890-abcd-ef1234567890"
bash

Example Response

{
  "days": {
    "2026-10-01": 116,
    "2026-10-02": 109,
    "2026-10-03": 105,
    "2026-10-04": 0,
    "2026-10-05": 109
  },
  "computed": true,
  "timezone": "America/New_York"
}
json

Rate limit: 20 requests / minute per IP

Upcoming Events

GET/api/public/{slug}/events
Returns upcoming active public events with capacity information and ticket inventory. Only events on or after today (in the business timezone) are included.

Parameters

NameTypeRequiredDescription
localestringOptionalResolve event name, description and description_html — and the nested inventory[] ticket-tier names — into this language, omitting the *_translations maps. See Localization.

Example Request

curl https://reservly.io/api/public/luxe-salon/events
bash

Example Response

{
  "events": [
    {
      "id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
      "name": "Summer Hair Workshop",
      "description": "Learn the latest styling techniques in a three-hour hands-on workshop. Tools and a take-home kit are provided; no experience needed.",
      "description_html": "<p>Learn the latest styling techniques in a three-hour hands-on workshop. Tools and a take-home kit are provided; no experience needed.</p>",
      "date": "2026-05-20",
      "start_time": "14:00:00",
      "end_time": "17:00:00",
      "image_url": "https://cdn.reservly.io/events/workshop.jpg",
      "total_capacity": 30,
      "spots_remaining": 18,
      "inventory": [
        {
          "id": "d4e5f6a7-b8c9-0123-def4-567890123456",
          "name": "General Admission",
          "price": 45,
          "quoted_price": 50,
          "total_capacity": 20,
          "spots_remaining": 12,
          "name_translations": null
        },
        {
          "id": "e5f6a7b8-c9d0-1234-ef56-789012345678",
          "name": "VIP Front Row",
          "price": 85,
          "quoted_price": 93.5,
          "total_capacity": 10,
          "spots_remaining": 6,
          "name_translations": null
        }
      ],
      "name_translations": null,
      "description_translations": null,
      "description_html_translations": null,
      "payment_requirement": "full",
      "deposit_value": null
    }
  ],
  "currency": "USD",
  "quote": {
    "date": "2026-08-07",
    "applies_pricing_rules": true,
    "basis": "inventory[].quoted_price is the engine price of ONE unit of that tier for its event's own date and start time (this business's pricing rules applied; `date` here is the purchase date used for sale windows). TABLE rows quote their flat deposit verbatim — deposits are never rule-adjusted. Cart-level and customer-specific rules settle at booking; POST /book returns the authoritative total."
  }
}
json

List Rentals

GET/api/public/{slug}/rentals
Returns active rental items with pricing, availability constraints, and upsell relationships. Supports hourly and daily rentals. Two quantity fields, and they mean different things: quantity is the owner's listed online offer, while max_bookable_online is the ceiling actually bookable online — the smaller of that offer and the rental's in-service units. Drive a quantity selector from max_bookable_online, not from quantity. It is an upper bound: maintenance blocks and existing bookings are not subtracted from it, so call GET /rental-availability for a specific date range. A rental with no listed offer reports 0 — an unknown offer never means unlimited. null appears only when the ceiling could not be determined for this response.

Parameters

NameTypeRequiredDescription
localestringOptionalResolve name, description and description_html — across both rentals[] and add_on_rentals[] — into this language, omitting the *_translations maps. category is not resolved by this endpoint (the same is true of /services; of the endpoints that expose a category, only /staff resolves it). See Localization.
datestringOptionalBooking date (YYYY-MM-DD) quoted_price should assume. Defaults to the business's current date. Malformed values return 400. See Pricing quotes.

Example Request

curl https://reservly.io/api/public/luxe-salon/rentals
bash

Example Response

{
  "rentals": [
    {
      "id": "a7b8c9d0-e1f2-3456-abcd-789012345678",
      "name": "Private Styling Suite",
      "description": "Fully equipped private suite for events, with styling chairs, mirrors, a wash station and its own entrance. Sleeps up to twelve guests comfortably.",
      "description_html": "<p>Fully equipped private suite for events, with styling chairs, mirrors, a wash station and its own entrance. Sleeps up to twelve guests comfortably.</p>",
      "category": "Spaces",
      "price": 150,
      "quoted_price": 172.5,
      "price_type": "hourly",
      "quantity": 2,
      "max_bookable_online": 2,
      "image_url": "https://cdn.reservly.io/rentals/suite.jpg",
      "min_rental_days": null,
      "max_rental_days": null,
      "min_rental_hours": 2,
      "max_rental_hours": 8,
      "name_translations": null,
      "description_translations": null,
      "description_html_translations": null,
      "payment_requirement": "deposit_percent",
      "deposit_value": 25
    }
  ],
  "add_on_rentals": [],
  "rental_upsells": [
    {
      "rental_id": "a7b8c9d0-e1f2-3456-abcd-789012345678",
      "upsell_rental_id": "b8c9d0e1-f2a3-4567-bcde-890123456789",
      "is_required": false,
      "sort_order": 0
    }
  ],
  "currency": "USD",
  "quote": {
    "date": "2026-08-07",
    "applies_pricing_rules": true,
    "basis": "quoted_price is the engine-adjusted rate (price_type unchanged) for a booking on `date`, this business's pricing rules applied to the listed rate as a single-unit base. Duration, quantity, setup fees, duration discounts, and time/cart/customer-specific rules settle at booking; POST /book returns the authoritative total."
  }
}
json

Rental Availability

GET/api/public/{slug}/rental-availability
Checks whether a quantity of one rental can be booked for a date range (and, for hourly rentals, a time window) before the customer fills in the rest of the booking. Answers { "available": true }, or { "available": false, "conflictReason": "…" } with a sentence saying why (for example "Only 3 available"). A rental id this business does not have — including another business's — answers 200 with { "available": false, "conflictReason": "Rental not found" }, exactly as an id nobody has. The answer is never cached. A unit that a customer still holds — marked picked up (or recorded as out) and not yet marked returned — counts as booked for the rest of the business's current day, so a range that includes today can answer unavailable even though that unit's booked dates have ended; future dates are never held this way. A range over several days (or a window over several hours) is available when enough units are free on every day (or at every moment) of it — the busiest day decides, never the total of every booking that touches the range somewhere. Errors are JSON with one error sentence: 400 for a missing or invalid parameter, 404 "Business not found". POST /book re-checks availability when the booking is made.

Parameters

NameTypeRequiredDescription
rentalIduuidRequiredThe rental to check.
startDatestringRequiredFirst day, YYYY-MM-DD.
endDatestringRequiredLast day, YYYY-MM-DD, on or after startDate. The range may span at most 730 days.
quantityintegerOptionalHow many units, 1–1000. Defaults to 1.
startTimestringOptionalHourly rentals: start time, HH:MM (24-hour).
endTimestringOptionalHourly rentals: end time, HH:MM (24-hour).
langstringOptionalLanguage code for the conflictReason sentence (one of the 36 dictionary codes, e.g. de). Falls back to the reservly_language cookie, then the business default. messageCode is unaffected.

Example Request

curl "https://reservly.io/api/public/luxe-salon/rental-availability?rentalId=b7c8d9e0-f1a2-3456-bcde-f78901234567&startDate=2026-10-10&endDate=2026-10-10&quantity=4"
bash

Example Response

{
  "available": false,
  "conflictReason": "Only 3 available"
}
json

Rate limit: 60 requests / minute per IP

Rental Available Dates (month calendar)

GET/api/public/{slug}/rental-available-dates
Returns, for every day in a window of up to 62 days and every rental you name, how many units a customer could book for a single-day hire that day — for colouring a rental date picker. A day present with 0 was checked and has nothing free; days before the business's today or past its advance-booking limit are left out (not checked). computed: false means the window could not be checked — treat it as unknown. Duplicate rental ids are counted once. The answer is never cached. Errors are JSON with one error sentence: 400 for a missing or invalid parameter (for example "Too many rental_id values (max 20)."), 404 "Business not found", and 404 "Rental not found" when none of the rental ids is one of this business's. In a list that mixes this business's rentals with ids it does not have, the others are left out of days rather than refused.

Parameters

NameTypeRequiredDescription
startstringRequiredFirst day of the window, YYYY-MM-DD (a real calendar date).
endstringRequiredLast day of the window, YYYY-MM-DD, on or after start. At most 62 days including both ends.
rental_iduuidRequiredRental UUID. Comma-separated for several rentals, up to 20.

Example Request

curl "https://reservly.io/api/public/luxe-salon/rental-available-dates?start=2026-10-01&end=2026-10-03&rental_id=b7c8d9e0-f1a2-3456-bcde-f78901234567"
bash

Example Response

{
  "days": {
    "2026-10-01": { "b7c8d9e0-f1a2-3456-bcde-f78901234567": 3 },
    "2026-10-02": { "b7c8d9e0-f1a2-3456-bcde-f78901234567": 3 },
    "2026-10-03": { "b7c8d9e0-f1a2-3456-bcde-f78901234567": 0 }
  },
  "computed": true,
  "timezone": "Europe/Zagreb"
}
json

Rate limit: 20 requests / minute per IP

Create a Booking

POST/api/public/{slug}/book
Creates a new booking. While the business has paused online bookings (see online_bookings_paused on GET /info) it answers 403 with messageCode not_accepting_bookings and creates nothing. The end time is calculated automatically from the combined duration of the selected services. If the business has payment providers connected and the service requires payment, the response includes payment details. A created booking answers HTTP 201. The request takes time as HH:MM; the response returns start_time and end_time as HH:MM:SS.

Parameters

NameTypeRequiredDescription
service_idsuuid[]RequiredArray of service UUIDs to book
datestringRequiredBooking date in YYYY-MM-DD format
timestringRequiredStart time in HH:MM format (24-hour)
customer_namestringRequiredFull name of the customer
customer_emailstringRequiredCustomer email address
staff_iduuidOptionalThe staff member to book (a staffId from /slots). Leave it out and, for a service a staff member performs, the first staff member free at that time is assigned in the business Auto-Assign order, the same choice the booking page makes. 409 when nobody is free then.
customer_phonestringOptionalCustomer phone number
form_dataobjectOptionalCustom form field responses. Reserved keys beginning with an underscore are ignored on API-key requests — in particular `_consent`, which records marketing/SMS opt-in and can only be set by the customer on the business’s own booking page.
party_sizenumberOptionalNumber of guests (defaults to 1)
payment_choicestringOptionalOne of "online" or "at_door" (anything else is a 400). Only used when the business lets the customer choose how to pay (see `payment_modes.services` on GET /info): "online" creates the charge and the response carries `paymentRequired: true`, "at_door" reserves now and the customer pays in person — no charge, no card. Ignored when the business always charges online or always collects at the door. Omitted under customer choice = at the door.
class_schedule_iduuidOptionalClass session UUID. For a class-based service the session is resolved from date + time and its seat is claimed automatically, and a time with no session is rejected — so you normally omit this. Send it only to disambiguate a date and time where two sessions of the same class start together.
sourcestringOptionalAttribution source (e.g. "vapi", "n8n", "make"). Tagged "api" if omitted.
preferred_providerstringOptionalPreferred payment provider: "stripe"

Request Body

{
  "service_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
  "date": "2026-04-15",
  "time": "10:00",
  "customer_name": "Jane Smith",
  "customer_email": "jane@example.com",
  "customer_phone": "+1-555-0123",
  "staff_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
  "source": "n8n",
  "party_size": 1
}
json

Example Request

curl -X POST https://reservly.io/api/public/luxe-salon/book \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer rsvly_YOUR_API_KEY" \
  -d '{
    "service_ids": ["a1b2c3d4-e5f6-7890-abcd-ef1234567890"],
    "date": "2026-04-15",
    "time": "10:00",
    "customer_name": "Jane Smith",
    "customer_email": "jane@example.com"
  }'
bash

Example Response

{
  "success": true,
  "booking": {
    "id": "e5f6a7b8-c9d0-1234-abcd-567890123456",
    "date": "2026-04-15",
    "start_time": "10:00:00",
    "end_time": "11:00:00",
    "status": "pending",
    "source": "n8n",
    "services": [
      {
        "name": "Haircut & Style",
        "duration_minutes": 60,
        "price": 75
      }
    ]
  }
}
json

Rate limit: 10 requests / minute per IP (429), and 100 requests / minute per API key (answered 401, see below)

Common Error Codes

StatusMeaningExample
400Invalid parametersMissing required field, bad date format, invalid UUID
401Not authenticated (POST /book)Missing, malformed, invalid, revoked or expired API key. Also returned when one key exceeds 100 requests per minute (the error reads "Rate limit exceeded").
403Not allowedThe API key's scope cannot create bookings (use "write" or "all"), the business's subscription is inactive, the business has paused online bookings (the body carries messageCode: "not_accepting_bookings"; see online_bookings_paused on GET /info), or the business account is scheduled for removal and no longer accepts bookings
404Not foundInvalid slug, service ID does not exist, or resource is inactive
409Booking conflictThe requested time slot is already booked or no longer available — or, when no staff_id was sent for a service a staff member performs, no staff member is free at that time
429Rate limitedToo many requests. Retry after the period indicated in the Retry-After header.
503Availability not verifiedPOST /book with no staff_id: Reservly could not count that date's availability to assign a staff member, so it booked nothing. Retry shortly.
500Server errorAn unexpected error occurred. Contact support if it persists.