bookbaltic

com.bookbaltic.services.intake

Status: Draft. Version 2026-11-03. Part of BookBaltic Scheduling Extensions for USP / UCP Local Services.

Extends: dev.usp-protocol.services.booking_questions, dev.usp-protocol.services.bookings

Schema: https://bookbaltic.com/schemas/usp/intake.json

What it adds: What USP 11.4 Booking Questions defers: a form per guest of a party, and an agent draft of non-sensitive answers that only the guest confirms through their own link


6. Pre-visit intake per guest

Base USP already does: Section 11.4 Booking Questions (dev.usp-protocol.services.booking_questions, added 2026-10-31) covers pre-visit forms: Service.booking_question_sets[] with purpose: "intake", completion_timing: "before_confirmation" | "before_service", frequency, validity, subject: "buyer" | "recipient", due_at, items with sensitive and collection: "business_surface"; the booking carries booking_question_state[] (status, continue_url for pending business-surface work); the buyer-input Action requires the buyer, not the agent, to supply each answer; platforms MUST never infer an answer from memory without buyer confirmation (11.4.8, 11.4.11 platform rule 4). This extension does not redefine any of that.

Gaps (USP 11.4.13 defers them): (i) "Group forms per attendee": a pair needs two independent forms, one per guest, and USP models one subject per set (subject: buyer | recipient; "USP has no general participants concept"), a group service whose form needs answers from more than the one modelled recipient "stays withheld" (vector bq-flow-group-participants-deferred); an appointment for two (party_size: 2) has the same gap and no rule at all; (ii) there is no state "prepared by the agent, not yet confirmed by the guest", and no way for the second guest to confirm their own form.

Related: reuses the guest pool of UCP Lodging (#780). Input to #922 Phase 3 "intake forms", proposed upstream as a revision of USP 11.4. Checked: #953 and #954 cover lodging guest and booker identity, not forms; PR #915 (declared_fields) has no per-person or sensitive-data rules; discussion #1022 asks for an explicit end-customer consent statement in the booking request (who, which data, which purpose). None defines per-guest intake or an agent draft.

Schema

{
  "$id": "https://bookbaltic.com/schemas/usp/intake.json",
  "requires": { "protocol": { "min": "2026-08-20" },
    "capabilities": { "dev.usp-protocol.services.booking_questions": { "min": "2026-08-20" },
                      "dev.usp-protocol.services.bookings": { "min": "2026-08-20" } } },
  "$defs": {
    "BookingQuestionSetExt": { "type": "object", "description": "Added to a USP BookingQuestionSet.", "properties": {
      "per_guest": { "type": "boolean", "default": false,
        "description": "Each guest of the party answers this set on their own. Valid only with purpose intake. Every item with sensitive true MUST use collection business_surface." },
      "agent_draft": { "enum": ["none", "non_sensitive"], "default": "none",
        "description": "non_sensitive: an agent may draft in_band items whose sensitive is not true." } } },
    "PerGuestSetRule": { "description": "Applied to a USP BookingQuestionSet that carries this extension: with per_guest true, every item with sensitive true uses collection business_surface (collection defaults to in_band, so it must be present).",
      "if": { "required": ["com.bookbaltic.services.intake"], "properties": {
        "com.bookbaltic.services.intake": { "required": ["per_guest"], "properties": { "per_guest": { "const": true } } } } },
      "then": { "properties": { "items": { "items": {
        "if": { "required": ["sensitive"], "properties": { "sensitive": { "const": true } } },
        "then": { "required": ["collection"], "properties": { "collection": { "const": "business_surface" } } } } } } } },
    "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, as UCP Lodging guest_assignments.role" },
      "first_name": { "type": "string" } } },
    "GuestQuestionState": { "type": "object", "required": ["guest_id", "set_id", "revision", "status"], "properties": {
      "guest_id": { "type": "string", "description": "guests[].id of this booking" },
      "set_id": { "type": "string" },
      "revision": { "type": "string" },
      "status": { "enum": ["pending", "completed", "expired", "waived", "extended", "under_review", "rejected"],
        "description": "As USP BookingQuestionState.status" },
      "draft_awaiting_guest": { "type": "boolean", "description": "An agent draft exists and waits for this guest's confirmation; status stays pending." },
      "continue_url": { "type": "string", "format": "uri", "pattern": "^https://",
        "description": "This guest's own session link: unguessable, expiring, no personal data." },
      "due_at": { "type": "string", "format": "date-time" } } },
    "BookingExt": { "type": "object", "properties": {
      "guests": { "type": "array", "items": { "$ref": "#/$defs/Guest" }, "description": "Same shape as guests[] of com.bookbaltic.services.party (UCP Lodging id and first_name, role from guest_assignments.role); omitted when that extension is negotiated, which then carries the pool" },
      "guest_question_state": { "type": "array", "items": { "$ref": "#/$defs/GuestQuestionState" },
        "description": "One entry per guest for each per_guest set. Never carries answers." } } },
    "DraftRequest": { "type": "object", "required": ["set_id", "revision", "answers"], "properties": {
      "set_id": { "type": "string" }, "revision": { "type": "string" },
      "answers": { "type": "object", "description": "Keyed by question id, as USP BookingQuestionSubmission.answers; only in_band items whose sensitive is not true." } } },
    "DraftResponse": { "type": "object", "required": ["status", "draft_awaiting_guest"], "additionalProperties": false, "properties": {
      "status": { "const": "pending" },
      "draft_awaiting_guest": { "const": true },
      "expires_at": { "type": "string", "format": "date-time" } } }
  }
}

Rules

First-draft names replaced to follow USP 11.4: form_id → set_id + revision; due: before_confirm | before_visit → completion_timing: before_confirmation | before_service; per → per_guest; agent_fillable → agent_draft; data_classes → USP sensitive per question; actions[] intake_form → guest_question_state[]. In 2026-11-03: agent_draft values contact_only and contact_and_yes_no → non_sensitive (yes/no health answers are sensitive and never drafted); expires_at on the guest entry → due_at, as USP; fhir_questionnaire_url removed (form formats other than the USP item model are out of scope).

Example (HEADSPA)

HEADSPA asks every guest, before the visit, 9 yes/no health questions and one free-text health note, plus contacts, preferences and consents; the guest fills it on the HEADSPA site (one signed link per guest, www.head-spa.lv/anketa/...) and staff read it in its health module. The extension is not yet declared in the HEADSPA profile; the example shows the intended shape.

Service fragment (USP set with this extension):

"booking_question_sets": [{ "id": "headspa-intake", "revision": "v6",
  "purpose": "intake", "completion_timing": "before_service", "frequency": "every_booking", "subject": "recipient",
  "items": [ "... 9 yes/no health questions and a health note, all sensitive: true, collection: business_surface; contacts, preferences, consents ..." ],
  "com.bookbaltic.services.intake": { "per_guest": true, "agent_draft": "non_sensitive" } }]

Draft request for the second guest (answers keyed by question id):

{ "set_id": "headspa-intake", "revision": "v6",
  "answers": { "language": "en", "preferences": "medium pressure, quiet room" } }

Response, no values echoed:

{ "status": "pending", "draft_awaiting_guest": true, "expires_at": "2026-11-07T12:00:00+02:00" }

Booking for two, after the primary guest completed the form and the agent drafted the partner's preferences:

"com.bookbaltic.services.intake": {
  "guests": [ { "id": "g1", "role": "primary" }, { "id": "g2", "role": "accompanying", "first_name": "Marta" } ],
  "guest_question_state": [
    { "guest_id": "g1", "set_id": "headspa-intake", "revision": "v6", "status": "completed" },
    { "guest_id": "g2", "set_id": "headspa-intake", "revision": "v6", "status": "pending", "draft_awaiting_guest": true,
      "continue_url": "https://business.example.com/forms/sess_Kp9w...", "due_at": "2026-11-07T12:00:00+02:00" } ] }

HEADSPA links today are per guest and signed, not yet expiring. The set-level booking_question_state stays pending until g2 confirms.

Without the extension: USP 11.4 collects one form for the one recipient; the second guest of a pair has no form of their own (11.4.13 fails closed), and the agent can only hand over the business-surface link.

Honest risks: the merchant cannot verify what the agent platform retains, which is why sensitive answers stay off the agent path; an agent can mis-record a drafted choice, so the confirm page shows each answer and the venue check stays; one-tap confirmation can become blind consent, so consent is a separate explicit step.

Open questions: should per-guest sets move into USP 11.4 once a participants concept exists, sharing UCP Lodging's guests[], or a shared model if #922 defines one?