Availability rules and interactions
How hours, blocks, booking windows, quotas, and per-customer caps interact, and why GET /v3/availability can return no slots.
Environment tip: All examples use
v3.onsched.comfor production. Replace the host withapi-stage.onsched.comwhen calling the staging environment.
Purpose
This page explains how availability settings combine. For the GET /v3/availability request itself, see Availability Guide. For field-by-field limits, see Booking Limits.
Hours and allocations open time. Blocks, external busy, appointments, holds, and padding close time. Booking windows, daily quotas, and bookingsPerSlot cap what remains. Per-customer caps do not hide slots; they return 400 when that customer tries to hold or book.
Precedence
When layers overlap, closures win. An open hour does not restore a blocked or booked interval.
| Layer | Opens / closes / caps | Scope | Timezone | Visible on GET /v3/availability? |
|---|---|---|---|---|
| Location weekly hours | Opens | Location | Location IANA (new locations default America/New_York) | Yes |
| Service or resource weekly schedule, or allocation mode (not both) | Opens | That entity | Location wall-clock (then company) | Yes |
| Weekly / single allocations | Opens extra windows | Service, resource, or pair | Same | Yes |
| Recurring / one-off unavailability | Closes | Entity | Stored on the block | Yes |
| External calendar busy | Closes | Resource | Event times | Yes when those calendars are in use |
| Appointments, holds, padding | Closes / uses capacity | Slot | Stored in UTC; daily caps use location or service-scoped TZ | Yes |
bookInAdvance, bookAheadUnit + bookAheadValue | Caps the window (can clamp to empty) | Service | Clock “now” | Yes — days can vanish with no error |
dailyBookingLimitCount / dailyBookingLimitMinutes | Caps the calendar day, all customers | Service | Company TZ (company-scoped service) or location TZ (location-scoped) | Yes |
bookingsPerSlot | Caps concurrent starts at one instant | Service, resource, allocation | n/a | Yes |
bookingLimit, maxBookingLimit, maxResourceBookingLimit, customerDailyBookingLimit | Caps that customer | Service / resource / location | Location TZ for the daily cap | No — enforced at hold/book/reschedule |
A service or resource uses schedule (weeklyAvailability) or allocation (weekly + single allocations), never both. Locations always use schedule mode.
Same-slot capacity (bookingsPerSlot)
bookingsPerSlot)Stored default 1 on Service and Resource is fallback, not a cap over a higher peer.
Service bookingsPerSlot | Resource bookingsPerSlot | Effective concurrent starts |
|---|---|---|
1 (default) | 1 (default) | 1 |
1 (default) | 8 | 8 (service 1 is fallback) |
8 | 1 (default) | 8 (resource 1 is fallback) |
8 | 3 | 3 (both explicit and greater than 1: lower wins) |
A resource-specific allocation bookingsPerSlot overrides both for that allocation window. Weekly allocation bookingsPerSlot applies only to service+resource weekly rows.
Booking window
| Field | Meaning |
|---|---|
bookInAdvance | Minimum minutes from now before a start is offered (default 0; past times are never returned) |
bookAheadUnit | 0 = days, 1 = weeks, 2 = months |
bookAheadValue | How many of those units ahead |
GET /v3/availability silently intersects your startDate/endDate with this window. If nothing remains, you get 200 with empty availableTimes — not a 400.
Example: bookInAdvance: 1440 (24 hours) and a request for “today” in the morning returns no slots even if the calendar looks open.
Zero slots?
Work this list in order. Empty availability is usually a 200, not an error.
startDate/endDateare ISO 8601 orYYYY-MM-DD(notnow). Names areServiceIdandLocationId— exact casing. Unknown query keys are dropped.- The resource is linked to both that service and that location (Linked services).
- The entity has hours: schedule or allocation, not mixed incorrectly.
- Booking window (
bookInAdvance/bookAhead*). - Service-wide daily quota exhausted.
bookingsPerSlotfull at that start.- Unavailability, padding, or external busy covering the window.
roundRobin=COMBINEDwithout every required id inResourceIds.- Per-customer caps: they will not empty this response. They 400 later.
Holds without expiry
Location expirationDelay is an integer number of seconds. A positive value expires an IN hold after that many seconds if it is not booked or reserved.
New locations default to 900 seconds (15 minutes) when the field is omitted. A positive value sets a custom delay; 0 or null disables the timer. On update, omitting the field keeps the stored value. Existing locations are not backfilled.
Warning: Without a positive delay, the hold never expires. It keeps the slot closed and counts toward
bookingLimitand daily quotas until you book, reserve, or delete it. Locations created before this default may have no expiry and should set a positive delay.
Worked scenarios
1. Group class
Ten people may share 9:00 AM. Availability stays until ten concurrent bookings; the eleventh start disappears for everyone.
{
"bookingsPerSlot": 10
}Do not set dailyBookingLimitCount: 10 unless you also want at most ten bookings that whole day across every start time.
2. Clinic: one visit per patient per day
Set the location per-customer daily cap. Availability still lists the day; a second hold the same local day returns 400.
{
"customerDailyBookingLimit": 1
}A booking at 11:00 PM Eastern counts as that location’s calendar date in America/New_York (the default for new locations), not as “the next UTC day”.
dailyBookingLimitCount: 1 on the service instead would allow one booking total that day for all patients combined.
3. Trainer: at most two upcoming, three in a lifetime
{
"bookingLimit": 2,
"maxBookingLimit": 3
}bookingLimit caps how many upcoming appointments a customer can hold for this service at once. It counts booked appointments that have not ended yet plus unexpired holds. Reserved (RS), cancelled, rescheduled-away and past appointments do not count, so a spot frees up as soon as an appointment ends. To cap total bookings over a customer lifetime, use maxBookingLimit.
After two upcoming sessions, a third hold is 400 bookingLimit. After three lifetime uses (including cancelled rows), further holds are 400 maxBookingLimit even if nothing is upcoming.
Safe defaults
- Confirm each location has a positive
expirationDelay(new locations default to 900 seconds; older locations may still have none). - Leave numeric limits at
0(unlimited) until you add one limit and test hold + book. - Never copy service-wide daily fields when you mean per-customer rules.
- After each change, call
GET /v3/availabilityfor a known day and then hold as the test customer.
Limit errors at booking time
HTTP 400. Per-customer caps are not applied to anonymous availability.
errors[].field | Meaning | Suggested end-user message |
|---|---|---|
bookingLimit | Upcoming-appointment cap (service or this resource on this service) | You’ve reached the limit of upcoming appointments. |
maxBookingLimit | Lifetime cap for this service | You’ve used all bookings for this service. |
maxResourceBookingLimit | Lifetime cap with this resource on this service | You’ve used all bookings with this provider. |
customerDailyBookingLimit | Per-customer visits this local day at this location | You already have a visit at this location today. |
Example:
{
"success": false,
"message": "Customer has reached the maximum number of active appointments for this service.",
"errors": [
{
"field": "bookingLimit",
"message": "Active booking limit reached"
}
]
}UI pattern when you know the customer: show slots → POST /v3/appointment/hold → on 400 read errors[].field → offer cancel, another day, or another service.
See also
Updated 1 day ago
