Booking Lifecycle¶
Capability: dev.usp-protocol.services.bookings
The bookings capability defines the lifecycle of a service booking from creation through completion. For paid services, it also defines the actions array (including payment actions with payment_context) and the confirm-payment operation.
Single-Service Design
USP bookings are single-service by design. Each POST /bookings creates exactly one booking for one service at one time slot. Multi-service coordination (e.g., a haircut followed by a color treatment) is handled by the platform issuing separate bookings.
Booking Status Lifecycle¶
stateDiagram-v2
[*] --> pending
pending --> confirmed : auto-confirm or manual approve
pending --> requires_action : payment needed
pending --> canceled : timeout or cancel
requires_action --> confirmed : payment confirmed
requires_action --> canceled : timeout or cancel
confirmed --> in_progress : service begins
confirmed --> canceled : cancel
in_progress --> completed : service delivered
in_progress --> no_show : client absent
completed --> [*]
no_show --> [*]
canceled --> [*] Status Descriptions¶
| Status | Description |
|---|---|
pending | Booking created, awaiting confirmation. For auto mode, this is transient -- moves to confirmed immediately (or requires_action if payment is needed). For manual mode, remains until business confirms. |
requires_action | One or more actions in the actions array have status: pending. Inspect actions[] for required tasks (e.g., payment, waiver signing). |
confirmed | Booking is confirmed and service will proceed at the scheduled time. |
in_progress | Service is currently being delivered. Transitioned by the business. |
completed | Service has been delivered. Terminal state. |
no_show | Client did not attend within the grace period. Terminal state. Business MAY charge the no-show fee. |
canceled | Booking has been canceled. Can be reached from pending, requires_action, or confirmed. Terminal state. |
Permitted Transitions by Operation¶
The lifecycle diagram shows which statuses can follow which. It does not say which operation is legal from a given status, which is what a platform needs to know whether a call will be accepted. This table is normative.
Yes means the business MUST accept the call when all other preconditions hold. No means it MUST reject with invalid_transition.
| Current status | confirm | cancel | reschedule | confirm-payment | update |
|---|---|---|---|---|---|
pending | Yes (manual) | Yes | SHOULD | No | Yes |
requires_action | No | Yes | MAY | Yes | Yes |
confirmed | Yes (idempotent) | Yes | Yes | No | Yes |
in_progress | No | No | No | No | Yes |
completed | No | No | No | No | No |
no_show | No | No | No | No | No |
canceled | No | No | No | No | No |
The cells implementations diverge on:
reschedulefrompendingisSHOULD. A business that does not allow it MUST reject withinvalid_transitionrather than silently succeeding.reschedulefromrequires_actionisMAY, because an outstanding payment action may be priced against the original slot. A business that allows it MUST apply the reschedule price-change rules.confirmfrompendingisYesonly whenconfirmation_modeismanual. For anauto-mode booking the operation does not apply: a business MUST return the current booking unchanged when it is alreadyconfirmed(idempotent), and MUST reject withinvalid_transitionotherwise.confirmfromconfirmedis idempotent: it MUST return the current booking, so a retried confirmation is safe.updateon terminal statuses isNo. The booking is a historical record; contact and address edits MUST NOT be accepted oncompleted,no_show, orcanceledbookings.
Transitions to in_progress, completed, and no_show are business-initiated and have no platform-facing operation. They surface via GET /bookings/{booking_id} and the booking.updated webhook.
invalid_transition is a business outcome, not a protocol error
Cancelling an already-canceled booking is a well-formed, authorized request that the business understood. By the error family test, that makes it a business outcome: HTTP 200 OK with the code in messages[], not a 4xx Problem Details response.
Booking Schema¶
The booking object represents a scheduled service instance for a specific buyer at a specific time.
Deployment Mode Differences
In UCP-Native Mode, the payment field is not present on the booking (payment state is managed by the UCP checkout object). In Standalone Mode, the payment field is present and actions may contain payment action types.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique booking identifier, generated by the business. |
service_id | string | Yes | The booked service. |
service_name | string | Yes | Service display name, captured at booking time (snapshot). |
slot | object | Yes | {id, start, end, duration} -- the booked time slot. |
buyer | Buyer | Yes | {first_name, last_name, email, phone_number} -- the person making and paying for the booking. |
recipient | Buyer | No | {first_name, last_name, email, phone_number} -- the person receiving the service, when different from the buyer. |
party_size | integer | Yes | Total number of attendees. |
resources | Array[object] | No | {id, type, name} -- specific resources assigned to this booking. |
location | object | No | {id, name} -- specific location for this booking. |
status | string | Yes | Current booking status. See Status Lifecycle. |
confirmation_mode | string | Yes | auto or manual. |
payment | BookingPayment | Conditional | Payment state. MUST be present when requires_payment is true and payment_timing is at_booking or deposit_required. MUST be omitted when requires_payment is false. |
actions | Array[Action] | Conditional | Ordered array of pending tasks. MUST be present and non-empty when status is requires_action; MUST be absent or empty otherwise. |
notes | string | No | Buyer-provided special requests or notes. |
booking_url | string | No | Stable URL where the buyer can view and manage this booking. |
messages | Array[Message] | No | Soft messages from the business providing context about the booking state. |
dispute | Dispute | No | Present when a payment dispute has been opened. |
cancellation | object | No | {reason, canceled_by, fee, refund_amount, canceled_at} -- present when canceled. |
created_at | string | Yes | RFC 3339 timestamp of creation. |
updated_at | string | Yes | RFC 3339 timestamp of last modification. |
expires_at | string | No | RFC 3339 expiration deadline. A business that does not hold slot capacity for a pending or requires_action booking MAY omit expires_at. A business that holds slot capacity for an unconfirmed booking MUST include it. Advertising the field is a claim the business MUST honour (see Booking Expiry). |
Booking Expiry¶
expires_at on Booking is an advertised deadline. Omitting it is conformant when the business does not hold slot capacity for that unconfirmed booking. The numbered rules below apply only when expires_at is present. A business that advertises expires_at MUST honour it.
When a pending or requires_action booking that includes expires_at reaches that deadline without being resolved:
- The business MUST transition the booking to
status: canceled. - The business SHOULD send a
booking.canceledwebhook. - The expired booking MUST remain retrievable via
GET /bookings/{booking_id}withstatus: canceled. - The business MUST release any underlying slot hold.
Hold Alignment
For hold-backed bookings that advertise expires_at, the hold's expires_at SHOULD be aligned with or earlier than the booking's expires_at to prevent a race condition where the slot is released but the booking has not yet expired.
Operations¶
Create Booking -- POST /bookings¶
Creates a new booking for a service at a specific time slot. Resource selection is encoded in the slot_id -- the platform selects resources by choosing the appropriate slot at availability query time.
Request Fields:
| Field | Type | Required | Description |
|---|---|---|---|
service_id | string | Yes | The service to book. |
slot_id | string | Yes | The selected time slot. |
hold_id | string | No | Hold ID from a prior hold operation. |
buyer | object | Yes | Buyer contact information. |
recipient | object | No | The person receiving the service, when different from the buyer. |
party_size | integer | No | Number of participants. Default: 1. |
notes | string | No | Free-text notes for the business. |
post_payment_return_request | object | No | Platform's return instruction for redirect-based checkout flows. |
Idempotency
- With a hold: The business SHOULD treat
hold_idas a natural idempotency key. A secondPOST /bookingswith the samehold_idMUST return the existing booking. - Without a hold: Platforms SHOULD send an
Idempotency-Keyheader. The business MUST honor it.
{
"service_id": "svc_haircut_kids",
"slot_id": "slot_20260316_1600",
"hold_id": "hold_abc456",
"buyer": {
"first_name": "Alice",
"last_name": "Williams",
"email": "alice@example.com",
"phone_number": "+12125551234"
},
"recipient": {
"first_name": "Max",
"last_name": "Williams"
},
"party_size": 1,
"notes": "He is 7 years old"
}
{
"usp": {
"version": "2026-08-20",
"capabilities": {
"dev.usp-protocol.services.bookings": [
{ "version": "2026-08-20" }
]
}
},
"booking": {
"id": "bkg_456def",
"service_id": "svc_massage_001",
"service_name": "Deep Tissue Massage",
"slot": {
"id": "slot_20260316_1400",
"start": "2026-03-16T14:00:00-04:00",
"end": "2026-03-16T15:00:00-04:00",
"duration": "PT60M"
},
"buyer": {
"first_name": "Alice",
"last_name": "Williams",
"email": "alice@example.com",
"phone_number": "+12125551234"
},
"party_size": 1,
"resources": [
{ "id": "staff_jane", "type": "staff", "name": "Jane Smith" }
],
"status": "requires_action",
"confirmation_mode": "auto",
"payment": {
"status": "pending",
"timing": "at_booking",
"amount": 12000,
"currency": "USD",
"amount_due": 12000
},
"actions": [
{
"type": "payment",
"status": "pending",
"continue_url": "https://business.example.com/pay/bkg_456def",
"expires_at": "2026-03-16T13:10:00-04:00",
"message": {
"type": "info",
"code": "payment_required",
"content": "Payment of $120.00 is required to confirm this booking.",
"severity": "requires_buyer_input"
},
"payment_context": {
"amount_due": 12000,
"currency": "USD",
"description": "Deep Tissue Massage - Mar 16, 2:00 PM",
"line_items": [
{
"label": "Deep Tissue Massage (60 min)",
"amount": 12000,
"quantity": 1,
"item_id": "svc_massage_001"
}
],
"metadata": {
"booking_id": "bkg_456def",
"service_id": "svc_massage_001",
"service_type": "appointment",
"slot_start": "2026-03-16T14:00:00-04:00"
}
}
}
],
"created_at": "2026-03-14T22:05:00Z",
"updated_at": "2026-03-14T22:05:00Z",
"expires_at": "2026-03-16T13:10:00-04:00"
}
}
{
"usp": {
"version": "2026-08-20",
"capabilities": {
"dev.usp-protocol.services.bookings": [
{ "version": "2026-08-20" }
]
}
},
"booking": {
"id": "bkg_789ghi",
"service_id": "svc_yoga_free",
"service_name": "Community Yoga",
"slot": {
"id": "slot_20260318_1000",
"start": "2026-03-18T10:00:00-04:00",
"end": "2026-03-18T11:00:00-04:00",
"duration": "PT60M"
},
"buyer": {
"first_name": "Alice",
"last_name": "Williams",
"email": "alice@example.com"
},
"party_size": 1,
"status": "confirmed",
"confirmation_mode": "auto",
"created_at": "2026-03-14T22:05:00Z",
"updated_at": "2026-03-14T22:05:00Z"
}
}
Get Booking -- GET /bookings/{booking_id}¶
Returns the current state of a booking. Same structure as the booking object above.
Update Booking -- PUT /bookings/{booking_id}¶
Updates mutable fields on a booking. Only buyer, recipient, and notes are mutable after creation. Omitted fields are left unchanged (partial update semantics).
| Field | Type | Required | Description |
|---|---|---|---|
buyer | object | No | Updated buyer contact information. |
recipient | object | No | Updated recipient information. |
notes | string | No | Updated special requests or notes. |
{
"booking": {
"id": "bkg_456def",
"notes": "Please use hypoallergenic products",
"buyer": {
"first_name": "Alice",
"last_name": "Williams",
"email": "alice.new@example.com",
"phone_number": "+12125551234"
},
"updated_at": "2026-03-15T10:00:00Z"
}
}
Confirm Booking -- POST /bookings/{booking_id}/confirm¶
Business-initiated confirmation for bookings with confirmation_mode: manual. A platform or buyer agent MUST NOT call this operation, and a booking_scoped_credential MUST NOT authorize it. Authentication of the business caller is deployment-defined.
When confirmation_mode is manual and the booking is pending, the business MUST transition it to confirmed. When confirmation_mode is auto and the booking is already confirmed, the business MUST return the current booking unchanged (idempotent). When confirmation_mode is auto and the booking is not already confirmed (including a UCP-Native unpaid booking that remains pending), the business MUST reject with invalid_transition at HTTP 200 OK in messages[].
| Field | Type | Required | Description |
|---|---|---|---|
notes | string | No | Optional message from the business to the buyer. |
{
"booking": {
"id": "bkg_456def",
"status": "confirmed",
"confirmation_mode": "manual",
"updated_at": "2026-03-15T09:00:00Z"
}
}
Cancel Booking -- POST /bookings/{booking_id}/cancel¶
Cancels a booking. Eligible from pending, requires_action, or confirmed status. Cancellation fees are applied per the service's cancellation policy.
| Field | Type | Required | Description |
|---|---|---|---|
reason | string | No | Human-readable cancellation reason. |
canceled_by | string | No | Who initiated: buyer, business, or system. Default: buyer. |
{
"booking": {
"id": "bkg_456def",
"status": "canceled",
"cancellation": {
"reason": "Schedule conflict",
"canceled_by": "buyer",
"fee": 0,
"refund_amount": 12000,
"canceled_at": "2026-03-15T08:30:00Z"
},
"payment": {
"status": "refunded",
"timing": "at_booking",
"amount": 12000,
"currency": "USD",
"amount_due": 0,
"transaction_id": "txn_abc123"
},
"updated_at": "2026-03-15T08:30:00Z"
}
}
Reschedule Booking -- POST /bookings/{booking_id}/reschedule¶
Moves a booking to a different time slot. Rescheduling preserves the booking id -- this is not a cancel + rebook. The original slot is released and the new slot is occupied.
Eligible statuses: see Permitted Transitions by Operation. Rescheduling a canceled or terminal-state booking MUST be rejected with invalid_transition.
| Field | Type | Required | Description |
|---|---|---|---|
slot_id | string | Yes | The new slot to reschedule to. |
hold_id | string | No | Hold ID for the new slot, when business supports holds. |
Reschedule is atomic
Acquiring the new slot and releasing the original MUST be atomic: either the booking moves and the original is released, or the booking is left entirely unchanged. The original slot MUST NOT be released before the new one is secured, because a failure in between leaves the buyer with no booking at all, which is worse than a rejected reschedule.
When the new slot cannot be secured, the booking stays on its original slot and the business returns:
| Cause | Code |
|---|---|
| New slot is taken or not bookable | slot_unavailable |
hold_id for the new slot has lapsed | hold_expired |
New slot lacks capacity for party_size | capacity_exceeded |
| New slot violates the booking window | booking_window_violated |
| Rescheduling policy limit reached | reschedule_limit_reached |
| Booking status cannot be rescheduled | invalid_transition |
A failed reschedule MUST NOT release a still-valid hold.
Credential Expiry on Reschedule
A booking_scoped_credential is issued at creation and its expires_at is chosen against the slot the booking occupied then; rescheduling to a later slot does not move it. A business whose credential lifetime is derived from the slot SHOULD re-issue on the reschedule response, as a sibling of booking, whenever the existing credential would otherwise expire before the new slot. Section 5.6 already permits re-issuing to the same bound key, and the previous credential MAY be left to expire on its own. Without this the platform holds a booking it will stop being able to read, reschedule or cancel before the appointment occurs -- it has only a proof of possession, and credentials are issued on creation, not on demand.
Price Changes on Reschedule
When slot-level pricing differs between the original and new slot (e.g., rescheduling from off-peak to peak), the business SHOULD update payment.amount. If additional payment is required, the business MUST add a new payment-type action to actions[], transitioning the booking to requires_action.
Confirm Payment -- POST /bookings/{booking_id}/confirm-payment¶
The universal callback the platform calls after payment succeeds. This completes the payment action in the booking's actions array. If no pending actions remain, the booking transitions to confirmed.
{
"usp": {
"version": "2026-08-20",
"capabilities": {
"dev.usp-protocol.services.bookings": [
{ "version": "2026-08-20" }
]
}
},
"booking": {
"id": "bkg_456def",
"status": "confirmed",
"payment": {
"status": "paid",
"timing": "at_booking",
"amount": 12000,
"currency": "USD",
"amount_due": 0,
"transaction_id": "txn_abc123",
"order_reference": "ord_xyz789"
},
"updated_at": "2026-03-14T22:06:00Z"
}
}
payment_result Fields:
| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | paid (full amount) or deposit_paid (deposit amount). |
provider | string | No | Payment provider identifier (e.g., stripe, adyen). Informational. |
transaction_id | string | Yes | Transaction identifier for reconciliation and refund processing. |
amount_paid | integer | Yes | Amount paid in minor currency units. |
currency | string | Yes | ISO 4217 currency code. |
order_reference | string | No | External order identifier for cross-system reconciliation. |
Webhooks¶
Businesses SHOULD notify platforms of state changes via webhooks. Webhook payloads MUST be signed.
Booking Webhooks¶
| Event | Trigger |
|---|---|
booking.confirmed | Business confirms (manual mode) or payment completes |
booking.canceled | Business or system cancels the booking |
booking.rescheduled | Business reschedules the booking |
booking.reminder | Upcoming appointment reminder (e.g., 24 hours before) |
booking.completed | Service has been delivered |
booking.no_show | Client did not attend within the grace period |
booking.refund_issued | A full or partial refund has been issued |
booking.dispute_opened | A dispute or chargeback has been opened |
booking.dispute_resolved | A dispute has been resolved |
booking.service_started | Service delivery has begun |
booking.service_updated | Service details changed during delivery |
Webhook Payload Schema:
| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event type (e.g., booking.confirmed). |
event_id | string | Yes | Unique event identifier for idempotent processing. |
booking_id | string | Yes | The booking this event relates to. |
order_id | string | No | UCP order ID. SHOULD be included for UCP-Native Mode bookings. |
timestamp | string | Yes | RFC 3339 timestamp of when the event occurred. |
data | object | No | Full booking object. SHOULD be included for most events. MAY be omitted for reminder events. |
{
"event": "booking.confirmed",
"event_id": "evt_789abc",
"booking_id": "bkg_456def",
"order_id": "ord_ucp_001",
"timestamp": "2026-03-15T09:00:00Z",
"data": {
"id": "bkg_456def",
"service_id": "svc_massage_001",
"service_name": "Deep Tissue Massage",
"slot": {
"id": "slot_20260316_1400",
"start": "2026-03-16T14:00:00-04:00",
"end": "2026-03-16T15:00:00-04:00",
"duration": "PT60M"
},
"buyer": {
"first_name": "Alice",
"last_name": "Williams",
"email": "alice@example.com"
},
"party_size": 1,
"status": "confirmed",
"confirmation_mode": "manual",
"created_at": "2026-03-14T22:05:00Z",
"updated_at": "2026-03-15T09:00:00Z"
}
}
Catalog Change Webhooks¶
Businesses SHOULD notify platforms of catalog changes via webhooks. This provides a push-based complement to the pull-based service catalog feed.
| Event | Trigger |
|---|---|
service.created | A new service has been added to the catalog |
service.updated | Service details have changed |
service.deleted | A service has been permanently removed |
service.suspended | A service is temporarily unavailable |
| Field | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event type. |
event_id | string | Yes | Unique event identifier. |
service_id | string | Yes | The service this event relates to. |
subscription_id | string | Yes | The subscription that triggered this notification. |
timestamp | string | Yes | RFC 3339 timestamp. |
data | object | No | Full service object for created and updated events. |
Post-Booking Lifecycle¶
After a booking reaches a terminal state (completed, no_show, canceled), additional lifecycle events may occur.
Refund Tracking¶
When a refund is issued, the booking's payment object MUST be updated:
payment.status | Description |
|---|---|
refunded | Full refund issued. |
partially_refunded | Partial refund issued (e.g., cancellation fee withheld). |
The business MUST send a booking.refund_issued webhook when a refund is processed.
Dispute Resolution¶
When a payment dispute (chargeback) is opened, the business SHOULD update the booking with dispute information and send a booking.dispute_opened webhook.
Dispute Object:
| Field | Type | Required | Description |
|---|---|---|---|
status | string | Yes | opened, under_review, resolved_buyer, or resolved_business. |
reason | string | Yes | Machine-readable reason: service_not_provided, quality_issue, unauthorized, duplicate. |
opened_at | string | Yes | RFC 3339 timestamp. |
resolved_at | string | No | Present only when resolved. |
Payment Status During Disputes
Opening a dispute does NOT change payment.status -- the payment remains paid while the dispute is under review. payment.status MAY change to refunded or partially_refunded only if the dispute resolves in the buyer's favor.
Service Delivery Events¶
For complex services (e.g., multi-step healthcare, ongoing rentals), businesses MAY emit intermediate delivery events:
| Event | Trigger |
|---|---|
booking.service_started | Service delivery has begun (e.g., rental pickup, appointment check-in) |
booking.service_updated | Service details changed during delivery (e.g., extended rental, additional treatment) |
These events are informational and do not change the booking's primary status.