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
Example Request
curl https://reservly.io/api/public/luxe-salon/infobash
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
}jsonList Services
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| locale | string | Optional | Resolve name, description and description_html into this language and omit the *_translations maps. See Localization. |
| date | string | Optional | Booking 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/servicesbash
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."
}
}jsonList Staff
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| service_id | uuid | Optional | Filter staff to only those assigned to this service |
| locale | string | Optional | Resolve 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>" }
}
]
}jsonAvailable Slots
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| date | string | Required | Date in YYYY-MM-DD format |
| service_id | uuid | Required | Service UUID. Supports comma-separated IDs for multi-service bookings. |
| staff_id | uuid | Optional | Filter slots to a specific staff member |
| is_package | string | Optional | Set 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"
}jsonRate limit: 60 requests / minute per IP
Available Dates (month calendar)
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| start | string | Required | First day of the window, YYYY-MM-DD (a real calendar date — 2026-02-30 is refused). |
| end | string | Required | Last day of the window, YYYY-MM-DD, on or after start. At most 62 days including both ends. |
| service_id | uuid | Required | Service UUID. Comma-separated for a multi-service booking, up to 20. |
| staff_id | uuid | Optional | Count only this staff member's slots. |
| variants | string | Optional | Service variants as comma-separated serviceId:variantId UUID pairs. |
| is_package | string | Optional | Set to "true" when booking a package. |
| staffing_mode | string | Optional | For 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"
}jsonRate limit: 20 requests / minute per IP
Upcoming Events
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| locale | string | Optional | Resolve 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/eventsbash
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."
}
}jsonList Rentals
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| locale | string | Optional | Resolve 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. |
| date | string | Optional | Booking 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/rentalsbash
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."
}
}jsonRental Availability
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| rentalId | uuid | Required | The rental to check. |
| startDate | string | Required | First day, YYYY-MM-DD. |
| endDate | string | Required | Last day, YYYY-MM-DD, on or after startDate. The range may span at most 730 days. |
| quantity | integer | Optional | How many units, 1–1000. Defaults to 1. |
| startTime | string | Optional | Hourly rentals: start time, HH:MM (24-hour). |
| endTime | string | Optional | Hourly rentals: end time, HH:MM (24-hour). |
| lang | string | Optional | Language 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"
}jsonRate limit: 60 requests / minute per IP
Rental Available Dates (month calendar)
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| start | string | Required | First day of the window, YYYY-MM-DD (a real calendar date). |
| end | string | Required | Last day of the window, YYYY-MM-DD, on or after start. At most 62 days including both ends. |
| rental_id | uuid | Required | Rental 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"
}jsonRate limit: 20 requests / minute per IP
Create a Booking
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| service_ids | uuid[] | Required | Array of service UUIDs to book |
| date | string | Required | Booking date in YYYY-MM-DD format |
| time | string | Required | Start time in HH:MM format (24-hour) |
| customer_name | string | Required | Full name of the customer |
| customer_email | string | Required | Customer email address |
| staff_id | uuid | Optional | The 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_phone | string | Optional | Customer phone number |
| form_data | object | Optional | Custom 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_size | number | Optional | Number of guests (defaults to 1) |
| payment_choice | string | Optional | One 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_id | uuid | Optional | Class 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. |
| source | string | Optional | Attribution source (e.g. "vapi", "n8n", "make"). Tagged "api" if omitted. |
| preferred_provider | string | Optional | Preferred 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
}jsonExample 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"
}'bashExample 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
}
]
}
}jsonRate limit: 10 requests / minute per IP (429), and 100 requests / minute per API key (answered 401, see below)
Common Error Codes
| Status | Meaning | Example |
|---|---|---|
| 400 | Invalid parameters | Missing required field, bad date format, invalid UUID |
| 401 | Not 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"). |
| 403 | Not allowed | The 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 |
| 404 | Not found | Invalid slug, service ID does not exist, or resource is inactive |
| 409 | Booking conflict | The 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 |
| 429 | Rate limited | Too many requests. Retry after the period indicated in the Retry-After header. |
| 503 | Availability not verified | POST /book with no staff_id: Reservly could not count that date's availability to assign a staff member, so it booked nothing. Retry shortly. |
| 500 | Server error | An unexpected error occurred. Contact support if it persists. |