OSBP v0.1.0 Specification
Author: Andy Volk · First published: June 15, 2026
Current Revision: July 8, 2026
Status: proof of concept
Scope: local stdio MCP, credential-free reference backend, one external platform adapter (Openings), and one create-booking flow
Compatibility: pre-1.0; breaking changes may occur
View in Markdown format
The Open Service Booking Protocol (OSBP) is a small, open protocol that lets an AI agent book a service appointment only inside a user-approved authorization called a BookingMandate. The agent can search, compare, and prepare freely; it can mutate only inside the mandate. OSBP authorizes the booking, not the payment.
The v0.1.0 proof model is a provider-bound appointment timeslot: usually one provider’s time, one schedule/location, one duration, and one booking policy, with inventory that must not be double-sold. OSBP models that narrow object directly for this release. The merchant’s booking platform remains the system of record; OSBP validates authority, translates tool calls, and audits what happened.
This document is the complete v0.1.0 contract: the participants, the mandate, the eight tools and their wire shapes, the result envelope and error codes, the enforcement rules, and the audit trail. The key words must, must not, should, and may are to be interpreted as described in RFC 2119: must is an absolute requirement, should a strong recommendation with rare justified exceptions, may a true option. Platform integrators should read Core Objects and the Adapter Contract first; agent builders should start with BookingMandate and the Tool Set.
v0.1.0 proves the smallest useful OSBP loop:
BookingMandate → lookup → policy readback → create → verification if required → status → receiptParticipants
Section titled “Participants”| Participant | Role |
|---|---|
| Customer | The human principal: approves the mandate and the final slot, and supplies the verification code when required. In v0.1.0 the authorizing user and the customer are usually the same person |
| AI agent | Converts the customer’s intent into a scoped BookingMandate and calls OSBP tools. OSBP is host-agnostic at the protocol boundary, and v0.1.0 ships the local stdio MCP path first. Hosts that can launch local MCP tools can fill this role without a host-specific OSBP API |
| OSBP server | Exposes the eight booking tools and records audit events. A conforming OSBP implementation validates mandate scope before mutation; in the reference implementation the adapter performs that validation before create |
| Platform adapter | Translates OSBP calls into the booking platform’s API, and platform responses into OSBP results |
| Booking platform | Merchant system of record for services, availability, and appointments |
In v0.1.0 the OSBP server and the adapter run as a single local process beside the agent; OSBP provides the protocol core and server, and a platform integrator implements the adapter. The booking platform stays the system of record: OSBP never holds booking truth of its own; it authorizes, translates, and audits.
Canonical Flow
Section titled “Canonical Flow”The happy path runs lookup, policy readback, explicit approval, then mutation:
Customer → AI agent → MCP tool call → OSBP server / adapter boundary (mandate validation before mutation, audit at tool boundary) → platform adapter → booking platform API → normalized OSBP result → receipt and audit response to the agentIn tool terms: service.describe → availability.find → policy.explain → booking.create validation preflight with no platform appointment create → explicit user approval of the returned summary → booking.create approved retry → booking.status. The agent must read the policy impact back to the user before any mutation, and must ask before booking.
Final confirmation is an adapter-side gate. The reference adapter uses requireApproval in @osbp/core; other v0.1.0 adapters must enforce equivalent behavior. A valid booking.create call without approval returns requires_user_confirmation before any platform appointment create call. This gate exists to force the resolved service, merchant-local time, price, and policy into a user-visible summary, so the concrete booking is understood and authorized by the user before the platform sees an appointment-create mutation. Provider and location scope are pinned by ids and should be shown with display names when the adapter can resolve them safely. The challenge-form problem message carries both a user-facing approval summary and a machine-directed retry instruction. That retry instruction must embed the token exactly as approval.token="<token>", so agents, audit redactors, and conformance checks can recognize it without parsing free prose. The agent must show only the approval summary to the user, get final confirmation, then retry booking.create with the same idempotency_key and approval.
Verification is agent-mediated. The server never sees a code except to pass it through:
booking.create → adapter requires final confirmation → requires_user_confirmation (retryable, no appointment created) → agent shows the approval summary and asks the customer to confirm → booking.create retried with the same idempotency_key plus approval → platform requires verification → requires_verification (retryable) → verification.send → agent asks the customer for the code out of band → booking.create retried with the same idempotency_key plus verification_codeProduct posture: v0.1.0 defines no cancellation tool, so every create is irreversible within the protocol; for an agent, a clarifying question is always cheaper than a mutation made in doubt.
Non-Goals
Section titled “Non-Goals”v0.1.0 does not include:
- merchant search or marketplace behavior
- a generic adapter framework
- slot holds
- modification, cancellation, or reconfirmation
- payment collection, payment authorization, deposits, refunds, or card handling
- production mandate signing
- public conformance certification
- concurrent multi-tenant hosting (one server instance serving multiple users or organizations at once)
- classes, group capacity, multi-attendee bookings, multi-resource reservations, route/dispatch visits, waitlists, or recurring series
Protocol Model
Section titled “Protocol Model”The concepts every tool call shares: the domain objects, the mandate that authorizes mutation, and the money and time conventions they obey.
Internationalization Strategy
Section titled “Internationalization Strategy”OSBP v0.1.0 is global-ready at the protocol boundary, not a server-side localization system. The wire carries canonical machine-readable values: ISO 4217 currency codes and minor units, IANA timezones, RFC 3339 instants, merchant-local wall-clock strings, ISO 8601 durations, ISO weekday numbers, E.164 phone numbers, RFC 5322/RFC 6531 email addresses, and ISO 3166-1 country codes. The agent or host localizes presentation for the user from those canonical values.
Server-generated prose in v0.1.0 is English and carries no language tag: Problem.message, approval summaries, Receipt.text, and policy prose such as cancellation_note are explanatory text, not locale-specific UI. Agents conversing with a user in another language should translate that prose faithfully while preserving amounts, merchant-local times, and the merchant-visible customer name exactly as returned; they must not carry the machine-directed retry instruction or approval token into user-facing text. This is an implemented strategy for scaling internationally without adding server-side translations to the proof of concept; it is not a broad compatibility or global production-support promise.
Core Objects
Section titled “Core Objects”- Organization: the merchant (a clinic, a studio, a workshop). Identified by
organization_id, the id the mandate is anchored to. - Schedule: a bookable calendar or platform-specific availability container. In the v0.1.0 proof topology, the
schedule_ida slot carries is the create-ready id and may also pin the location or provider, depending on the platform’s native model. - Provider: the person or staff resource whose time the slot binds.
- Service: the thing being booked, with duration, price, and payment requirements.
Service.optionsmay expose variants or add-ons for display and agent reasoning, but v0.1.0booking.createhas no option selector. An agent must not claim that an option or add-on was booked unless the selectedservice_iditself represents that exact bookable service. If the user asks for an option that cannot be represented in the create payload, the safe v0.1.0 behavior is to stop, ask whether they want the base service instead, or hand off. - Slot: one bookable appointment opening in the v0.1.0 topology: a service, a provider, a start time, and a price. The start is carried twice: an absolute RFC 3339 instant in
starts_atand the merchant-local wall-clock instarts_at_local. - Customer: the person the appointment is for, whether or not any payment is involved. Usually the same human as the user approving the mandate, but the roles are distinct: the user authorizes, the customer attends. Customer phone numbers use E.164 format (“+15555550100”); email addresses are plain RFC 5322 addresses. Both pass through to the platform, which is the enforcer of its own contact formats. Customer names are intentionally light:
display_nameis the human-readable name to show the user, while optionalgiven_nameandfamily_nameare available only for platforms that require separate fields. OSBP does not enforce a legal-name match or reject nicknames such as “Alex” for “Alexander”; it must not invent missing name parts either. If an adapter needs separate name fields and the input is ambiguous, it fails closed or asks for clarification. If a name is supplied, final confirmation should show the merchant-visible name so transposed or surprising fields can be caught before create. - Booking and Receipt: the created appointment and OSBP’s proof of creation. The platform’s appointment record stays authoritative.
The tool examples below show populated shapes. Full field definitions, optionality, and TypeScript types live in the repository’s schema document.
BookingMandate
Section titled “BookingMandate”The mandate is the user’s scoped authorization. In v0.1.0 it is unsigned JSON; the server still validates every field before any mutation.
{ "id": "mnd_2026_05_01_001", "expires_at": "2026-05-01T23:59:59-07:00", "allowed_actions": ["booking.create"], "organization_id": "org_01jw3z8x9k2m4n6p8r0s1t3v5w", "service_ids": ["service_123"], "provider_ids": ["provider_123"], "schedule_ids": ["schedule_123"], "earliest_start": "2026-05-01T09:00:00", "latest_end": "2026-05-14T18:00:00", "max_price": { "amount_minor": 10000, "currency": "USD" }, "allow_policy_fee": false, "max_extra_fee": { "amount_minor": 0, "currency": "USD" }}expires_atis an absolute instant and must carryZor an explicit UTC offset. Naked wall-clock strings are rejected: they would parse in the host’s local timezone and could expire hours early or late.earliest_startandlatest_endare wall-clock strings in the merchant’s local time, with no offset. They are compared againstSlot.starts_at_localandSlot.ends_at_local, which follow the same convention.service_ids,provider_ids, andschedule_idsare optional. A looser mandate authorizes a wider booking funnel; a stricter mandate pins one service, one provider, one schedule. Omit one of these fields to leave that dimension unconstrained; an empty array is a present but empty allow-list and allows nothing. The ids must constrain the actual create-ready ids used forbooking.create, not display labels.
Every money-bearing value is a Money object, never a bare integer: { amount_minor?, currency, type?, display? }. The money-bearing fields are Service.price, ServiceOption.price, Slot.price, Policy.deposit, BookingMandate.max_price, and BookingMandate.max_extra_fee.
amount_minor is an integer in the currency’s ISO 4217 minor unit. The minor-unit exponent varies by currency: USD and EUR use 2 places, JPY, CLP, and VND use 0, and KWD, JOD, and TND use 3. It is not always cents, so adapters scale by the currency’s exponent, not a fixed factor of 100. For any currency an adapter supports, the exponent must come from current ISO 4217 or CLDR currency data, or from a dated, reviewed snapshot of that data, rather than an unexplained local shortlist; a merchant currency whose exponent is unknown is not safe to onboard. currency is the ISO 4217 code and is required whenever a Money is present, so cross-currency comparisons fail closed instead of guessing. type defaults to fixed when an amount is present; the non-fixed values insurance_dependent, quote_required, and unknown mark a price with no fixed amount_minor known at read time. display is an optional locale-formatted string such as “$95.00”: a best-effort, non-canonical hint. Agents should reformat amount_minor plus currency in the end user’s locale rather than parse the display string, and implementations never parse it. Use amount_minor for arithmetic and mandate checks.
Adapters must normalize the platform’s native money representation into amount_minor: a platform that reports major units (for example 80 meaning $80.00) is scaled by the currency’s minor-unit exponent, while a platform whose amounts are already minor units is passed through unscaled. Misreading the native unit yields prices wrong by a factor of the currency’s exponent.
OSBP never converts currencies implicitly. If a selected service, slot, or policy is in a different currency than the mandate cap, the server fails closed with currency_mismatch and asks for a new user approval. A selected price without a fixed amount_minor fails closed with requires_user_confirmation.
Time Discipline
Section titled “Time Discipline”A concrete slot or booking time is carried two ways at once, so a reader gets both the canonical absolute moment and the value the merchant platform stores:
- Absolute instants (
starts_at,ends_atonSlotandBooking): an RFC 3339 instant carryingZor an explicit offset (for example “2026-05-05T20:00:00Z”). This is the canonical, cross-vertical form: FHIR, Google, Square, Cal.com, and Zocdoc all key concrete appointment times to instants. The adapter computes it from the merchant-local wall-clock plus the schedule’s IANA timezone. Compare instants only against other instants. - Merchant-local wall-clock, no offset (
starts_at_local,ends_at_localonSlotandBooking; mandateearliest_startandlatest_end; thedateandtimeofbooking.create): a string like “2026-05-05T13:00:00” with no offset, interpreted in the merchant’s timezone. That timezone is carried asSlot.schedule_timezone/Booking.schedule_timezoneusing IANA Time Zone Database names (for example “America/Los_Angeles”). Agents must use the IANA timezone, not the street address, for wall-clock reasoning. These are the values that round-trip intobooking.create. - Other absolute instants: mandate
expires_atand auditcreated_atare also RFC 3339 instants.
OSBP carries the absolute instant (the adapter computes it from merchant-local time plus the merchant’s IANA timezone) but does not reconcile the user’s own timezone with merchant-local time; reconciling the user’s local intent (“book me 2pm my time”) with the merchant’s wall-clock is the agent’s job before it ever populates date/time or earliest_start/latest_end. There is no user-timezone field in the protocol, and starts_at is the merchant’s instant, not the user’s.
The split is deliberate. The wall-clock fields (starts_at_local, ends_at_local, earliest_start, latest_end, date, time) are intentionally not RFC 3339. Implementations, including generated ones, must not “fix” them by adding Z or a UTC offset, and must not strip the offset off the instant fields; the server rejects offset-carrying wall-clock values as invalid_mandate, and the instant fields must carry an offset.
The merchant-local wall-clock is authoritative for the platform round-trip, so the adapter always emits it and derives the absolute instant from it. The reference adapter is wall-clock-native, so it converts merchant-local time to the instant with a small, DST-correct helper and leaves starts_at absent rather than guessing when the schedule timezone is unknown. A platform that is instant-native instead converts the other direction (instant to merchant wall-clock for date/time on booking.create); naive truncation (slicing the offset off a UTC string) silently books the UTC wall-clock hour, wrong by the merchant’s offset. An illustrative merchant-local-to-instant conversion:
// Merchant-local wall-clock (no offset) -> UTC RFC 3339 instant, via the schedule IANA tz.function wallClockToInstant(localISO: string, timeZone: string): string { const guessUTC = new Date(`${localISO}Z`).getTime(); const dtf = new Intl.DateTimeFormat("en-US", { timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit" }); const p = Object.fromEntries(dtf.formatToParts(new Date(guessUTC)).map((x) => [x.type, x.value])); const asTz = Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second); return new Date(guessUTC - (asTz - guessUTC)).toISOString();}Where operating hours appear (schedule_hours on slots and locations), day is an ISO 8601 weekday number, 1 = Monday through 7 = Sunday, not the JS getDay convention (0 = Sunday); the adapter converts the platform’s value. open and close are wall-clock “HH:MM” strings in the location’s timezone. A close value earlier than open means the period continues into the following calendar day, for example 20:00 to 02:00. Hours are typical display context only; availability.find is the authority for bookable time. Durations (Service.duration, Policy.cancellation_window, Policy.late_grace) are ISO 8601 duration strings such as “PT45M”, not bare integers.
Wire Format
Section titled “Wire Format”How OSBP speaks: the transport it rides, what the tools are named, and the envelope every result uses.
Transport
Section titled “Transport”OSBP rides the Model Context Protocol rather than defining its own transport. v0.1.0 binds the tool set to a local stdio MCP server; tool results are returned as compact JSON text plus MCP structured content. Remote transport and authentication are not part of v0.1.0.
Naming
Section titled “Naming”Protocol-level tool names are dotted:
service.describeavailability.findpolicy.explainverification.sendverification.verifybooking.createbooking.statushandoff.requestMCP tool names are flattened:
osbp_service_describeosbp_availability_findosbp_policy_explainosbp_verification_sendosbp_verification_verifyosbp_booking_createosbp_booking_statusosbp_handoff_requestResult Envelope
Section titled “Result Envelope”Every tool returns one envelope shape. Success:
{ "ok": true, "value": { }, "audit_event_id": "aud_..."}Failure:
{ "ok": false, "problem": { "code": "price_exceeds_mandate", "message": "Selected service or slot price exceeds BookingMandate.max_price", "retryable": false }, "audit_event_id": "aud_..."}osbp_runtime is optional diagnostics for agents and operators. It describes how this response was produced, not durable booking truth. In v0.1.0 the only defined value is idempotency: "local_cache_replay", used when an adapter returns a settled result from local OSBP idempotency state instead of calling the booking platform again:
{ "osbp_runtime": { "idempotency": "local_cache_replay", "message": "No platform create call was made; this result was replayed from local OSBP idempotency state." }}problem.code is the stable machine-readable contract. problem.message is explanatory English prose in v0.1.0, not a parsing target; branch on problem.code only.
The codes in use:
| Code | Meaning |
|---|---|
invalid_mandate | Mandate is missing required fields, expires_at lacks an explicit timezone, or a wall-clock field carries an offset |
mandate_expired | Mandate expires_at is in the past |
action_not_allowed | Tool is not in allowed_actions |
organization_not_allowed | Organization is outside mandate scope |
service_not_allowed | Service is outside mandate scope |
schedule_not_allowed | Schedule is outside mandate scope |
provider_unknown | Slot does not identify provider required by mandate scope |
provider_not_allowed | Provider is outside mandate scope |
slot_too_early | Slot starts before earliest_start |
slot_too_late | Slot ends after latest_end |
price_exceeds_mandate | Price exceeds max_price |
policy_fee_exceeds_mandate | Required fee exceeds max_extra_fee |
currency_mismatch | Selected price currency does not match the mandate cap currency; fail closed for new user approval |
requires_payment_handoff | Mandate does not allow a required payment or policy fee |
requires_consultation_handoff | Service requires a consultation/quote workflow that v0.1.0 cannot drive; route to handoff.request |
requires_user_confirmation | Either final confirmation is required, or a payment requirement or amount is unknown. The final-confirmation challenge is retryable and includes an approval token; the fail-closed validator form is not retryable and carries no token |
requires_verification | Platform requires customer verification before booking (retryable) |
verification_failed | The submitted verification code was rejected (wrong or expired); retry the send step and then retry booking.create with the new raw code |
slot_taken | The selected slot is no longer available (retryable) |
idempotency_conflict | Same idempotency_key reused with a different payload |
booking_not_found | No appointment matches the status query |
booking_status_requires_verification | Appointment history is gated behind a verification step that read tools cannot complete. Rely on the booking.create receipt or hand off |
rate_limited | Upstream returned HTTP 429. Back off and retry (retryable) |
internal_adapter_error | An adapter threw unexpectedly; the host caught it and recorded an audited Problem rather than leaking the throw |
Adapters may emit additional adapter-specific codes, for example missing_config or invalid_verification_purpose. The codes above are the protocol-meaningful set.
Tool Set
Section titled “Tool Set”service.describe
Section titled “service.describe”Returns the service facts the agent needs to explain a bookable service.
Input
{ "service_id": "service_123"}service_id is optional; the adapter may substitute its configured default service.
Output
{ "ok": true, "value": { "id": "service_123", "name": "Haircut", "duration": "PT60M", "price": { "amount_minor": 9500, "currency": "USD", "type": "fixed", "display": "$95.00" }, "requires_consultation": false }, "audit_event_id": "aud_..."}When requires_consultation is true, booking this service routes to a merchant-side approval queue instead of creating an appointment. Agents must surface this to the user before booking.create.
availability.find
Section titled “availability.find”Returns candidate slots for one service, schedule, and date.
Input
{ "service_id": "service_123", "schedule_id": "schedule_123", "date": "2026-05-05", "provider_id": "provider_123"}date is a wall-clock date in the merchant’s local time. service_id and schedule_id are optional; the adapter may substitute configured defaults so an agent can ask “what’s open tomorrow” without discovering ids first. provider_id is optional.
Output
{ "ok": true, "value": [ { "id": "schedule_123:provider_123:service_123:2026-05-05T13:00:00", "organization_id": "org_01jw3z8x9k2m4n6p8r0s1t3v5w", "schedule_id": "schedule_123", "schedule_name": "Main Studio", "schedule_timezone": "America/Los_Angeles", "provider_id": "provider_123", "provider_name": "Jordan Lee", "service_id": "service_123", "starts_at": "2026-05-05T20:00:00.000Z", "ends_at": "2026-05-05T21:00:00.000Z", "starts_at_local": "2026-05-05T13:00:00", "ends_at_local": "2026-05-05T14:00:00", "price": { "amount_minor": 9500, "currency": "USD", "type": "fixed", "display": "$95.00" } } ], "audit_event_id": "aud_..."}Each slot carries the create-ready organization_id, schedule_id, provider_id, and service_id, so an agent can build a booking.create call and its mandate scope directly from the selected slot. starts_at is the absolute instant (here 13:00 America/Los_Angeles = 20:00Z); starts_at_local is the merchant-local wall-clock and round-trips into booking.create date and time unchanged.
There is no slot-hold mechanism in v0.1.0. OSBP must never claim a slot is held before booking is confirmed.
policy.explain
Section titled “policy.explain”Returns payment and cancellation policy facts before booking.create.
Input
{ "service_id": "service_123"}Output
{ "ok": true, "value": { "service_id": "service_123", "cancellation_enabled": true, "cancellation_window": "PT6H", "cancellation_note": "Cancel at least 6 hours before the appointment.", "late_grace": "PT15M", "cancellation_fee": { "amount_minor": 2500, "currency": "USD", "type": "fixed", "display": "$25.00" }, "no_show_fee": { "amount_minor": 5000, "currency": "USD", "type": "fixed", "display": "$50.00" }, "payment_requirement": "none", "deposit": { "amount_minor": 0, "currency": "USD" }, "verification_method": "sms", "organization": { "id": "org_01jw3z8x9k2m4n6p8r0s1t3v5w", "name": "Bayview Hair Studio", "timezone": "America/Los_Angeles", "currency": "USD", "country": "US" } }, "audit_event_id": "aud_..."}payment_requirement is "none" | "deposit" | "full_prepay" | "unknown". If it is "deposit" or "full_prepay", v0.1.0 still does not collect payment. A booking may proceed only when the required amount is fixed and explicitly allowed by the mandate (allow_policy_fee: true); if the mandate also provides max_extra_fee, that cap must be same-currency and cover the amount. Otherwise booking.create returns requires_payment_handoff or another fail-closed Problem. If it is "unknown", the server fails closed with requires_user_confirmation.
cancellation_fee and no_show_fee are surfaced (each a Money object) when the platform exposes them, so an agent can show the user the conditional liability of a booking; even a free service can carry a no-show fee. They appear only when the platform reports a fee amount, and are omitted otherwise. Agents must include any surfaced cancellation or no-show fee in the final user-visible approval summary, and must not describe those fees as capped by the BookingMandate. OSBP v0.1.0 does not yet enforce or cap them through the mandate: that is roadmap, tied to payment composition.
The organization block surfaces the merchant’s canonical id, since agents construct the mandate’s organization_id from a read tool, and the timezone and currency the agent needs for wall-clock and price reasoning. country is an ISO 3166-1 alpha-2 code.
verification_method reports the method OSBP can identify for the platform flow. The v0.1.0 enum is sms, email, none, or unknown; future methods such as voice or messaging apps should be additive. Agents should not promise a more specific channel than OSBP reports.
verification.send
Section titled “verification.send”Sends a verification code after booking.create returns requires_verification.
Input
{ "customer": { "phone": "+15555550100", "email": "jane@example.com" }, "purpose": "booking:org_01jw3z8x9k2m4n6p8r0s1t3v5w"}purpose follows the convention booking:{organization_id}.
Output
{ "ok": true, "value": { "sent": true, "method": "sms", "purpose": "booking:org_01jw3z8x9k2m4n6p8r0s1t3v5w" }, "audit_event_id": "aud_..."}verification.verify
Section titled “verification.verify”Verifies a code for non-booking flows that require explicit pre-verification. In the v0.1.0 booking flow, the agent normally supplies the raw code to booking.create after requires_verification, and the adapter lets the platform verify and consume it during appointment creation.
Agents MUST NOT call this before booking.create when the adapter documents that pre-verification consumes the booking code, challenge, or session. Adapters that lack a safe independent verify primitive should reject this tool with an adapter-specific Problem rather than attempting to simulate it. If a platform has an independent pre-verification primitive that does not consume the booking attempt, the adapter may support it for non-booking flows and must document whether it applies to booking.
Input
{ "customer": { "phone": "+15555550100", "email": "jane@example.com" }, "purpose": "booking:org_01jw3z8x9k2m4n6p8r0s1t3v5w", "code": "123456"}Output
{ "ok": true, "value": { "verified": true, "method": "sms", "purpose": "booking:org_01jw3z8x9k2m4n6p8r0s1t3v5w" }, "audit_event_id": "aud_..."}The agent must ask the customer for the code out of band. OSBP must not invent, suppress, or bypass verification.
Verification codes are sent by the merchant platform and may arrive in the platform’s language and channel-specific wording. Agents should tell the user only the method OSBP reported and should not promise text-message wording, sender names, or language that OSBP did not receive.
booking.create
Section titled “booking.create”Creates the appointment after user approval and mandate validation. Every booking mutation must include a BookingMandate and an idempotency_key.
Input
{ "mandate": { "id": "mnd_2026_05_01_001", "expires_at": "2026-05-01T23:59:59-07:00", "allowed_actions": ["booking.create"], "organization_id": "org_01jw3z8x9k2m4n6p8r0s1t3v5w", "service_ids": ["service_123"], "provider_ids": ["provider_123"], "schedule_ids": ["schedule_123"], "earliest_start": "2026-05-01T09:00:00", "latest_end": "2026-05-14T18:00:00", "max_price": { "amount_minor": 10000, "currency": "USD" }, "allow_policy_fee": false, "max_extra_fee": { "amount_minor": 0, "currency": "USD" } }, "idempotency_key": "idem_2026_05_01_001", "service_id": "service_123", "schedule_id": "schedule_123", "provider_id": "provider_123", "date": "2026-05-05", "time": "13:00", "customer": { "id": "customer_123", "display_name": "Jane Customer", "given_name": "Jane", "family_name": "Customer", "phone": "+15555550100", "email": "jane@example.com" }, "approval": { "confirmed": true, "token": "approve_opaque_adapter_token" }, "verification_code": "123456"}date and time are wall-clock values in the merchant’s local time, taken from the selected slot’s starts_at_local.
The first call for a logical booking attempt omits approval. The adapter validates the mandate and slot, then the approval gate returns requires_user_confirmation with an exact booking summary and an adapter-issued approval.token, and does not call the platform create endpoint. The summary is user-facing prose, not a dump of protocol enum labels; for example, say “No upfront payment or deposit is required to book” instead of payment_requirement: none. The message then carries a machine-directed retry instruction containing the token, formatted exactly as approval.token="<token>"; the agent must not display that token to the user. After the user confirms the summary, retry with the same payload, same idempotency_key, and approval: { "confirmed": true, "token": "<returned token>" }. The approval token is bound to the exact pending payload, including the idempotency_key, and to the resolved summary the user saw. If resolved facts such as price or policy text change before the retry, the adapter re-challenges with a fresh summary instead of accepting the old token. A token expires after a short TTL; the reference gate uses ten minutes. A leaked token cannot authorize a different booking, a different payload, or a second create under a new idempotency key.
The same requires_user_confirmation code is also used by mandate validation when a booking cannot be safely bounded, such as unknown payment requirements or non-fixed amounts. That fail-closed form is terminal for v0.1.0: it is not retryable and carries no approval.token. Agents must not invent a token or retry it as a final-confirmation challenge; explain the blocking fact and route to handoff.request.
If the booking input includes a customer name, the approval summary should show the merchant-visible name that will be sent to the platform. This catches common agent mistakes, such as swapped given_name / family_name, without creating a new OSBP-level identity gate. If the platform accepts “Alex Smith”, OSBP should not fail the booking merely because the user’s formal name might be “Alexander Smith”; if the platform rejects the contact/name pair, the adapter maps that platform rejection to a Problem.
A failed approved attempt may still leave non-appointment side effects on the booking platform, such as a create-or-find customer record or a verification message, before the final appointment create succeeds or fails. The final-confirmation gate guarantees no platform appointment create happens before approval; it is not a promise that every failed attempt is invisible to the merchant system.
verification_code is supplied only on retry after requires_verification.
Output
{ "ok": true, "value": { "id": "receipt_idem_2026_05_01_001", "booking_id": "appointment_123", "text": "Booked Haircut on 2026-05-05 at 13:00. Appointment id: appointment_123." }, "audit_event_id": "aud_..."}If the platform requires customer verification after the approved retry, booking.create fails with requires_verification (retryable). The agent then calls verification.send, asks the user for the code, and retries booking.create with the same idempotency_key, the same approval, and the raw verification_code.
The agent generates a fresh idempotency_key for each logical booking attempt and reuses it only when retrying that same attempt. approval and verification_code are transient fields and are excluded from the local payload hash. A successful create is cached: retrying with the same key and payload returns the same receipt without contacting the platform again. The same key with a different payload fails with idempotency_conflict.
booking.status
Section titled “booking.status”Reads appointment state. The booking_id is the one returned in the create receipt. When the platform exposes a direct appointment read, adapters should use it for status answers that claim current platform state. A local record may be used as a documented fallback or receipt-snapshot path, including immediately after an OSBP-created booking when the adapter’s integration notes say the answer is cache-first; such a response must not pretend to be fresh platform state. When the platform’s only readback is gated behind a verification step a read flow cannot complete, the adapter fails closed with booking_status_requires_verification rather than guessing. customer is accepted so an adapter that can read history may match by contact details.
Input
{ "booking_id": "appointment_123", "customer": { "phone": "+15555550100" }}Output
{ "ok": true, "value": { "id": "appointment_123", "status": "booked", "service_id": "service_123", "schedule_id": "schedule_123", "provider_id": "provider_123", "starts_at": "2026-05-05T20:00:00.000Z", "starts_at_local": "2026-05-05T13:00:00", "schedule_timezone": "America/Los_Angeles" }, "audit_event_id": "aud_..."}handoff.request
Section titled “handoff.request”Fallback when the adapter cannot safely complete the request. Use when:
- payment or deposit collection is required before confirming
- the selected slot disappeared
- the request exceeds mandate scope
- verification fails, expires, or rate-limits
- the platform API returns an ambiguous state
Input
{ "reason": "requires_payment_handoff", "message": "This service requires a deposit. Please complete the booking with the merchant directly.", "mandate_id": "mnd_2026_05_01_001"}Output
{ "ok": true, "value": { "status": "requires_human", "reason": "requires_payment_handoff", "message": "This service requires a deposit. Please complete the booking with the merchant directly.", "mandate_id": "mnd_2026_05_01_001" }, "audit_event_id": "aud_..."}handoff.request has no side effect: nothing is sent to the merchant. It returns a structured requires_human result for the agent to relay to the user, and the call is audited like any other schema-valid tool call. Merchant-side delivery of handoffs is out of scope for v0.1.0. The freeform reason and message fields must not carry customer names, contact details, or addresses; use opaque ids and non-identifying status text.
Server Obligations
Section titled “Server Obligations”What a conforming OSBP server must do on every booking mutation, regardless of platform.
Mandate Enforcement
Section titled “Mandate Enforcement”Before booking.create, OSBP must verify, in order:
- the mandate has an id and a well-formed
expires_atwith an explicit timezone - the mandate is not expired
booking.createis included inallowed_actions- the organization, service, schedule, and provider are all inside scope
earliest_start,latest_end, and the slot’sstarts_at_local/ends_at_localare wall-clock strings without offsets, while the slot’sstarts_at/ends_atare absolute RFC 3339 instants (mixed conventions on the wall-clock fields are rejected asinvalid_mandate), and the slot start and end fall inside the mandate window- the price currency matches
max_price.currencyand the amount is at or belowmax_price.amount_minor; a currency mismatch fails closed withcurrency_mismatch - the service does not require a consultation/quote workflow (fails closed with
requires_consultation_handoff) - any required payment or policy fee is allowed by
allow_policy_fee, matches themax_extra_feecurrency, and is at or belowmax_extra_fee.amount_minor; unknown payment requirements fail closed withrequires_user_confirmation - final confirmation is present before any platform appointment create; the reference adapter enforces this through the shared
@osbp/coreapproval gate, which returns an approval token from a prior validated no-writebooking.createcall - the same
idempotency_keyreturns the same result if retried; the same key with a different payload fails withidempotency_conflict, while transientapprovalandverification_codefields do not create a new logical attempt
OSBP idempotency lives in the local adapter cache. The audit log must store the mandate id and enough mandate content to explain each decision.
Every schema-valid tool call that reaches the OSBP tool handler produces a local audit event carrying redacted input and result snapshots plus stable hashes for later correlation:
{ "id": "aud_...", "mandate_id": "mnd_2026_05_01_001", "tool_name": "booking.create", "created_at": "2026-05-05T20:00:00.000Z", "source": "osbp-local-mcp", "input": { }, "result": { }, "input_hash": "3f2a...", "result_hash": "9c41..."}input_hash and result_hash are hex SHA-256 digests of the redacted snapshots.
Audit is also the operator-observability channel for upstream platform drift. Each event MAY carry platform (the adapter’s static identity) and upstream (response metadata from the most recent upstream call observed during the tool call). upstream is absent when the tool answered locally, for example an idempotency-cache hit or handoff.request. The shapes:
interface PlatformIdentity { vendor: string // stable platform slug api_version: string // the exact native API pin the adapter sends or encodes}
interface UpstreamMeta { call: string // "METHOD /path"; query strings omitted, contact-bearing segments redacted status: number server?: string // Server header, verbatim; an infrastructure fingerprint, not a version request_id?: string // first response header whose name contains "request-id" api_version?: string // response-side API version echo when the platform sends one deprecation?: string // Deprecation header (RFC 9745), verbatim sunset?: string // Sunset header (RFC 8594), verbatim HTTP-date ratelimit?: Record<string, string> // Retry-After, RateLimit, RateLimit-Policy, X-RateLimit-*, keyed lowercase}These are operator-observability fields, never agent-facing booking output, and they carry no customer data by construction.
The server appends redacted JSONL audit events to .osbp/audit/events.jsonl by default; set OSBP_AUDIT_LOG_PATH to override. Audit events must redact structured customer contact and name fields, provider image URLs, verification codes, approval tokens, contact-shaped phone/email strings in free text, and known sensitive strings discovered from structured input. Freeform tool text cannot reliably identify arbitrary names or addresses that appear nowhere else in structured input, so agents must not put those details in handoff.request reason or message. Checked-in traces must use audit aliases rather than raw local audit records.
Security and Privacy Considerations
Section titled “Security and Privacy Considerations”OSBP’s threat model includes the agent itself. An agent can be over-eager, confused, or compromised, so booking-create authorization is enforced server-side against the user-approved mandate; the agent is never trusted to self-limit. v0.1.0 mandates are unsigned JSON, which is a stated limitation, not a design goal: production mandate signing is a non-goal for this version and a requirement for any version that drops the “proof of concept” label. Verification tools are side-effecting but are not mandate-gated in v0.1.0; the limitation is called out below.
- Durable secrets stay server-side. Anything placed in a prompt, tool input, tool result,
Problem.message, or agent-facing transcript can be shown to the user, retained by the host, or copied into a debug session. Durable secrets such as platform API keys, OAuth bearer tokens, refresh tokens, merchant dashboard credentials, and platform-private override flags must stay in adapter/server config and must never be routed through the agent. - Attempt-scoped capabilities are tolerated, not trusted. Approval tokens and verification codes may pass through the agent only to complete the v0.1.0 booking loop. They must be scoped as narrowly as the platform allows, one-time-use where possible, short-lived, redacted from audit events and published traces, and insufficient by themselves to bypass mandate validation. Where a platform cannot provide one of those properties, for example a verification code scoped to a merchant rather than one booking attempt, the adapter documentation must say so.
- Correlation identifiers are not durable secrets. Mandate ids, idempotency keys, booking ids, receipt ids, and audit event ids exist so the user, adapter, and operator can correlate one attempt. They may appear in receipts, audit events, and agent-visible output. Their sensitivity inherits the platform’s readback and challenge semantics: where a platform offers unauthenticated by-id readback or keys a verification purpose to an appointment id, treat booking ids as sensitive in published traces and logs even though they are not platform credentials.
- Verification is sacred. OSBP must not invent, suppress, or bypass customer verification. Raw codes pass through to the platform and are never persisted; the audit log records that verification happened and by which method, never the code.
- Audit events are redacted. Structured customer contact and name fields, provider image URLs, verification codes, approval tokens, contact-shaped phone/email strings in free text, and known sensitive strings discovered from structured input are redacted before writing. Freeform handoff text must not carry customer names, contact details, or addresses that are not also present in structured fields. Checked-in traces must use audit aliases rather than raw local records.
- Fail closed. Unknown payment requirements, unknown fee amounts, and ambiguous platform states refuse or hand off rather than proceed. A mutation that cannot be explained to the user does not happen.
- No platform overrides. OSBP must never set platform-private flags that skip availability, verification, or payment checks, even when the platform exposes them.
- Caller identity is self-declared in v0.1.0. When OSBP calls the booking platform it identifies itself with self-declared HTTP headers only: a
User-Agent(for exampleOSBP/0.1.0 reference-backend/0.1.0 node/24) and anx-osbp-sourcemarker. This is not authentication, and a platform must not treat either as proof of origin. v0.1.0 propagates no verifiable agent or end-user identity to the platform, and the mandate is validated structurally, not cryptographically. Verifiable request signing (for example HTTP Message Signatures, RFC 9421) and OAuth resource-server authorization for a remote MCP server are roadmap items, not v0.1.0 guarantees. - No circumventing platform fairness controls. Beyond not overriding checks, OSBP must never bypass, disable, or evade a platform’s rate limits, queueing, per-user or per-account booking limits, waitlists, or anti-bot controls. OSBP traffic is subject to the same allocation, rate-limit, and safety controls the platform applies to native users. In v0.1.0 this is a conduct requirement, not an enforced one, since caller identity is self-declared: it states the conformance bar that verifiable identity later makes checkable.
- Payment authority is a separate layer. OSBP authorizes the booking, not the payment. A
BookingMandateis designed to compose alongside payment-mandate protocols; when a booking requires payment, OSBP hands off instead of handling the charge.
Known Limitations (Proof of Concept)
Section titled “Known Limitations (Proof of Concept)”v0.1.0 proves the authorization loop, not a production trust surface. These limitations are deliberate, and naming them is part of the contract: an integrator must not mistake the proof of concept for a hardened system. The incentive risks below are real even though nothing is exploiting them yet, and several are exactly the parts an adversarial agent or a careless integration would lean on.
- Narrow appointment topology. v0.1.0 models a single-attendee, single-provider appointment slot with no exposed capacity, room/equipment allocation, route stop, recurring series, waitlist position, or class roster. An adapter for a platform whose real booking object depends on one of those dimensions must fail closed or hand off rather than flattening the missing dimension into a misleading v0.1.0 slot.
- Idempotency is local and ephemeral. Deduplication of
booking.createlives in an in-process cache backed by a local file, keyed byidempotency_keyand payload-hashed. It does not survive a multi-instance deployment, a cross-session replay, or an adapter restart with a cleared store. A production deployment needs durable, platform-enforced idempotency; until then a buggy or adversarial caller can produce duplicate bookings the local cache cannot catch. - Final confirmation is adapter-side. The shared
@osbp/coreapproval gate (requireApproval, used by the reference adapter) requires a short-lived approval token before the platform appointment-create endpoint is called, which prevents a single accidentalbooking.createcall from booking immediately. It cannot prove the human saw or approved the summary, because the token still flows through the agent. The user-facing summary and the machine-directed retry instruction also share oneProblem.message, so v0.1.0 relies on agent guidance to show only the summary; audit logs and published traces redact tokens, but a live chat can still expose them if an agent pastes the whole message. Because the token flows through the agent and a mandate is reusable untilexpires_at, a compromised agent can self-approve repeated creates within the mandate’s scope. v0.1.0 has no per-mandate booking count cap. A production deployment needs a host/server confirmation primitive outside the agent’s unilateral control. - Verification-code pass-through is a platform accommodation, not a recommended pattern. The reference booking flow has the agent relay a raw SMS or email code through
booking.create. OSBP never persists the code, and the audit log records only that verification happened and by which method. But routing a raw human-delivered code through the agent at all is a concession to a platform’s current API, not a design to copy. Some platforms scope codes to a merchant and customer rather than to one booking attempt, so a leaked code remains useful for the platform TTL. Out-of-band or signed verification, where the agent never handles the raw code, is the intended direction. - Merchant-supplied text is untrusted display data. Service names, provider names, organization names, cancellation notes, and receipt text may originate in a merchant-controlled booking platform and flow into the agent context. v0.1.0 does not authenticate, sanitize, fence, or label those strings as untrusted content. Agents and hosts must treat them as display data, not instructions; a production host should add stronger tool-result labeling or content isolation.
verification.sendis not mandate-gated. The tool can dispatch a real SMS or email to a caller-supplied contact and is bounded only by platform rate-limit and safety controls in v0.1.0. The protocol does not yet bind the send destination to the customer of a pending gated booking attempt; a production version needs that binding or a mandate-scoped contact allowlist.- No slot holds: booking a scarce slot is a first-come race. OSBP holds no slot and the platform is the system of record, so the first completed
booking.createwins. When provider time is scarce a fast agent can win that race against a human on native UI, and it is sharper mid-booking when a deposit or verification step sits between selecting a slot and confirming it. v0.1.0 has no hold or lease, and no optimistic-concurrency token (availability_snapshot_id) to make a stale attempt fail cleanly. These are roadmap items (contention controls). - No fairness, fee-cap, or signed-receipt mechanisms. v0.1.0 has no allocation-policy disclosure, no conformance tiers, and no handoff-volume or unknown-rate metrics. An agent that repeatedly probes availability or fires handoffs is neither rate-limited nor measured by the protocol. Receipts and availability snapshots are unsigned, so neither the user nor the merchant gets a tamper-evident record of what was authorized or offered.
- Cancellation and no-show fees are surfaced but not capped by the mandate.
Policy.cancellation_feeandPolicy.no_show_feelet an agent show the conditional liability of a booking, but the mandate does not yet cap them or require reapproval when they change. A booking can carry a fee the user was shown without the mandate ever bounding it. Today the protection is explicit disclosure in final approval, not enforcement. provider_image_urlis a likeness and is not anonymized in tool results. The field is a URL to a provider’s display image, frequently a photograph of a named person. OSBP surfaces it on everySlotand does not redact or alias it in ordinary tool responses. The local audit logger redacts this key, and the checked-in trace redactor omits it by explicit allowlist. A raw availability response, or an integrator who widens a trace allowlist, exposes a named person’s image URL. Treat it as personal data when deciding what a trace, log, or downstream store may contain.
Implementing OSBP
Section titled “Implementing OSBP”The integrator path: the contract to implement, how to run the reference, and what a correct end-to-end trace contains.
Adapter Contract
Section titled “Adapter Contract”OSBP reaches a booking platform through one interface. Implementing OSBP for your platform means implementing these seven methods:
interface AdapterRuntimeDiagnostics { idempotency?: "local_cache_replay" message?: string}
type AdapterResult<T> = | { ok: true; value: T; osbp_runtime?: AdapterRuntimeDiagnostics } | { ok: false; problem: Problem; osbp_runtime?: AdapterRuntimeDiagnostics }
interface BookingAdapter { // Static platform identity and most-recent upstream response metadata, // recorded into audit events. See Platform Metadata and Versioning. readonly platform?: PlatformIdentity readonly upstreamMeta?: UpstreamMeta describeService(input: ServiceDescribeInput): Promise<AdapterResult<Service>> findAvailability(input: AvailabilityFindInput): Promise<AdapterResult<Slot[]>> explainPolicy(input: PolicyExplainInput): Promise<AdapterResult<Policy>> sendVerification(input: VerificationSendInput): Promise<AdapterResult<VerificationChallenge>> verifyCode(input: VerificationVerifyInput): Promise<AdapterResult<VerificationChallenge>> createBooking(input: BookingCreateInput): Promise<AdapterResult<Receipt>> getBooking(input: BookingStatusInput): Promise<AdapterResult<Booking>>}Adapters translate platform behavior into OSBP results and raise gaps explicitly. Canonical protocol policy (the mandate validator, the fail-closed rules) comes from the protocol core (validateMandate in @osbp/core) and is not the adapter’s to relax. Mandate validation is a server-side obligation that must run before the platform mutation; it may live in the host layer or inside the adapter. In the reference implementation the adapter performs it: createBooking fetches the service and policy, builds the Slot, and calls validateMandate before issuing any create call. The reference host does not re-validate, so a createBooking that assumes the host validates would ship no enforcement at all: call the validator yourself unless your host does. Booking idempotency lives in the adapter’s local cache.
Platform Metadata and Versioning
Section titled “Platform Metadata and Versioning”An adapter MUST declare its platform identity statically:
readonly platform: PlatformIdentity = { vendor: "osbp-reference-backend", api_version: "v1" }api_version is the exact native pin the adapter sends or encodes: a version request header value (for example Square-Version: 2026-05-21), a media-type version parameter, or the version segment of the base URL for path-versioned APIs. When the platform supports request-side pinning, the adapter MUST send the pin on every request and MUST NOT rely on account or dashboard defaults, which can change outside the adapter’s control. When the platform exposes no version mechanism, the adapter SHOULD record the documented spec version (for example OpenAPI info.version), its source, and the retrieval date in a source comment; that is build-time provenance, not a runtime signal, and drift detection falls to the read-only smoke.
Contract-drift monitoring is adapter-owned and platform-native, not a universal OSBP protocol shape. An adapter SHOULD add a scheduled drift monitor only when the platform offers a stable, official, non-PII-bearing contract source to diff and a clear operator action when the diff changes. A public OpenAPI document, a dated version echo, a generated client, a path version, an evolvable-enum policy, and a closed SDK all imply different monitoring strategies. Do not require an OpenAPI baseline or watcher from adapters whose platforms cannot support one, and do not substitute scraping, dashboard automation, or private endpoints for an official contract source.
For each upstream response the adapter SHOULD capture, into the audit channel: the method and path (query strings omitted, contact-bearing path segments redacted), the HTTP status, the Server header, the first header whose name contains request-id, any response-side API version echo, the Deprecation header (RFC 9745), the Sunset header (RFC 8594), and Retry-After / RateLimit headers. The Server value is an infrastructure fingerprint, frequently the CDN or edge-proxy build rather than the platform application, and MUST NOT be presented as an application or API version.
If a response carries Deprecation or Sunset, the adapter MUST surface it loudly on the operator channel, at least once per endpoint per process, so an operator learns the upstream is retiring before it breaks. A lifecycle announcement is not a failure: the call succeeded, so it MUST NOT become a Problem and MUST NOT change agent-facing output.
The reference backend is an in-process, unauthenticated loopback server, which is unusual; most real platforms require an API key or OAuth bearer. Put platform credentials in the adapter config (from the environment, never hard-coded), attach them in the single request helper alongside the User-Agent, and keep them out of observability: captureUpstreamMeta records response headers only, and credentials MUST NOT appear in UpstreamMeta, audit events, or Problem messages.
A platform-neutral sketch of the pattern:
const platform = { vendor: "example", api_version: "2026-05-21" } as const;
async function upstreamFetch(method: string, path: string, init: RequestInit = {}): Promise<Response> { const response = await fetch(`${BASE_URL}${path}`, { ...init, method, headers: { ...init.headers, "user-agent": `OSBP/${OSBP_VERSION} ${platform.vendor}/${OSBP_VERSION} node/${process.versions.node.split('.')[0]}`, // Send the native pin exactly where the platform requires it. "example-version": platform.api_version } });
const meta = captureUpstreamMeta(`${method} ${path}`, response); recordForAudit({ platform, upstream: meta });
if (meta.deprecation || meta.sunset) { warnOperatorOncePerEndpoint(meta); }
return response;}The reference adapter in the repository implements this pattern; its internal captureUpstreamMeta helper is the worked capture function.
Getting Started
Section titled “Getting Started”- Reference implementation: TypeScript monorepo with the protocol core, the credential-free synthetic reference backend, the conformance kit, and an adapter starter.
- Run the demo:
npm run demobooks a synthetic appointment inside aBookingMandate, then shows the guardrail rejecting an over-cap booking, with no account or secrets. - Trace gallery: replayable credential-free reference traces, from user request through mandate, tool calls, guardrails, and receipt.
Demo Trace
Section titled “Demo Trace”A complete reference trace includes, in order:
- user request
- generated
BookingMandate service.describeavailability.findpolicy.explainbooking.createvalidation preflight withoutapproval- explicit user approval text using the returned summary
booking.createretry withapprovalverification.send(if required), followed by retry ofbooking.createwith the sameapprovalplusverification_codebooking.status- final receipt returned to the user
- local audit event ids
Repository · Discuss · Apache-2.0