com.bookbaltic.services.party
Status: Draft. Version 2026-11-03. Part of BookBaltic Scheduling Extensions for USP / UCP Local Services.
Extends: dev.usp-protocol.services.catalog, dev.usp-protocol.services.availability, dev.usp-protocol.services.bookings
Schema: https://bookbaltic.com/schemas/usp/party.json
What it adds: Same-start multi-guest bookings (couples), one resource of a type per guest, per-guest level and price, party-only services, guest names from UCP Lodging
2. Pair / couples booking
Base USP already does: party_size, per_person pricing, capacity {min,max}, availability filtered by party_size. Gaps: USP says a slot "carries at most one resource of each type", so two couches or two therapists for one start cannot be expressed; party_size on appointment is "typically 1" with no per-guest detail; no per-guest level; no co-location ("side by side in one room").
Schema
{
"$id": "https://bookbaltic.com/schemas/usp/party.json",
"$defs": {
"ServiceExt": { "type": "object", "properties": {
"allowed_party_sizes": { "type": "array", "items": { "type": "integer", "minimum": 1 },
"description": "Exact sizes accepted, e.g. [1,2] or [2]. Narrower than capacity {min,max}." },
"party_pricing": { "enum": ["per_guest", "per_party"],
"description": "per_guest: sum of each guest's level price. per_party: base amount for the party." },
"levels": { "type": "array", "items": { "$ref": "#/$defs/Level" } },
"co_location": { "enum": ["same_room_side_by_side", "same_room", "none"] } } },
"Level": { "type": "object", "required": ["id", "name", "price"], "properties": {
"id": { "type": "string" }, "name": { "type": "string" },
"price": { "type": "integer", "description": "Per guest, minor units, service currency" },
"duration": { "type": "string", "description": "ISO 8601" } } },
"SlotExt": { "type": "object", "properties": {
"party_size": { "type": "integer" },
"guest_assignments": { "type": "array", "description": "As UCP Lodging guest_assignments, per guest instead of per room: which resources each guest gets on this slot.",
"items": { "type": "object", "required": ["guest_id", "resources"], "properties": {
"guest_id": { "type": "string", "description": "Matches guests[].id in the booking request" },
"role": { "type": "string", "description": "primary or accompanying, as UCP Lodging; unknown values read as accompanying" },
"resources": { "type": "array", "items": { "type": "object",
"required": ["id", "type"], "properties": { "id": {"type":"string"}, "type": {"type":"string"}, "name": {"type":"string"} } } } } } } } },
"BookingRequestExt": { "type": "object", "properties": {
"guests": { "type": "array", "description": "Root guest pool, as UCP Lodging guests[]; plus a level per guest.", "items": { "$ref": "#/$defs/Guest" } } } },
"Guest": { "type": "object", "required": ["id"], "properties": {
"id": { "type": "string", "description": "Platform-generated, session-scoped, as UCP Lodging guests[].id" },
"role": { "type": "string", "description": "primary or accompanying. Added for services: UCP Lodging has role on guest_assignments only. Unknown values read as accompanying" },
"first_name": { "type": "string" },
"level_id": { "type": "string", "description": "levels[].id of the service" } } }
}
}
Rules
- A party booking is one booking, one slot, one hold (
spots=party_size), one payment. All guests start together; cancelling cancels the party. allowed_party_sizes: [2]marks a pair-only service; the merchant MUST also set basecapacity {min: 2, max: 2}so a base client reads the same rule.allowed_party_sizesstays withincapacity {min, max}.- With
per_guest, the total MUST equal the sum of the guests' level prices; withper_partyit is the base amount. Basepricing.model: "per_person"with the lowest level price stays correct for a party without levels. - Availability for each party size is its own read: a start absent for 2 may exist for 1 (different booking windows, two couches needed). Agents MUST NOT derive party availability from solo availability.
- Base
slot.resourceskeeps at most one resource per type (the shared room); per-guest couches and staff go inguest_assignments. Everyguest_assignments[].guest_idmatches aguests[].idof the booking request. - With
co_locationother thannone, the merchant MUST return a slot only if all guests fit it; slot length is the longest guest's level duration. An agent promises "side by side" only when the merchant declaresco_location. rolefollows UCP Lodging: a merchant MUST treat an unknown value asaccompanying. Guest 2 needs at most a first name; email and phone stay onbuyer.- Seasonal services: outside the season the merchant returns no slots and MAY set
status: "suspended"; the season itself is published via extension 4.
Not in this extension:
- Per-guest add-ons. If Local Services needs them, we follow the #960 shape (
applies_to: "guest",selected[].guest_ids) rather than define our own;add_on_idsandper_guest_optionsof the first draft are removed. - The service-level resource needs. That is the discussion #1022 draft (
bookable_resources,requires[] {resource_type, quantity, customer_selectable},capacity_pool). For a party we propose one addition to it, to settle with its author: a per-requirement scope, so{ "resource_type": "couch", "quantity": 1, "scope": "guest" }means one couch per guest and{ "resource_type": "room", "quantity": 1, "scope": "booking" }one shared room. - Group classes and capacity pools (USP
groupslots, discussion #1022capacity_pool), one guest with two services at the same time (discussion #1022mode: concurrent), guest identity beyond a first name (lodging #953, #1033). Day entry without a fixed start: we would bring that to #479 rather than define our own.
Example (HEADSPA)
Catalog fragment, live at GET https://www.head-spa.lv/usp/v1/services/svc-ogonhi:
{ "id": "svc-ogonhi", "name": "Ogonhi", "type": "appointment",
"duration": { "fixed": "PT180M" },
"pricing": { "model": "per_person", "amount": 9000, "currency": "EUR" },
"capacity": { "min": 2, "max": 2 },
"com.bookbaltic.services.party": { "allowed_party_sizes": [2], "party_pricing": "per_guest",
"co_location": "same_room_side_by_side",
"levels": [{ "id": "basic", "name": "Basic", "price": 9000, "duration": "PT180M" }] } }
svc-aqua-head-spa is live with allowed_party_sizes: [1, 2] and three levels per guest: 6500, 8000, 9500 with PT90M, PT105M, PT120M. Pair starts in the point check carry slot ids ending in :pair.
Slot (specified, not served; illustrative ids):
{ "id": "svc-aqua-head-spa:2026-11-07T14:00:pair",
"resources": [ { "id": "room-1", "type": "room" } ],
"com.bookbaltic.services.party": { "party_size": 2, "guest_assignments": [
{ "guest_id": "g1", "role": "primary", "resources": [ { "id": "couch-a", "type": "couch" }, { "id": "staff-anna", "type": "staff", "name": "Anna" } ] },
{ "guest_id": "g2", "role": "accompanying", "resources": [ { "id": "couch-b", "type": "couch" }, { "id": "staff-elina", "type": "staff", "name": "Elina" } ] } ] } }
Booking request (specified, not served; Aqua Head Spa, two guests, different levels):
{ "service_id": "svc-aqua-head-spa", "slot_id": "svc-aqua-head-spa:2026-11-07T14:00:pair", "hold_id": "hold_7f2",
"party_size": 2, "buyer": { "first_name": "Laura", "email": "laura@example.com" },
"com.bookbaltic.services.party": { "guests": [
{ "id": "g1", "role": "primary", "level_id": "basic" },
{ "id": "g2", "role": "accompanying", "first_name": "Marta", "level_id": "premium" } ] } }
Total 6500 + 8000 = 14500 minor units (145 EUR). HEADSPA runs read-only (no bookings capability) and hands the guest to its own page (section 3).
Without the extension: the agent books party_size: 2 at the per-person Basic price (130 EUR for two Basics); it cannot mix levels, and pair-only is still enforced via capacity.min.
Alignment. Guest pool and roles follow UCP Lodging (#780): root guests[] (id, first_name), guest_assignments[].guest_id, role with primary and accompanying, so one agent model covers a hotel stay and a spa visit; earlier draft names participants, guest_index and allocations are replaced. Added for services: guest_assignments[].resources, guests[].role (Lodging has role on guest_assignments only) and guests[].level_id. The slot carries USP id (USP 4.1); the booking request keeps slot_id. party_size keeps the name used by USP and discussion #1022 and stays compatible with #317 max_party_size. What is new here: one resource of the same type per guest on one slot (USP 4.1 allows one per type), co_location, a level and price per guest, and party-only services (allowed_party_sizes). The resource side belongs to the discussion #1022 capacity draft, which we propose to write together with its author rather than in parallel; the namespace moves to whatever #922 settles on.
Open questions: should multi-resource slots be a change to core USP Section 4.1 instead of an extension (revisit only if a second implementer needs per-guest resources in the base slot)?
Evidence: who else needs multi-resource slots
Same-start bookings that need two resources of one type are common in spa and salon software, rare elsewhere as a single booking:
- Spa couples. Zenoti: a couples service is for two guests "taken simultaneously in the same room", usually two staff, and is stored as two appointments (help). Book4Time "Enhanced Couples Booking" places both guests in a designated couples room, fixed pair price or per-guest choice (release). Booker advertises couples sessions (page). A Square seller reports about 60% of its services are couples massages and books them as two services by hand (thread).
- Salon groups. Phorest online group booking assigns a service per guest and needs several staff (help); Fresha group appointments (help); Booksy groups 2-10, staff-side only (help).
- Restaurants. Table combinations are a merchant-side setting; the guest sees one party size (TableCheck).
- Group classes, courts and escape rooms book one resource with capacity; medical teams combine different types. No platform evidence found for these as same-type pairs.
What this shows: the need is real for wellness, but every platform implements it as a party on one shared resource (couples room) plus linked per-guest appointments inside the merchant. Google Actions Center has the same shape as USP: one staff_id, one room_id, party_size per slot (spec). No USP issue or PR and no #922 comment discusses multi-resource or couples slots; #922 Phase 3 lists "party-size rules" as vendor extensions. Weak points: no adoption numbers, Mindbody and Vagaro not confirmed from their docs.