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.com for production. Replace the host with api-stage.onsched.com when 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.

LayerOpens / closes / capsScopeTimezoneVisible on GET /v3/availability?
Location weekly hoursOpensLocationLocation IANA (new locations default America/New_York)Yes
Service or resource weekly schedule, or allocation mode (not both)OpensThat entityLocation wall-clock (then company)Yes
Weekly / single allocationsOpens extra windowsService, resource, or pairSameYes
Recurring / one-off unavailabilityClosesEntityStored on the blockYes
External calendar busyClosesResourceEvent timesYes when those calendars are in use
Appointments, holds, paddingCloses / uses capacitySlotStored in UTC; daily caps use location or service-scoped TZYes
bookInAdvance, bookAheadUnit + bookAheadValueCaps the window (can clamp to empty)ServiceClock “now”Yes — days can vanish with no error
dailyBookingLimitCount / dailyBookingLimitMinutesCaps the calendar day, all customersServiceCompany TZ (company-scoped service) or location TZ (location-scoped)Yes
bookingsPerSlotCaps concurrent starts at one instantService, resource, allocationn/aYes
bookingLimit, maxBookingLimit, maxResourceBookingLimit, customerDailyBookingLimitCaps that customerService / resource / locationLocation TZ for the daily capNo — 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)

Stored default 1 on Service and Resource is fallback, not a cap over a higher peer.

Service bookingsPerSlotResource bookingsPerSlotEffective concurrent starts
1 (default)1 (default)1
1 (default)88 (service 1 is fallback)
81 (default)8 (resource 1 is fallback)
833 (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

FieldMeaning
bookInAdvanceMinimum minutes from now before a start is offered (default 0; past times are never returned)
bookAheadUnit0 = days, 1 = weeks, 2 = months
bookAheadValueHow 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.

  1. startDate / endDate are ISO 8601 or YYYY-MM-DD (not now). Names are ServiceId and LocationId — exact casing. Unknown query keys are dropped.
  2. The resource is linked to both that service and that location (Linked services).
  3. The entity has hours: schedule or allocation, not mixed incorrectly.
  4. Booking window (bookInAdvance / bookAhead*).
  5. Service-wide daily quota exhausted.
  6. bookingsPerSlot full at that start.
  7. Unavailability, padding, or external busy covering the window.
  8. roundRobin=COMBINED without every required id in ResourceIds.
  9. 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 bookingLimit and 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

  1. Confirm each location has a positive expirationDelay (new locations default to 900 seconds; older locations may still have none).
  2. Leave numeric limits at 0 (unlimited) until you add one limit and test hold + book.
  3. Never copy service-wide daily fields when you mean per-customer rules.
  4. After each change, call GET /v3/availability for 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[].fieldMeaningSuggested end-user message
bookingLimitUpcoming-appointment cap (service or this resource on this service)You’ve reached the limit of upcoming appointments.
maxBookingLimitLifetime cap for this serviceYou’ve used all bookings for this service.
maxResourceBookingLimitLifetime cap with this resource on this serviceYou’ve used all bookings with this provider.
customerDailyBookingLimitPer-customer visits this local day at this locationYou 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


Did this page help you?