Availability

  • GET /v3/availability?syncExternal=true: Availability cache invalidation now runs only after external calendar changes commit, and the request skips cached availability if sync cannot complete cleanly. This prevents stale cached slots from being returned after Google or Outlook busy blocks are synced into OnSched.

Availability

  • GET /v3/availability and POST /v3/appointment/hold: Default Service or Resource bookingsPerSlot: 1 now acts as fallback capacity instead of capping the other entity's higher same-slot capacity. For example, a Resource with bookingsPerSlot: 2 can accept a second concurrent hold even when the linked Service is left at its default bookingsPerSlot: 1.

API

Controller Error Statuses

  • API controllers now use typed error responses for invalid payloads, missing records, external integration failures, and unsupported external calendar states.
  • POST /v3/customer: Duplicate customer email attempts now return 409 Conflict instead of a generic server error.

Appointment Lifecycle And Audit

  • PUT /v3/appointment/{id}/cancel: Cancels now require write access to the authenticated company appointment and no longer allow a request scoped to one company to cancel another company's appointment.
  • PUT /v3/appointment/{id}/reschedule: Successful reschedule audit events are keyed to the new appointment ID so GET /v3/appointment/{newId}/audit returns the success event.
  • GET /v3/appointments: Date-only from / to filters now use the shared resource timezone when resourceIds are supplied without a locationId and all matching resources share one timezone.

External Calendars

  • GET / PUT / DELETE / regenerateAuthUrl on /v3/resource/{id}/externalCalendar/{externalCalendarId}: The path resource must belong to the authenticated company, and the external calendar must belong to that resource.
  • GET /v3/resource/{id}/externalCalendars/list: Pending OAuth shells are ignored when inferring the connected provider, and connected rows with refresh tokens are preferred.
  • DELETE /v3/resource/{id}/externalCalendar/{externalCalendarId}: Disconnecting a provider removes only sibling calendar rows that share the revoked refresh token instead of removing unrelated provider connections on the same resource.

API

V1 migration weekly availability

  • POST /v3/migration/sync: When a V1 resource uses allocation mode, migration now still copies the resource's V1 availability into V3 weeklyAvailability for profile and team display reads. Allocation rows remain the source for booking availability. Recreate failures mid-sync roll back weekly schedule replacement.

External calendar list

  • GET /v3/resource/{id}/externalCalendars/list: When provider is omitted, the API infers Google or Outlook from the resource's connected external calendar. Omitting provider still requires exactly one connected provider; otherwise the request fails as before.

Public docs: Booking limits, Weekly allocations.

Booking limits and service quotas

The platform now enforces configured limits consistently across GET /v3/availability, hold, reserve, book, and reschedule when a customer is known.

Per-customer caps

  • bookingLimit (service or resource) — caps active appointments (BK + IN; not RS) per customer.
  • maxBookingLimit / maxResourceBookingLimit (service) — lifetime caps per customer; rescheduled-away (RE) rows do not count.
  • Expired IN holds are ignored when counting active caps (aligned with availability SQL).
  • Limits are checked on reschedule, including metadata-only updates.

Service-wide daily quotas

  • dailyBookingLimitCount — hides slots on days when the service has reached its booking count cap.
  • dailyBookingLimitMinutes — hides slots when remaining same-day minute headroom is less than the candidate appointment duration.
  • PUT /book re-runs slot validation when confirming a reserved (RS) appointment so exhausted daily quotas cannot be bypassed.

Hold and book alignment

  • allowPaddingOverflow and ignoreExternalEvents chosen at hold or reserve are preserved when the appointment is booked.
  • 0 or omitted = unlimited for numeric limit fields unless noted otherwise.

Weekly allocation same-slot capacity

Corrected weekly allocation capacity semantics for service+resource pair scope:

  • bookingsPerSlot on weekly allocations applies only when both ServiceId and ResourceId scope the allocation.
  • Availability, hold, and book treat pair-scoped bookingsPerSlot as concurrent capacity at the same instant; other slots in the week are unaffected.
  • Weekly allocation routes do not support weekly booking quota fields.
  • Same-slot capacity uses one unit of load per booking (a third consumer is rejected when capacity is 2).

Availability reliability

  • COMBINED hold validation requires every requested resource to be free at the slot (aligned with GET /v3/availability capacity checks).
  • Redis availability cache is cleared when appointments are cancelled and when service or resource settings that affect slot generation change (for example dailyBookingLimitCount, duration, bookingsPerSlot).

Dashboard

  • Service, Resource, and Allocation forms group booking limit fields into labeled sections with inline helper copy that distinguishes same-slot capacity, service daily quotas, and per-customer caps.
  • Unlimited fields display No limit when the API value is 0.
  • Service info exposes customer limit dropdowns for bookingLimit, maxBookingLimit, and maxResourceBookingLimit.
  • Resource info exposes bookingLimit for the current service.
  • Weekly allocations scoped to a resource show the bookingsPerSlot same-slot capacity field when editing service allocation rows.

Calendar event templates

by ReadMe GitHub Action

Dashboard

  • Calendar templates: The Notification Templates page now includes a Calendar Templates pane with a Google/Outlook Event template.
  • Synced event content: The template Subject controls the Google/Outlook calendar event title, and the Body controls the event details. Calendar templates use the same merge variables and helpers as notification templates.

API

  • External calendars: Free/transparent provider events (for example Google Working Location or Outlook showAs: free) are not ingested and do not create Unavailability rows.
  • GET /v3/unavailability: Optional row fields external_transparency, external_event_type, external_busy for intervals from external calendar sync (null for other sources).
  • POST /v3/migration/sync: Imports v1 resource blocks (/setup/v1/resources/:id/blocks) as v3 recurring blocks per resource, with legacyId when the v1 block id is present.

Unavailability calendar rebuild & recurring-block timezones

Public docs: Unavailability blocks, Recurring blocks.

Unavailability calendar (GET /v3/unavailability)

  • Responses are one row per concrete interval, tagged with source (weekly, recurring, appointment, holiday, block) and the affected entity_type / entity_id. Rows are not merged.
  • The roundRobin query parameter is removed (previously unused for distinct merged modes).
  • ServiceId is optional; scope still requires at least one of LocationIds, ResourceIds, or ServiceId.
  • GET /v3/unavailability/calendar is deprecated and returns the same payload as GET /v3/unavailability — migrate callers to the canonical path.

Stored blocks

  • DELETE /v3/unavailability/blocks/:id removes an out-of-office (OOF) block by id (company ownership enforced). Use native flows for appointment-linked rows.

Recurring blocks

  • Rules persist iana with wall-clock startTime / endTime; expansions respect DST and fractional UTC offsets. Availability and calendar queries share this interpretation.

Integrators

  • Update any UI that assumed merged start_time/end_time only, or that passed roundRobin.
  • Expect snake_case fields on calendar rows (start_time, entity_type, …).

Dashboard — recurring unavailability on Availability tabs

See also Recurring blocks.

Merchant dashboard

On the Availability tab for locations, services, and resources, you can now manage recurring unavailable periods in addition to one-off blocks:

  • List recurring rules with schedule summary, wall-clock time window, timezone, and active date range.
  • Add or edit a rule (name, frequency, interval, start/end dates, times, weekday selection for weekly/biweekly, day-of-month for monthly; yearly repeats on the month and day of the start date).
  • Delete a rule with confirmation.

Behavior matches the existing /v3/unavailability/recurringBlock APIs (structured recurrence, not RFC 5545 RRULE strings). One-off blocks are still saved with the main Save action; recurring rules save immediately from the recurring-block dialog.

V1 → V3 migration sync expands service field coverage

Guide: Migrating from V1.

Migration

POST /v3/migration/sync now persists the full set of mappable Service fields when creating V3 services from V1, instead of only name, description, duration, weekly availability, and the schedule-vs-allocation type. Migrated services now also retain:

  • Duration options: durationSelect, durationMin, durationMax, durationInterval.
  • Book-ahead window: bookAheadUnit, bookAheadValue, bookInAdvance.
  • Capacity and limits: bookingLimit, bookingInterval, padding, bookingsPerSlot (from V1 maxCapacity), dailyBookingLimitCount, dailyBookingLimitMinutes, maxBookingLimit, maxResourceBookingLimit.
  • Fees: feeAmount, feeTaxable, cancellationFeeAmount, cancellationFeeTaxable, nonRefundable.
  • Other: imageUrl, showOnline, roundRobin (V1 integer mapped to V3 NONE / RANDOM / BALANCED / COMBINED), and custom fields (field1field10).

Type → availabilityType mapping (unchanged, now documented)

The mapping from V1 type to V3 availabilityType is unchanged but worth restating: V1 type=1 (Appointment) maps to V3 availabilityType=schedule; V1 type=2 (Event) maps to V3 availabilityType=allocation.

Re-running migration

Existing migrated tenants can backfill these fields by re-running POST /v3/migration/sync. The migration is idempotent on legacyId, so previously migrated services that already exist in V3 will be skipped — to pick up the expanded coverage on those rows, delete the V3 service (or update it manually) before re-syncing.

Fields still not migrated

Some V1 fields remain unmapped because V3 has no equivalent column or the concept has been retired (serviceGroupId/serviceGroupName, calendarId/calendarResourceGroupId, mediaPageUrl, defaultService, consumerPadding, maxGroupSize). For those, use native /v3/* endpoints to configure the equivalent V3 behavior after migration.

Allocation legacy IDs and mapping

Guides: Weekly allocations, Single allocations.

Mapping

  • GET /v3/mapping/ids accepts allocationId (V1 allocation ID). The response value is always a JSON array of matching V3 UUIDs (weekly and/or single allocation rows). Scalar keys (locationId, serviceId, etc.) remain a single UUID or null.

Allocations

  • POST /v3/singleAllocation/setSingleAllocations and POST /v3/weeklyAllocation/setWeeklyAllocations accept an optional legacyId on each allocation item so migrated data can retain the V1 identifier.

Existing Stage tenants can repopulate legacyId by re-running the V1 migration sync (or an allocation-only sync) so allocations are recreated from V1 with IDs attached.

Multi-resource template variables

  • Use case — Appointments that book more than one resource (e.g. couples or multi-provider sessions) can now show all resource names in custom email and SMS bodies, not only the first.
  • {{appointment.resources}} — Comma-separated names of every resource on the appointment.
  • {{#each resources}}...{{/each}} — Repeat a fragment once per resource. Inside the block, use bare keys like {{name}} or {{email}} for the current row; outer paths like {{appointment.time}} still work. Nested {{#each}} blocks are not supported in this release.
  • {{resources.0.name}}, {{resources.1.name}}, … — Optional indexed access when you want fixed slots in the copy.
  • Backward compatibility{{resource.name}}, {{appointment.resource}}, and {{customer.resource}} still refer to the first resource only, so existing templates do not change unless you edit them.

See the Notification guide for full merge syntax and examples.

Availability — allocation mode

  • Weekly schedule merge — Prevents a spurious full-day unavailability block (UTC 00:00–23:59) from merging with partial “closed outside open hours” blocks on the same calendar day, which could erase operating-hour gaps and return no slots (or the wrong slot set) when a SingleAllocation narrows the bookable window.
  • service_allocation_days — Avoids treating every date in a weekly allocation’s calendar range as a “covered” service day; that over-approximation could stop the “outside allocation” full-day guard from running and incorrectly open weekdays that the allocation pattern does not cover.