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.
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 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.
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.
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
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
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:
Other:imageUrl, showOnline, roundRobin (V1 integer mapped to V3 NONE / RANDOM / BALANCED / COMBINED), and custom fields (field1–field10).
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.
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.
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.
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.
Dashboard — Company settings — Under Company Info, Disable notifications is replaced by Disable email notifications and Disable SMS notifications. Each toggle suppresses only that channel org-wide (appointment and related outbound notifications); webhooks are unchanged.
API — Company model — Responses include disableEmailNotifications and disableSmsNotifications instead of disableNotifications. PATCH / POST bodies should send those fields when updating behavior per channel.
Backward compatibility — writes — If the body includes disableNotifications and neitherdisableEmailNotifications nor disableSmsNotifications, both channel flags are set from that single value (same semantics as before). The legacy field is deprecated in OpenAPI.
Database — Migration adds the two columns, copies values from disableNotifications, then drops disableNotifications. Deploy migrations before relying on the new API shape.
V1 shim — disableEmailAndSmsNotifications is true only when both email and SMS are disabled for the company (equivalent to “all outbound email/SMS off”).
Faster list endpoints — GET /v3/locations, GET /v3/services, GET /v3/resources, and GET /v3/companies now read with a smaller include shape (the list responses no longer eager-load Address or Company objects) and count the total with a direct COUNT(*) on the base table when no association filter is applied. The response fields stay the same when you list without include; fetch a single resource (or pass include=services,resources / include=locations,resources etc.) to hydrate the full object graph. A short per-request Redis cache collapses dashboard refresh bursts into a single database round trip.
Company search is now a prefix match — The optional search query parameter on GET /v3/companies (used by the dashboard company switcher) now matches company names by prefix, backed by a functional lower-name index. A search for acme matches “Acme Corp” and “ACME Clinic” but no longer matches “Great Acme Holdings.” If you rely on the old substring behavior, switch your client to match by leading characters (for example, type the start of the company name rather than a substring).
Dashboard navigation — The company switcher debounces typed input a little longer and skips one-character queries (which match almost everything and only add load) before issuing a request. The locations, services, and resources dashboards no longer re-fetch the full company-wide lists on every row click; they prime reference data once per company context and leave warm caches alone.
Integration notes
Public API callers that pass include=services,resources (or analogous combinations) continue to receive the same nested payload on the list endpoints. Only the default (no-include) list shape trims the always-on Address/Company objects, which were never documented as part of the list payload.
The prefix search is the only customer-visible behavior change on GET /v3/companies?search=…. The parameter name, limit, and offset semantics are unchanged.
No migrations are required on the caller side. A Postgres functional index (company_lower_name_prefix_active_idx) is added by a platform migration for the new prefix search.
Channel — Notification templates support channel: EMAIL or SMS. Existing rows default to EMAIL; SMS copy can differ while reusing the same merge variables and tooling as email.
Resolution — When sending SMS, the API uses an SMS template if present, otherwise falls back to the email template for that type and recipient (then platform defaults), so behavior stays unchanged until you add SMS-specific templates.
Sending — If a recipient uses both email and SMS (ALL), email and SMS bodies are built separately from the matching channel (or fallback).
API — GET / POST/v3/emailTemplates accept optional channel (default EMAIL). EXTERNAL_CALENDAR_EVENT cannot be saved as SMS (calendar descriptions stay email-only). Database: unique (CompanyId, type, recipientType, channel); run migrations before deploy.
Portal — Notification templates page adds Email | SMS tabs; SMS body is what is sent as plain text after merge (subject is stored but not sent via SMS).
Dashboard RBAC
Roles — Owner, Admin, User, and Resource dashboard roles now drive what company data and scheduling entities each login can read or change. Machine integrations using OAuth client credentials are unchanged: access remains scope-based (read/write as today); dashboard role flags on the synthetic principal do not apply.
User location scope — User accounts may have locationAccessMode (COMPANY = all locations, or LOCATIONS with an allowedLocationIds list). Writes and list results for locations, services, resources, and appointments respect this scope (including company-scoped services vs location-linked services).
Resource role — A Resource login is tied to a linkedResourceId and may read non-secret company settings and address, read linked locations/services, and read/write only that resource row. Company responses for this role omit API keys, client secrets, and similar credentials.
User management — Owner may assign any role including Owner. Admin may not assign Owner, may not change another Admin’s role, and may promote User → Admin (per product rules). At least one Owner must remain; demoting the last owner continues to be rejected with the same error the dashboard expects.
API
setUsers / adjustUsers — Request bodies may include locationAccessMode, allowedLocationIds, and linkedResourceId per user; linkedResourceId is required when the role is resource and must belong to the company. OpenAPI and bundled openapi.json are updated.
Portal
Navigation — API Settings is hidden for Resource roles; Company settings tabs that require user management are gated for User roles.
API Settings — User roles can view the page but cannot regenerate or rotate credentials where the API forbids it; Resource roles are redirected away.
Company users — Forms support location scope, allowed locations, and resource linking, with client-side rules aligned to the admin matrix.