Service Catalog¶
Capability: dev.usp-protocol.services.catalog
The catalog enables platforms to discover what services a business offers -- types, pricing, policies, resources, and delivery channels.
Catalog Feed¶
Businesses SHOULD publish a service catalog feed for aggregators and indexing platforms. The feed enables incremental synchronization -- aggregators maintain a cursor and fetch only changed records since their last sync, rather than re-fetching the entire catalog.
Feed Endpoint: GET /services/feed
The feed returns a paginated, chronologically ordered list of service records, sorted by modified_at ascending. This design follows the Realtime Paged Data Exchange (RPDE) pattern used by OpenActive.
{
"usp": {
"version": "2026-08-20",
"capabilities": {
"dev.usp-protocol.services.catalog": [
{ "version": "2026-08-20" }
]
}
},
"items": [
{
"state": "updated",
"modified_at": "2026-03-10T09:15:00Z",
"data": {
"id": "svc_haircut_001",
"business_id": "biz_glamour_salon_nyc",
"name": "Women's Haircut & Style",
"type": "appointment",
"...": "full service object"
}
},
{
"state": "deleted",
"modified_at": "2026-03-10T10:00:00Z",
"data": {
"id": "svc_old_service_002",
"business_id": "biz_glamour_salon_nyc"
}
}
],
"pagination": {
"next_cursor": "crs_f7g8h9i0j1k2",
"has_more": true
},
"feed_meta": {
"feed_generated_at": "2026-03-10T10:05:00Z",
"total_services": 47,
"feed_status": "healthy"
}
}
Feed Fields¶
| Field | Type | Required | Description |
|---|---|---|---|
items[].state | string | Yes | updated (new or modified service) or deleted (service removed; aggregators MUST prune this from their index). |
items[].modified_at | string | Yes | RFC 3339 timestamp of when this record was last modified. Defines the feed's sort order. It is not the cursor. |
items[].data | object | Yes | Full service object for updated state; object containing only id for deleted state. |
pagination.next_cursor | string | Yes | Opaque cursor to pass as the cursor query parameter on the next request. Aggregators MUST NOT parse or construct it. |
pagination.has_more | boolean | Yes | Whether more records exist beyond this page. |
feed_meta.feed_generated_at | string | Yes | RFC 3339 timestamp of when this feed page was computed. |
feed_meta.total_services | integer | Yes | Total number of active (non-deleted) services in the business's catalog. |
feed_meta.feed_status | string | Yes | Health status: healthy, degraded, or rebuilding. |
Feed Cursor vs Pagination Cursor
The feed response field is named pagination.next_cursor rather than the generic cursor used by all other paginated USP operations. Only the name differs; the opacity contract is identical.
The feed orders items by (modified_at, id) ascending, using id as a deterministic tie-break so records sharing a timestamp cannot straddle a page boundary and be skipped or duplicated on resume. next_cursor is an opaque token encoding that position. Aggregators MUST pass it back verbatim and MUST NOT parse, construct, or substitute a timestamp for it.
Catalog Caching and Indexing¶
Service catalog data is relatively static -- services, pricing, and policies change infrequently compared to real-time availability. Platforms and aggregators SHOULD cache catalog data rather than querying it on every user interaction.
Recommended caching strategies:
- Merchant aggregators (e.g., Google Merchant Center): Index by consuming the catalog feed via incremental cursor-based synchronization. Synchronize at least once per hour for high-frequency businesses and once per day for low-frequency businesses.
- Web crawlers and structured data: Businesses SHOULD expose service catalog data as schema.org/Service structured data on their website.
- Platform-level caching: Cache catalog responses according to HTTP
Cache-Controlheaders. Refresh at intervals between 1 and 24 hours.
Real-time Operations
Availability and booking are real-time operations and MUST NOT be served from stale caches. See Availability Caching Strategy for the tiered caching approach.
Structured Data Mapping Guide¶
When exposing service catalog data as schema.org structured data:
| USP Field | schema.org Property | Notes |
|---|---|---|
name | schema:name | Direct mapping |
description | schema:description | Direct mapping |
type | schema:serviceType | Map USP vertical to a human-readable string |
pricing.amount | schema:offers.price | Convert from minor units to decimal (e.g., 7500 -> 75.00) |
pricing.currency | schema:offers.priceCurrency | Direct mapping (ISO 4217) |
channel.type: virtual | schema:availableChannel.serviceType | Set to OnlineOnly |
channel.type: at_business_location | schema:availableChannel.serviceLocation | Map to schema:Place with address |
channel.type: at_buyer_location | schema:areaServed | Map channel.service_area, if present. Do not publish the buyer's delivery_address |
locations[] | schema:areaServed / schema:serviceLocation | Map each location to a schema:Place |
availability_hint.next_available_date | schema:availabilityStarts | Approximate; use with schema:Offer |
media[].url (type=image) | schema:image | Direct mapping. Filter to type: "image" entries |
policies.cancellation | schema:cancellationPolicy | Map to a human-readable string |
duration.fixed | schema:duration | No direct schema.org equivalent for service duration |
capacity.max | schema:maximumAttendeeCapacity | For group and reservation types |
Service Schema¶
The service object represents a bookable offering from a business. Each service has a type (vertical), duration, pricing, policies, and optional resource requirements.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique service identifier, scoped to the business. The composite key (business_id, id) is globally unique. |
business_id | string | Yes | Identifier of the business that owns this service. Together with id, forms the globally unique composite key. |
provider | Provider | No | Inline business metadata for display without a separate profile fetch. |
name | string | Yes | Human-readable display name (e.g., "Women's Haircut & Style"). |
description | string | Description | No | Plain string or structured Description object with multiple format variants. |
type | string | Yes | Service vertical: appointment, group, reservation, rental, field_service, or vendor-defined. |
categories | Array[ServiceCategory] | No | Multi-taxonomy category labels. Each entry has required taxonomy plus optional id, name, parent_id, value, and primary. Simple case: one-element array with taxonomy: "merchant". See Category rules. |
duration | Duration | Yes | Duration configuration. See Duration below. |
pricing | Pricing | Yes | Pricing model and amounts. See Pricing below. |
locations | Array[Location] | No | Physical or virtual locations where the service is offered. |
resources | Array[ResourceRequirement] | No | Required staff, rooms, or equipment. See Resource Requirement. |
channel | object | Yes | Delivery channel for the service. See channel types below. |
policies | ServicePolicies | Yes | Booking, cancellation, rescheduling, and payment policies. See Service Policies. |
capacity | object | No | {min, max, waitlist} -- REQUIRED for group and reservation types. |
media | Array[Media] | No | Service media items (images, videos). |
rating | object | No | {value, scale_min, scale_max, count} -- aggregate service rating. |
status | string | No | active (default), suspended, or archived. |
handle | string | No | URL-friendly slug (e.g., womens-haircut-style). |
url | string | No | Canonical service page URL on the business's website. |
tags | Array[string] | No | Freeform tags for categorization and search. |
metadata | object | No | Business-defined custom data. Platforms SHOULD pass through opaquely. |
availability_hint | AvailabilityHint | No | Approximate availability summary for agent-assisted discovery. |
links | Array[Link] | No | Typed links to policy and information pages specific to this service. |
localized | LocalizedFields | No | Per-locale overrides for human-readable text fields. category_name overrides the primary categories[] entry's name. |
Category rules¶
JSON Schema: /$defs/ServiceCategory
Each categories[] entry carries required taxonomy and at least one of id, name, or value. External (non-merchant) taxonomies MUST carry value. Exactly one entry is primary: if exactly one has primary: true, that is the primary; else if no entry sets primary and exactly one has taxonomy: "merchant", that entry is the primary; else the first entry is the primary. Never more than one primary: true. The primary entry is the source for display, localization, and registry projection of ServiceSearchResult.category (pick order: primary name, else primary value, else primary id, else first entry value, else service type).
Catalog filters (category_id / categories) match the primary entry's id and MAY match any entry's id. Filter parameters remain flat ID strings.
"categories": [
{
"taxonomy": "merchant",
"id": "cat_haircut",
"name": "Haircut",
"parent_id": "cat_hair",
"value": "beauty > hair > haircut",
"primary": true
},
{
"taxonomy": "google_business_profile",
"value": "job_type_id:hair_styling"
}
]
Channel Types¶
channel.type | Description | Additional Fields |
|---|---|---|
at_business_location | Service is delivered at the business's physical location. The buyer travels there. | instructions: optional arrival instructions |
at_buyer_location | Service is delivered at a location the buyer specifies. The business travels there; the booking requires delivery_address. | instructions, service_area: optional description of the area served |
virtual | Service is delivered remotely via video/audio call. | virtual_provider: platform name (e.g., "Zoom"). instructions: join instructions |
phone | Service is delivered via phone call. | instructions: optional call-in details |
hybrid | Delivered via more than one of the above channels, at the buyer's choice. | virtual_provider, instructions, service_area |
Description Schema¶
The description field accepts either a plain string or a structured object:
| Field | Type | Required | Description |
|---|---|---|---|
plain | string | Yes | Plain text content. Always required as the universal fallback. |
markdown | string | No | Markdown-formatted content. |
html | string | No | HTML-formatted content. Platforms MUST sanitize before rendering. |
Format Preference
Platforms SHOULD prefer the richest format they can safely render (html > markdown > plain), falling back to plain for unsupported formats.
Provider Schema¶
The optional provider object carries inline business metadata so platforms can display the business name and policy links without a separate profile fetch.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Business display name (e.g., "Glamour Salon NYC"). |
url | string | No | Business website URL. |
links | Array[Link] | No | Typed links to policy and information pages. Well-known type values: privacy_policy, terms_of_service, refund_policy, cancellation_policy, faq. |
"provider": {
"name": "Glamour Salon NYC",
"url": "https://glamoursalon.nyc",
"links": [
{
"type": "cancellation_policy",
"url": "https://glamoursalon.nyc/policies/cancellation"
},
{
"type": "terms_of_service",
"url": "https://glamoursalon.nyc/terms",
"title": "Booking Terms"
}
]
}
Availability Hint¶
An optional snapshot of near-term availability for agent-assisted discovery and registry ranking. It carries a natural-language summary, and MAY add structured slot_bitmaps, so platforms can narrow availability query date ranges and registries can rank search results without treating the hint as a live slot feed.
| Field | Type | Required | Description |
|---|---|---|---|
summary | string | Yes | Natural-language description of near-term availability for AI agents. |
generated_at | string | Yes | RFC 3339 timestamp when the snapshot was produced. Freshness timestamp only; not bit 0 on any ruler. |
valid_until | string | No | RFC 3339 instant after generated_at when the snapshot stops being usable for availability ranking. |
next_available_date | string | Conditional | Service-local YYYY-MM-DD date of the earliest available start across all duration bitmaps. Required when any bitmap has a set bit; MUST be omitted when every bitmap is all-zero or when slot_bitmaps is absent. |
slot_bitmaps | Array[AvailabilitySlotBitmap] | No | Optional duration-specific availability rulers. A summary-only hint is valid; when present the array MUST be non-empty. See Structured slot bitmaps below. |
Each AvailabilitySlotBitmap entry:
| Field | Type | Required | Description |
|---|---|---|---|
duration | string | Yes | ISO 8601 duration of the booking this bitmap describes (for example PT60M). |
starts_at | string | Yes | RFC 3339 instant of bit 0 (first candidate start on this ruler, not hint generation time). |
start_interval | string | Yes | ISO 8601 duration between consecutive candidate starts. Independent of duration. |
slot_count | integer | Yes | Number of candidate starts on this ruler (1 through 4294967296). Every decoded index MUST be strictly less than slot_count. |
encoding | string | Yes | MUST be roaring32-portable-base64. |
bitmap | string | Yes | Standard Base64 of a 32-bit Roaring portable serialization. Set bits are candidate starts with at least one approximately bookable unit. |
Not a Substitute for Real-time Queries
The availability hint is an approximation. Platforms MUST NOT use it as a substitute for real-time availability queries. Registries MAY use structured bitmaps for bounded secondary ranking only (Discovery Registry - Availability ranking).
A producer that did not sample a structured availability grid MAY publish a summary-only hint, and MUST omit slot_bitmaps rather than publishing an empty array or a dummy all-zero ruler to mean unknown.
Structured slot bitmaps¶
Why a bitmap¶
The hint answers a discovery question, not a booking question: which candidate start times were approximately bookable when the catalog snapshot was made? summary answers it in prose. slot_bitmaps answers it as a finite set of integer indices on a time ruler, so a consumer can reason about it mechanically. One bit records one candidate start without repeating a timestamp or slot object.
USP uses the 32-bit Roaring portable format because availability can be sparse or dense, and Roaring stays compact in both cases while supporting set intersection, cardinality, and minimum directly. Standard Base64 is only the JSON transport wrapper around the portable Roaring bytes; consumers decode the bytes to an integer set before interpreting them.
A bit represents a candidate booking start for one duration. It is not an occupied minute, capacity count, live slot, or score.
availability_hint is optional on a service, and slot_bitmaps is optional within the hint. A producer that did not sample a structured availability grid MAY publish a summary-only hint, and MUST omit slot_bitmaps rather than publishing an empty array or a dummy all-zero ruler to mean unknown.
slot_bitmaps is an array because one service can have more than one bookable duration. Each entry is one duration-specific ruler with its own duration, time origin, tick spacing, length, encoding, and bitmap. A duration MUST appear at most once. Shared snapshot metadata (summary, generated_at, optional valid_until, and conditional next_available_date) lives on the hint; per-duration grid metadata lives on each entry.
generated_at MUST NOT be treated as bit 0. starts_at MAY be later than generated_at (for example, the hint is generated at 07:30 and the shop opens at 09:00).
The producer MAY publish valid_until as an RFC 3339 instant after generated_at. When valid_until is absent, a registry MAY apply a documented validity policy. While a hint is usable, its age MUST NOT continuously reduce its score.
If any duration bitmap contains a set bit, next_available_date MUST be present and MUST equal the service-local calendar date of the earliest available start across all duration entries. If every duration bitmap is all-zero, next_available_date MUST be omitted. An all-zero bitmap means sampled and known empty; an omitted hint or a summary-only hint means unknown.
Producers MUST NOT encode unknown values as 0, "", a zero-length blob, an empty array, or a dummy all-zero ruler. Consumers MUST map absent data to a neutral availability signal, never to maximum soonness, and malformed, unsupported, expired, or out-of-range bitmap data MUST NOT exclude the service from search results.
{
"id": "svc_back_massage_001",
"business_id": "biz_downtown_spa",
"name": "Back Massage",
"type": "appointment",
"duration": {
"range": {
"min": "PT60M",
"max": "PT90M",
"step": "PT30M"
}
},
"availability_hint": {
"summary": "Good availability this morning and late afternoon. Midday is mostly booked, and 90-minute sessions are limited to 9:00-9:30 and after 16:00.",
"generated_at": "2026-03-14T07:30:00-04:00",
"valid_until": "2026-03-14T19:30:00-04:00",
"next_available_date": "2026-03-14",
"slot_bitmaps": [
{
"duration": "PT60M",
"starts_at": "2026-03-14T09:00:00-04:00",
"start_interval": "PT30M",
"slot_count": 17,
"encoding": "roaring32-portable-base64",
"bitmap": "OjAAAAEAAAAAAAcAEAAAAAAAAQACAAYACgAOAA8AEAA="
},
{
"duration": "PT90M",
"starts_at": "2026-03-14T09:00:00-04:00",
"start_interval": "PT30M",
"slot_count": 16,
"encoding": "roaring32-portable-base64",
"bitmap": "OjAAAAEAAAAAAAMAEAAAAAAAAQAOAA8A"
}
]
}
}
The two slot_bitmaps entries share the same starts_at and start_interval in this example, but their 1-bits differ because a longer booking fits fewer starts. Consumers MUST read each duration ruler independently and MUST NOT derive one duration's bitmap from another's.
Roaring can serialize the same set with different container layouts. Consumers MUST compare decoded integer sets, not Base64 text. Cache keys MUST NOT assume byte-stable re-indexing.
What start_interval is¶
start_interval is the spacing of the grid of candidate start times that the bits sit on. It answers "how far apart are consecutive bits?", which is different from "how long is the booking?" (duration).
start_interval is per bitmap entry. Two entries on the same service MAY use different starts_at, start_interval, and slot_count values. Publishing it on the entry lets a consumer turn a bit index into a start instant without fetching other catalog fields.
start_interval MUST NOT be treated as a restatement of booking_window.slot_interval (Service Policies). That policy is the business's advertised generation interval for live slots. The ruler's tick spacing is the grid this snapshot used, and those two durations can differ. Consumers MUST decode each entry using that entry's start_interval and MUST NOT substitute booking_window.slot_interval.
| Field | Meaning | Example |
|---|---|---|
duration | How long the booking occupies | PT60M or PT90M |
start_interval | Gap between consecutive bits | PT30M |
When start_interval is smaller than duration, consecutive 1-bits describe overlapping windows. That is normal. A 90-minute booking starting at 09:00 and another starting at 09:30 are two legitimate options that cannot both be taken.
Both fields are required on every entry. The bits do not mean "occupied 30-minute blocks." They mean "a booking of this entry's duration can start at this tick."
How one bitmap maps onto time¶
Think of a ruler whose tick marks are candidate start times, not occupied minutes.
generated_at 07:30-04:00 (freshness only; not a slot start)
starts_at 09:00-04:00 bit 0
09:30-04:00 bit 1
10:00-04:00 bit 2
10:30-04:00 bit 3
11:00-04:00 bit 4
...
last tick bit (slot_count - 1)
Each 1-bit means: a booking of this entry's duration can start at that tick.
Each 0-bit means: no known availability for that start.
The mapping MUST be:
All arithmetic MUST use instants (RFC 3339), not local clock labels. Equal instants with different offsets MUST compare equal.
A 1-bit MUST mean at least one approximately bookable unit exists for that start and duration at hint generation time. It MUST NOT be interpreted as a resource count, remaining capacity, or a live availability guarantee. Platforms MUST NOT treat the bitmap as a substitute for real-time slot queries (Availability).
With duration PT60M and start_interval PT30M, bits 2 and 3 both being 1 means two overlapping candidate windows, 10:00-11:00 and 10:30-11:30.
09:00 09:30 10:00 10:30 11:00 11:30 12:00 12:30
1 0 1 1 0 0 1 0
|------60m------|
(taken)
|------60m------|
|------60m------|
(taken)
|------60m------|
slot_count is the length of the ruler. It is required because Roaring32 indices stop at 4294967295 and because an open buyer preference must stop at the end of represented data. Consumers MUST clip intent projection to [0, slot_count).
Duration¶
The duration object defines how long a service takes. Exactly one of fixed, range, or undetermined MUST be provided.
| Field | Type | Required | Description |
|---|---|---|---|
fixed | string | Conditional | ISO 8601 duration. REQUIRED if neither range nor undetermined is present. |
range | object | Conditional | {min, max, step} -- all ISO 8601 durations. Buyer selects a duration within this range. |
undetermined | boolean | Conditional | Set to true when the service has no meaningful duration to display. |
buffer_before | string | No | ISO 8601 duration. Non-bookable prep time before the service. |
buffer_after | string | No | ISO 8601 duration. Non-bookable cleanup time after the service. |
Pricing¶
The pricing object defines how a service is priced.
| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | The pricing model. See values below. |
amount | integer | Conditional | Price in minor currency units (e.g., 7500 = $75.00). REQUIRED for fixed, hourly, or per_person. |
currency | string | Yes | ISO 4217 currency code. REQUIRED even when model is free. |
price_range | object | No | {min, max} -- displayable price range in minor units. RECOMMENDED for variable, hourly, or per_person. |
deposit | object | No | {type, value, refundable} -- REQUIRED when payment_timing is deposit_required. |
Pricing Models¶
| Model | Description |
|---|---|
fixed | Single, fixed price regardless of duration or party size. |
hourly | Price per hour. Total = amount * duration_in_hours. |
per_person | Price per participant. Total = amount * party_size. |
variable | Price varies by time of day, demand, etc. Actual price returned on each slot in availability response. |
free | No charge. amount MUST NOT be present. requires_payment MUST be false. |
Service Policies¶
Machine-readable policies that enable agents to make informed decisions. These policies govern the booking lifecycle and MUST be enforced by the business.
| Field | Type | Required | Description |
|---|---|---|---|
cancellation | object | Yes | allowed, free_cancellation_until (ISO 8601 duration), late_cancellation_fee (minor units), no_cancellation_after (ISO 8601 duration). |
rescheduling | object | Yes | allowed, free_reschedule_until, max_reschedules, fee. |
no_show | object | No | fee (fixed) or fee_percentage (0-100), grace_period (ISO 8601 duration). |
booking_window | object | Yes | min_advance, max_advance, slot_interval -- all ISO 8601 durations. |
confirmation_mode | string | Yes | auto (confirmed immediately) or manual (requires business approval; the business SHOULD respond within 24 hours). If the booking advertises expires_at and the business does not confirm before that deadline, the booking transitions to canceled per Booking Expiry. |
requires_payment | boolean | Yes | Whether this service requires any payment. |
payment_timing | string | Conditional | REQUIRED when requires_payment is true. One of: at_booking, at_service, deposit_required. |
ServicePolicies covers booking lifecycle timing only. It does not declare minimum age, audience tiers, or merchant-mandated checkboxes. For mandatory acceptances, policy links, and eligibility enforcement, see UCP-Native merchant policy parity and service links.
Links¶
Typed links to policy and information pages. Platforms SHOULD surface these before the buyer confirms.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Well-known values include cancellation_policy, rescheduling_policy, terms_of_service, privacy_policy, waiver, faq. |
url | string | Yes | URL to the linked page. |
title | string | No | Optional display text. |
Service links[] complement provider-level links. They are hints for display; mandatory acceptance and waivers that require affirmative buyer action are enforced through UCP checkout escalation or booking requires_action, not through a separate acceptance field on the service object.
Resource Requirement¶
Defines what staff, rooms, or equipment are needed for a service, and whether the buyer can select a specific resource.
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | staff, room, equipment, or other. |
name | string | No | Human-readable label (e.g., "Stylist", "Treatment Room"). |
selectable | boolean | No | Whether the buyer can choose a specific resource. Default: false. |
options | Array[Resource] | No | {id, name, description, image_url} -- available resource instances. REQUIRED when selectable is true. |
Validation Rules¶
Payment and Pricing Constraint Matrix¶
requires_payment | payment_timing | pricing.model | pricing.amount | Legal? | Notes |
|---|---|---|---|---|---|
false | (absent) | free | (absent) | Yes | Free service |
false | (absent) | fixed | (any) | No | If no payment required, model MUST be free |
true | at_booking | free | (any) | No | Cannot require payment for a free-priced service |
true | at_booking | fixed | (required) | Yes | Standard paid service |
true | at_booking | variable | (optional) | Yes | Variable pricing; actual price on each slot |
true | at_service | fixed | (required) | Yes | Price shown but collected in person |
true | deposit_required | fixed | (required) | Yes | Deposit collected upfront, remainder at service |
Summary Rules¶
- When
requires_paymentisfalse,pricing.modelMUST befreeandpayment_timingMUST NOT be present. - When
requires_paymentistrue,pricing.modelMUST NOT befree. - When
payment_timingisdeposit_required, thepricing.depositobject MUST be present. - When
pricing.modelisfree,pricing.amountMUST NOT be present. - When
pricing.modelisfixed,hourly, orper_person,pricing.amountMUST be present and greater than zero. - Exactly one of
duration.fixed,duration.range, orduration.undeterminedMUST be present. - When
duration.undeterminedistrue,pricing.modelMUST NOT behourly.
Operations¶
List Services -- POST /services/list¶
Returns a filtered, paginated list of services from the business catalog. Designed for interactive use by platforms and AI agents.
Filters:
| Field | Type | Description |
|---|---|---|
type | string | Service vertical (e.g., appointment, group). |
category_id | string | Single category ID to filter by. Matches the primary categories[] entry's id, and MAY match any entry's id. |
categories | Array[string] | Category IDs to filter by (OR logic). Same match rule as category_id. |
location_id | string | Location ID for multi-location businesses. |
price | object | {min, max} in minor currency units. |
All specified filters combine with AND logic. Within categories, values combine with OR logic.
Context (optional):
| Field | Type | Description |
|---|---|---|
address_country | string | Buyer's country (ISO 3166-1 alpha-2). |
coordinates | object | {latitude, longitude} for proximity-based ranking. |
language | string | Preferred language (IETF BCP 47). |
currency | string | Preferred currency (ISO 4217). |
intent | string | Free-text description of the buyer's intent. |
{
"query": "deep tissue massage",
"filters": {
"type": "appointment",
"categories": ["wellness", "spa"],
"price": { "min": 5000, "max": 15000 }
},
"context": {
"address_country": "US",
"coordinates": { "latitude": 37.4419, "longitude": -122.1430 },
"language": "en",
"currency": "USD",
"intent": "looking for a relaxing post-workout massage"
},
"pagination": { "limit": 10, "cursor": null }
}
{
"usp": {
"version": "2026-08-20",
"capabilities": {
"dev.usp-protocol.services.catalog": [
{ "version": "2026-08-20" }
]
}
},
"services": [
{
"id": "svc_haircut_001",
"business_id": "biz_glamour_salon_nyc",
"name": "Women's Haircut & Style",
"type": "appointment",
"duration": { "fixed": "PT60M", "buffer_after": "PT15M" },
"pricing": { "model": "fixed", "amount": 7500, "currency": "USD" },
"channel": { "type": "at_business_location" },
"resources": [
{
"type": "staff",
"name": "Stylist",
"selectable": true,
"options": [
{ "id": "staff_jane", "name": "Jane Smith" },
{ "id": "staff_alex", "name": "Alex Johnson" }
]
}
],
"policies": {
"cancellation": {
"allowed": true,
"free_cancellation_until": "PT24H",
"late_cancellation_fee": 2500
},
"rescheduling": { "allowed": true, "free_reschedule_until": "PT24H", "max_reschedules": 2 },
"no_show": { "fee_percentage": 100, "grace_period": "PT15M" },
"booking_window": { "min_advance": "PT2H", "max_advance": "P60D", "slot_interval": "PT30M" },
"confirmation_mode": "auto",
"requires_payment": true,
"payment_timing": "at_service"
},
"availability_hint": {
"summary": "Good availability next week on Tuesday afternoon and Wednesday morning.",
"generated_at": "2026-03-11T08:00:00-04:00",
"valid_until": "2026-03-18T08:00:00-04:00",
"next_available_date": "2026-03-17",
"slot_bitmaps": [
{
"duration": "PT60M",
"starts_at": "2026-03-17T09:00:00-04:00",
"start_interval": "PT30M",
"slot_count": 17,
"encoding": "roaring32-portable-base64",
"bitmap": "OjAAAAEAAAAAAAcAEAAAAAAAAQACAAYACgAOAA8AEAA="
}
]
}
}
],
"pagination": { "cursor": null, "has_more": false }
}
Get Service -- GET /services/{service_id}¶
Returns the full service object for a single service.
{
"usp": {
"version": "2026-08-20",
"capabilities": {
"dev.usp-protocol.services.catalog": [
{ "version": "2026-08-20" }
]
}
},
"service": {
"id": "svc_haircut_001",
"business_id": "biz_glamour_salon_nyc",
"name": "Women's Haircut & Style",
"type": "appointment",
"description": "A full haircut and styling session with one of our experienced stylists. Includes consultation, shampoo, cut, and blow-dry.",
"duration": { "fixed": "PT60M", "buffer_after": "PT15M" },
"pricing": { "model": "fixed", "amount": 7500, "currency": "USD" },
"channel": { "type": "at_business_location" },
"policies": {
"cancellation": { "allowed": true, "free_cancellation_until": "PT24H", "late_cancellation_fee": 2500 },
"rescheduling": { "allowed": true, "free_reschedule_until": "PT24H", "max_reschedules": 2 },
"no_show": { "fee_percentage": 100, "grace_period": "PT15M" },
"booking_window": { "min_advance": "PT2H", "max_advance": "P60D", "slot_interval": "PT30M" },
"confirmation_mode": "auto",
"requires_payment": true,
"payment_timing": "at_service"
}
}
}
Lookup Services -- POST /services/lookup¶
Returns full service objects for a batch of service IDs in a single request. Designed for hydrating multiple service references at once.
Batch Limits
Businesses MUST accept requests with at least 50 IDs. If the request exceeds the business's limit, return 422 Unprocessable Entity. Duplicate IDs are silently ignored. The response array is unordered.
{
"usp": {
"version": "2026-08-20",
"capabilities": {
"dev.usp-protocol.services.catalog": [
{ "version": "2026-08-20" }
]
}
},
"services": [
{ "id": "svc_haircut_001", "business_id": "biz_glamour_salon_nyc", "name": "Women's Haircut & Style", "type": "appointment" },
{ "id": "svc_massage_002", "business_id": "biz_glamour_salon_nyc", "name": "Deep Tissue Massage", "type": "appointment" }
],
"messages": [
{
"type": "warning",
"code": "service_not_found",
"content": "Service ID 'svc_nonexistent_999' was not found.",
"path": "$.ids[2]"
}
]
}
Feed Subscriptions -- POST /services/feed/subscriptions¶
Platforms and aggregators MAY register for push-based catalog change notifications.
| Operation | Method | Path | Description |
|---|---|---|---|
| Create Subscription | POST | /services/feed/subscriptions | Register for catalog change notifications |
| Get Subscription | GET | /services/feed/subscriptions/{subscription_id} | Get subscription status |
| Pause Subscription | POST | /services/feed/subscriptions/{subscription_id}/pause | Temporarily stop receiving events |
| Resume Subscription | POST | /services/feed/subscriptions/{subscription_id}/resume | Resume receiving events |
| Cancel Subscription | DELETE | /services/feed/subscriptions/{subscription_id} | Permanently cancel the subscription |
Conformance Requirements¶
A conforming implementation of dev.usp-protocol.services.catalog MUST:
- Implement
POST /services/listreturning a paginated list with theuspenvelope,servicesarray, andpaginationobject. - Implement
GET /services/{service_id}returning a single service with theuspenvelope. - Implement
POST /services/lookupaccepting at least 50 IDs with partial-success semantics. - Include all required fields on each
Serviceobject:id,business_id,name,type,duration,pricing,channel,policies. - Conform to the validation rules for
requires_payment,payment_timing, andpricing.modelcombinations. - Ignore unrecognized
query,context, andfiltersfields without returning an error (forward compatibility). - Use opaque cursors for pagination across all catalog endpoints.
A conforming implementation SHOULD:
- Implement
GET /services/feedfor incremental catalog synchronization. - Populate
provider,rating,availability_hint, andprice_rangewhen data is available.