Multilocation Business APIs
Twenty-four web endpoints list a business's records across every location the signed-in user may read. Each one answers for the whole business, or for the locations picked in the location filter. The mobile app's endpoints are unchanged.
Status
| Part | State |
|---|---|
| Shared location scope (who may read which locations) | Pushed. Running on the preview VM. |
| 24 list endpoints | Pushed. Running on the preview VM. |
| Contract for all 24 (requests, responses, errors, cache keys) | Written. Status: proposed. |
| Unit tests | 10,433 pass across the nine libraries touched and the controller tests. 3 scheduling tests fail; they compare the server's form-answer table with the client app's widget code, which this work did not change. |
| Live check on the preview VM | 24 of 24 endpoints pass. 17 were compared against real rows; 7 have no data on the VM. |
| Production | Not deployed. The branch is 93 commits behind develop and the PR is a draft. |
| Web pages reading these endpoints | Not started. |
| Writes by business | Not built. Creating and editing stay on each location's existing endpoints. |
Database side of this work: Multilocation Database Ownership.
Shape
Every endpoint has the same address form and the same answer form.
GET /business/:businessEntityId/<resource>. The business comes first. The word "multilocation" is in no path.locationEntityIds, a comma-separated list. Left out, the answer covers every location the user may read.page and limit (1 to 200, default 20). A page past the end returns the last page.items, totalCount, totalPages, currentPage, and locationEntityIds: the locations the answer was read over.Each item carries its location. A definition shared by several locations (a service, a coupon) is one item with a locations list. Something that happened at one place (a sale, an appointment) is one item with a single location. Records are never merged.
Who sees what
A location is included only when the user could open that location's own list for the same records. The check uses the same permission the per-location page uses, so the business view shows nothing the location pages would hide.
| Case | Result |
|---|---|
| User has the permission at two of three locations | The answer covers those two. |
| User asks for a location they cannot read | 403. The request is refused, not silently narrowed. |
| User has the permission at no location of the business | 403. |
| A row belongs to another business | Never returned. |
| A row has no owner recorded yet | Returned when it sits at a location in the answer. |
Per-row rules carry over. Client contact details are hidden row by row using the user's permissions at that client's own location. A chart restricted to certain jobs stays hidden from staff who do not hold that job at the chart's location.
Endpoints
All paths start with /business/:businessEntityId.
Catalogue
| Path | Lists | Filters |
|---|---|---|
/services | Active services | search, categoryId |
/services/categories | Categories, each with its service count | search |
/bundles | Bundles | search |
/addons | Add-ons | search, isActive |
/addon-groups | Add-on groups | search, isActive |
/resources | Rooms and equipment | isActive |
/resource-groups | Resource groups | isActive |
/products | Retail and back-bar products | filterSearch, filterIsActive, filterIntent |
Packages, memberships and gift cards
| Path | Lists | Filters |
|---|---|---|
/packages | Package and membership templates | status (active or archived), billingType, search |
/packages/sold | Packages clients have bought | status |
/memberships/sold | Memberships clients hold | status |
/gift-cards | Issued gift cards | status, source, search, dateFrom, dateTo, recipientClientId |
/gift-card-designs | Gift card designs | ownership |
Clients and team
| Path | Lists | Filters |
|---|---|---|
/clients | Clients | searchKey, source |
/staff | Team members with their location assignments | includeArchived |
Sales and appointments
| Path | Lists | Filters |
|---|---|---|
/sales | Sales | status, search, dateFrom, dateTo |
/bookings | Appointments | status, search, startDate, endDate, providerId |
Promotions
| Path | Lists | Filters |
|---|---|---|
/offers | Offers | status, search |
/coupons | Coupons | status, search, origin, autoGenType |
/auto-coupon-configs | Automatic coupon rules | none |
Forms and charts
| Path | Lists | Filters |
|---|---|---|
/forms | Forms and charts | kind, search, status, managementView |
/form-tags | Form tags | none |
/dot-phrases | Dot phrases | none |
/form-submissions | Charts waiting for sign-off | formId, from, to, assignedToMe |
Verification
Run on 2026-10-05 against the preview VM (one business, two locations), signed in as the business owner.
Checked on every endpoint: the answer has exactly the five fields; no record repeats; no item exposes the owner column; every item names a location in the answer; narrowing to one location returns only that location's rows; a page past the end returns the last page; a location outside the business is refused; every filter is accepted; an unknown status is refused.
| Endpoint | Rows | Compared with |
|---|---|---|
| Services | 28 | A direct database count: 28. |
| Categories | 6 | Each category's service count equals the services filtered by that category. |
| Clients | 62 | The two per-location lists: 32 and 30. |
| Sales | 125 | The two per-location lists: 65 and 60. |
| Appointments | 124 | The two per-location lists: 64 and 60. Filtered to one staff member: 27, same as that location's list. |
| Team | 15 | 15 member rows. A member assigned to both locations is one item with two locations. |
| Products | 1 | 1 product row. |
| Bundles, coupons, forms, form tags, dot phrases, resources, resource groups | 0 | One test record created at a location through the existing API. It appeared in the business list and the location's list, at that location only, and left both after delete. |
| Add-ons, add-on groups | 2 | The same test. An archived record stays in both lists. |
| Packages | 0 | The same test. An archived template moves to the archived tab. |
| Offers | 0 | A draft created through the API is hidden from both the business list and the location's web list. See Open decisions. |
| Sold packages, sold memberships, gift cards, gift card designs, automatic coupon rules, charts waiting for sign-off | 0 | Nothing. The VM has no such rows. The requests succeed and return an empty page; no row content was compared. |
What is different from the per-location lists
- Rows are slimmer. Values worked out for one location (display price, staff coverage, stock on hand, a website status) are left out.
- Unknown filter values are refused with a 400. Several per-location lists ignore them or fail.
- Search treats
%and_as plain characters. - Every list has one fixed order, newest first.
- Offers that need ops review are not listed.
Remaining work
Rebase onto develop
The PR conflicts withdevelop and CI has not run on it. Web pages
Move each Your business page onto its endpoint. Hook names and cache keys are in the contract.Domain documentation
The forms pack is updated. Services, clients, staff, bookings, payments and inventory packs still need their entries before the PR can merge.Query plans
The preview database is small. Searches and counts across many locations have not been measured on real volumes.Writes by business
Creating a record for several locations at once still goes through each location's endpoint.Open decisions
| Decision | Why it matters |
|---|---|
| Show rows with no owner recorded? | Today they are shown. Hiding them empties a location's lists until its backfill has run. |
| History of a location that changed business | Sales and clients stamped with the previous business are left out of the new business's lists. Appointments carry no owner, so they are shown. The two disagree. |
| Permissions for bundles, gift cards, designs and products | They have none per location today. Any staff member assigned to a location reads its gift card codes. |
| A record shared to a second location | It appears in the business list for that location, but some per-location detail pages still look only at the record's first location and answer "not found". Nothing creates such a share yet. |
| Sales permission | Reading sales is gated by the permission to create a sale. A separate read permission does not exist. |
| What the Offers list should hold | The web offers list hides offers that go through ops review, and every offer created today is of that kind. The business list mirrors the web list, so it holds only older self-serve offers. |
| Tags and dot phrases for staff who only fill charts | Both lists need the manage permission, so those staff are refused. |
| Sign-off from the business queue | Sign-off stays per location, 50 items at a time. A selection across locations has to be split by the page. |
Each group's full list of open questions is in the contract, docs/specs/multilocation-business-apis-contract.md.