Booking Lifecycle¶
Capability: dev.usp.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. |
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 time for pending and requires_action bookings. |
Booking Expiry¶
When a pending or requires_action booking reaches its expires_at deadline:
- 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 the underlying slot hold.
Hold Alignment
For hold-backed bookings, 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-02-09",
"capabilities": {
"dev.usp.services.bookings": [
{ "version": "2026-02-09" }
]
}
},
"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-02-09",
"capabilities": {
"dev.usp.services.bookings": [
{ "version": "2026-02-09" }
]
}
},
"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. Transitions from pending to confirmed. Calling this on an auto-mode booking that is already confirmed MUST return the current booking state (idempotent).
| 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: confirmed (MUST be supported). pending SHOULD be allowed. requires_action is at the business's discretion. Rescheduling a canceled or terminal-state booking MUST return a 409 error.
| 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. |
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-02-09",
"capabilities": {
"dev.usp.services.bookings": [
{ "version": "2026-02-09" }
]
}
},
"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.