bookbaltic

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

Not in this extension:

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:

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.