Added
OnSched 3.16.0 - location daily booking limit, tenant isolation, and pair allocations
about 15 hours ago by ReadMe GitHub Action
Location daily booking limit
- Location
customerDailyBookingLimitis an integer on merchant create/update/GET.0or omitted means unlimited. - The cap is per customer, per location, across all services. The calendar day is the appointment start in the location timezone (UTC if timezone is unset).
- Counted statuses are booked (
BK) and non-expired holds (IN). Reserved (RS), cancelled (CN), rescheduled-away (RE), and expired holds do not count. - Hold, reserve, book, and reschedule enforce the cap when a customer is known. Hold without a customer skips it; booking that hold still enforces it.
GET /v3/availabilityis unchanged. Public Location GET still returns onlyidandname.- Dashboard staff bookings are not exempt. The cap stacks with service, resource, lifetime, and service-wide daily limits.
- Reschedule excludes the current appointment, so moving off a full day frees it. Landing on a full day returns
400Customer has reached the daily booking limit for this location. - Customers already over a newly lowered cap keep existing appointments; only new hold/book/reschedule is blocked.
- The Merchant Portal Location form includes Daily appointments per customer (this location).
See Booking Limits.
Machine-token company isolation
- Machine tokens can read and modify only their bound company and its records. Foreign path IDs return
404; foreign body/query references return400. - Company creation, user-profile changes, migration, support tickets, and dashboard refresh-token generation require a dashboard user token (
403for machine tokens). - Company detail excludes a shared user's memberships in other companies. Location, Resource, and Service association payloads omit another company's linked records, including leftover join-table links. OAuth read/write scope enforcement remains in place.
Public booking Origin allowlist
/v3/public/*identifies the company fromx-client-id. The requestOriginmust match an Allowed Domain for that company (exact host;httporhttps). Missing or unmatchedOriginreturns403.x-api-keyis not required on public routes. Register real hostnames; wildcard*is rejected.- Browser widgets that already send
Originfrom a registered host are unchanged. Scripted callers withoutOrigin, or that relied on another company's domain or a substring match, receive403.
Webhook destinations
- Webhook URLs must be public
https.http, IP literals, localhost, internal hosts, and cloud-metadata destinations return400on create/replace. Deliveries pin to a public resolved address, do not follow redirects, and time out after five seconds. - Hop-by-hop and metadata request headers are rejected.
Authorizationand custom names such asWebhook-Secretstill work. Appointment create/cancel/reschedule still succeeds if a stored destination cannot be delivered. - Receivers on a private or CGNAT-only network need a public HTTPS ingress. Existing non-https rows remain on
GET /v3/webhookuntil replaced; they are not sent.
Personalized Docs login
- Logging into docs from the merchant portal now mints an identity-only Docs Handoff Token (name, email, docs version; expires in 15 minutes). It does not include an API key, dashboard JWT, or public client ID.
- Try It is no longer prefilled after that hop. Paste credentials from company settings. The docs URL may still carry the identity token as
auth_tokenbecause that is how ReadMe Personalized Docs login works.
Service pair allocations
- On Service → Allocations, choosing one resource now shows and saves only that service + resource pair. Resource-wide hours from
GET /v3/weeklyAllocationare no longer copied into the pair replace payload (which previously created extra or shifted allocation windows). - On Resource → Allocations, pair rows remain visible for the aggregate view, but Save sends only resource-wide windows. Pair hours stay on their service instead of being rewritten as resource-wide availability for every service on that resource.
- Pair
startDate/endDateonGET /v3/weeklyAllocationuse the resource timezone, matching create and get-by-id. Service-level rows still use the service location timezone.
Rate limits
- Native
/v3and/v3/publicrequests are rate limited per company (or per dashboard user when no company is on the request).POST /v3/oauth/tokenis limited per client id and by a global mint budget. Exceeding a budget returns429withRetry-After. - Machine and public availability reads share one company availability budget. Appointment list and cursor walks keep the budgets introduced in 3.13.0.
- Honor
Retry-Afterand retry the same request. Operators who need higher throughput can raise the platform rate-limit environment values.
OAuth scopes
POST /v3/oauth/tokenaccepts only OAuth Scopesreadandwrite. Unknown names (including mixed lists such asread admin) return400Invalid OAuth scopeand do not issue a token.- Omit
scope, or send a blank value, to receiveread write. The JSON response includes the grantedscopestring. - A
writetoken may call GET routes. Areadtoken cannot POST, PUT, PATCH, or DELETE (403Insufficient scope: write required).
JWT access token profiles
- Native
/v3Bearer tokens must be a Machine Token or a Dashboard JWT. The API checks HS256, issuer, audience, and expiry on every request. - Machine Token:
issonsched:v3-api,audapi. Tokens minted before this change fail until the client callsPOST /v3/oauth/tokenagain (about one hour). - Dashboard JWT: Supabase access token (
audauthenticated). A refresh JWT and a Docs Handoff Token return401Token could not be verified.
