com.bookbaltic.services.handoff
Status: Draft. Version 2026-11-01. Part of BookBaltic Scheduling Extensions for USP / UCP Local Services.
Extends: dev.usp-protocol.services.catalog, dev.usp-protocol.services.availability
Schema: https://bookbaltic.com/schemas/usp/handoff.json
What it adds: A vendor-format browser link per start that opens the merchant's own booking page with service, start, party size and level preselected, no hold; target form is UCP permalink (#523) with booking/* field paths
3. Booking handoff for the read-only phase
Base UCP and USP already do: UCP dev.ucp.shopping.permalink (#523) is a browser-addressable intent: a GET navigation that initialises shopping state from URL inputs, never authorises an order, treats inputs as untrusted and keeps attribution (utm_*, attribution/*). It is defined for shopping line items (/buy/{item}:{quantity}). USP has service.url (service page) and continue_url only once a booking or checkout exists (5.2). Gap: no permalink fields for a service start: an agent cannot open the merchant's page with service, start, party size and level preselected.
Status: vendor format. The fields below (permalink_template on the service, permalink_url on a start) are what HEADSPA serves today and stay in this extension because they are live. They are not a conforming UCP permalink. In UCP permalink_url names the link to an existing order or booking (order.json, lodging booking_confirmation.json), which a pre-booking start link is not. The target form is UCP dev.ucp.shopping.permalink (#523) applied to services, proposed upstream: the business declares the permalink capability with config.endpoint, and the agent builds the link from the compact item path and booking/* field paths addressing the USP BookingContext (schemas/paid_bookings.json):
| Choice | Field path | Notes |
|---|---|---|
| Service | compact item {service_id}:1 | booking/service_id MAY be omitted; the business takes it from the single compact item. |
| Start from availability | booking/slot/id | Preferred when the agent has a slot. |
| Start without a slot id | booking/slot/start | RFC 3339 with offset, percent-encoded (+ as %2B); the business resolves it to a slot (slot.id is required in SlotReference). |
| Party size | booking/party_size | The compact quantity stays 1 per booking; party size is never read from it. |
| Attribution | attribution/* or non-UCP utm_* | Unchanged from permalink. |
Target form (HEADSPA target, not served yet). Profile entry:
"dev.ucp.shopping.permalink": [{ "version": "2026-08-25",
"spec": "https://ucp.dev/2026-08-25/specification/permalink",
"schema": "https://ucp.dev/2026-08-25/schemas/shopping/permalink.json",
"config": { "endpoint": "https://www.head-spa.lv/book" } }]
A start with the slot id returned by availability (preferred):
https://www.head-spa.lv/book/svc-aqua-head-spa:1?booking/slot/id=svc-aqua-head-spa%3A2026-12-05T17%3A00%3Apair&booking/party_size=2&attribution/utm_source=agent
The same start known only by its time:
https://www.head-spa.lv/book/svc-aqua-head-spa:1?booking/slot/start=2026-12-05T17%3A00%3A00%2B02%3A00&booking/party_size=2&attribution/utm_source=agent
Possible resolution:
HTTP/1.1 303 See Other
Location: https://www.head-spa.lv/?ritual=svc-aqua-head-spa&guests=2&date=2026-12-05&time=17%3A00&utm_source=agent
Cache-Control: no-store
Referrer-Policy: no-referrer
In the target form a pre-booking start link gets no permalink_url field. A business that supports USP bookings without dev.ucp.shopping.checkout MAY declare the permalink capability; for it the booking root keeps the USP BookingContext shape (paid_bookings.json#/$defs/BookingContext), the business maps booking/slot/id and booking/party_size to the Create Booking fields slot_id and party_size (USP 5.3.1), and resolves booking/slot/start to a slot itself. A stale start keeps the 303 and lands the guest on the nearest valid step. On alignment the vendor fields below, including permalink_url on a start, are dropped; until then agents use them.
Schema
{
"$id": "https://bookbaltic.com/schemas/usp/handoff.json",
"$defs": {
"ServiceExt": { "type": "object", "properties": {
"permalink_template": { "type": "string",
"description": "Vendor format. RFC 6570 level 1 template; variables: locale, service_id, level, party_size, date, time. Resolved under the UCP permalink input rules (GET, no order, no hold, inputs untrusted); target form is a UCP permalink with booking/* field paths." },
"continue_url_template": { "type": "string", "deprecated": true, "description": "Earlier name of permalink_template; same value." },
"locales": { "type": "array", "items": { "type": "string" } } } },
"SlotExt": { "type": "object", "properties": {
"permalink_url": { "type": "string", "format": "uri", "pattern": "^https://", "description": "Vendor field: a pre-booking link to this start. Not the UCP permalink_url, which names an existing order or booking." },
"continue_url": { "type": "string", "format": "uri", "deprecated": true, "description": "Earlier name of permalink_url. In UCP continue_url resumes an existing session, which this link is not." },
"holds_slot": { "const": false } } }
}
}
Rules
permalink_urlfollows the UCP permalink input rules: HTTPS on the merchant's own origin (same origin asservice.url), a GET navigation, inputs untrusted, never treated as authorisation to book or charge. It MUST NOT carry buyer personal data.- No hold on load. Resolving the link MUST NOT create a hold and MUST NOT apply a hold id from the URL (in the target form
booking/hold_id); it consumes and drops it. This is the only new requirement on top of permalink:booking/booking_idandbooking/booking_statusare response-only and already never set (permalink Query Processing). Browsers, prefetchers, link-preview bots and scanners load links without buyer intent. The slot is held or booked only after an explicit guest action on the destination page. - Unlike the cart-merge MUST the Shopping TC reverted (#968, reverting #789), whose worst case stays inside the loader's own session, a hold removes a shared slot from every other buyer, so a link preview or scanner would cause harm outside the loader's session.
- The page MUST tolerate a stale or invalid choice (taken time, past time, level the service lacks, solo on pair-only): it opens at the nearest valid step for that service with the remaining choices kept, never an error page.
- Agents SHOULD tell the guest the time is not reserved until they confirm. Agents MAY append their own attribution parameters (
utm_*orattribution/*, as in permalink); merchants keep them as permalink allows for attribution and never forward sensitive values. continue_url_templateandcontinue_urlare deprecated synonyms with the same values, kept so agents that read the first draft keep working. In UCPcontinue_urlmeans resuming an existing session; this link starts nothing.
Example (HEADSPA live format)
"com.bookbaltic.services.handoff": {
"permalink_template": "https://www.head-spa.lv/{locale}?ritual={service_id}&level=basic&guests={party_size}&date={date}&time={time}&hs_via=ai_profile",
"locales": ["lv", "en", "ru"] }
The template is live on each service of POST https://www.head-spa.lv/usp/v1/services/list; continue_url_template is sent alongside with the same value. A start in a point check answer (5.2) carries "permalink_url": "https://www.head-spa.lv/en?ritual=svc-aqua-head-spa&level=basic&guests=2&date=2026-11-05&time=11%3A30&hs_via=ai_check" and continue_url with the same value; hs_via is the merchant's own source tag. The /en prefix is redirected: the site answers 307 to / with the query kept (Cache-Control: public, max-age=0, must-revalidate, Referrer-Policy: strict-origin-when-cross-origin), then 200; /lv and /ru answer 200 directly. Agents follow the redirect as an ordinary navigation.
Without the extension: the agent links service.url, and the guest picks everything again.
Open questions: does the services permalink overlap with ESP (USP Section 9.5) as a "link" delegation mode?