{
  "openapi": "3.1.0",
  "info": {
    "title": "Agent Salon SF API",
    "version": "1.3.0",
    "description": "San Francisco haircuts, structured for AI agents. Independent; not affiliated with any salon listed; booking happens on the salon's own system. All data carries provenance (status, source, observedAt). Unknown facts are null with status \"unknown\". No Google or Yelp content is published.\n\n1.3.0: additive decision contract GET /api/v1/options and MCP compare_options; selected/mandatory add-ons, explicit known subtotals and unresolved prices, consultation/deposit/cancellation/patch-test conditions, first-party hours with preserved conflicts, evidence and next actions. Existing endpoint fields/types are preserved. /openapi.json aliases this specification. Telemetry retains minimized normalized fields, not raw IP/search text/arguments.\n\n1.2.0: new depth value \"detailed\" (imported salon whose own website publishes a priced menu, an online booking link, and business-published hours or durations); depth=detailed filters; crawl_log (every source fetched or checked, with time, result and facts) on salon detail endpoints and MCP get_merchant; last_verified_at on summaries. Additive only. See docs/API-CHANGELOG.md.\n\n1.1.0: listed salons; fields may be null when the business hasn't published the fact; profiled salons keep 1.0 types (except the DataSF neighborhood slugs and the removed stylist routine/fullName). Additive fields: depth, availability_basis, confirmed, priceKind/priceMax/priceView, unconfirmed. See docs/API-CHANGELOG.md and x-changelog."
  },
  "x-changelog": [
    {
      "version": "1.3.0",
      "date": "2026-10-23",
      "summary": "1.3.0: additive decision contract GET /api/v1/options and MCP compare_options; selected/mandatory add-ons, explicit known subtotals and unresolved prices, consultation/deposit/cancellation/patch-test conditions, first-party hours with preserved conflicts, evidence and next actions. Existing endpoint fields/types are preserved. /openapi.json aliases this specification. Telemetry retains minimized normalized fields, not raw IP/search text/arguments.",
      "additive": [
        "/api/v1/options",
        "MCP compare_options",
        "/openapi.json alias"
      ],
      "behaviour": [
        "Current first-party hours override older/community facts; conflicting values retained",
        "Decision contract uses known subtotal with add-ons; unresolved charges and availability prevent firm matches",
        "Minimized telemetry with enforced retention"
      ]
    },
    {
      "version": "1.2.0",
      "date": "2026-10-24",
      "summary": "1.2.0: new depth value \"detailed\" (imported salon whose own website publishes a priced menu, an online booking link, and business-published hours or durations); depth=detailed filters; crawl_log (every source fetched or checked, with time, result and facts) on salon detail endpoints and MCP get_merchant; last_verified_at on summaries. Additive only. See docs/API-CHANGELOG.md.",
      "additive": [
        "depth enum adds \"detailed\" (Depth, MerchantSummary.depth, salon.depth)",
        "depth filter: /api/v1/salons?depth=, /api/v1/search?mode=merchants&depth=, MCP list_salons/search_merchants depth",
        "crawl_log[] + last_verified_at on /api/v1/salons/{slug}, /api/v1/merchants/{ref}, MCP get_merchant",
        "last_verified_at on merchant summaries, /api/v1/salons, MCP list_salons, offerings-search salon objects",
        "depth_notes.detailed"
      ],
      "behaviour": [
        "ordering: profiled, then detailed, then listed (merchant search, hubs, MCP list_salons)"
      ]
    },
    {
      "version": "1.1.0",
      "date": "2026-10-23",
      "summary": "1.1.0: listed salons; fields may be null when the business hasn't published the fact; profiled salons keep 1.0 types (except the DataSF neighborhood slugs and the removed stylist routine/fullName). Additive fields: depth, availability_basis, confirmed, priceKind/priceMax/priceView, unconfirmed. See docs/API-CHANGELOG.md and x-changelog.",
      "nullableForListedSalons": [
        "Salon.address.street",
        "Salon.address.postalCode",
        "Salon.phone.value",
        "Salon.website.value",
        "Salon.hours.value",
        "Salon.policies",
        "Salon.attributes",
        "BookingSource.capabilities",
        "Offering.haircutType",
        "Offering.hairLength",
        "Offering.audience",
        "Offering.includes.shampoo",
        "Offering.includes.rinse",
        "Offering.includes.blowDry",
        "Offering.includes.style",
        "Offering.includes.consultation"
      ],
      "preexistingExceptionsForProfiled": [
        "Salon.neighborhood: DataSF slugs instead of the 1.0 enum (1.0 slugs still accepted as input)",
        "Stylist.routine and Stylist.fullName removed (privacy)"
      ],
      "additive": [
        "depth, depth_note, depth_info",
        "availability_basis, confirmed, candidate_start_times.{availability_basis,confirmed,label}",
        "availability offering.priceKind / priceMax / priceView (price unchanged)",
        "unconfirmed[] + unconfirmed_reason, uncertain[]",
        "priceRange, availabilityBasis, depth_notes on salon lists",
        "catalog fields: subtypes, tags, areas, nearbyNeighborhoods, license, licenseLookup, registration, sources, fieldSources, externalIds, serviceCategory, durationMaxMinutes, price.amountMax"
      ],
      "behaviour": [
        "require_availability=true is strict (provider-confirmed only; none today) in offerings and merchant mode",
        "haircut_type returns listed salons too",
        "merchant mode selected by mode=merchants or merchant-only parameters"
      ],
      "doc": "https://agentsalonsf.com/agents"
    },
    {
      "version": "1.0.0",
      "date": "2026-10-22",
      "summary": "Hair Shaper only."
    }
  ],
  "servers": [
    {
      "url": "https://agentsalonsf.com"
    }
  ],
  "paths": {
    "/api/v1/options": {
      "get": {
        "operationId": "compareOptions",
        "summary": "Evidence-backed decisions for a requested service and selected extras. No booking or hold.",
        "description": "Returns service-level decisions grouped as options (known requested constraints fit), uncertain (missing facts or provider-confirmed availability), and excluded (known over-budget or closed). Date-specific tasks remain uncertain because no provider-confirmed appointments exist. Known subtotal is not a guaranteed final price. Never interpret schedule candidates or opening hours as open slots. All groups share one stable pagination order: geographic match, then salon/service alphabetically, never quality.",
        "parameters": [
          {
            "name": "service_intent",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 100
            },
            "description": "Supported intent: color, balayage, balayage color, creative color, color consultation, highlights, haircut, mens haircut, womens haircut, wash cut style or blowout. Unknown intents are rejected rather than relaxed.",
            "example": "balayage color",
            "required": true
          },
          {
            "name": "budget",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0
            },
            "description": "Inclusive USD cap on known subtotal including selected/mandatory extras. Unbounded prices do not prove budget fit.",
            "example": 260
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Calendar date in America/Los_Angeles; establishes requested day, not an available appointment.",
            "example": "2026-10-24"
          },
          {
            "name": "neighborhood",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Documented neighborhood/area slug or alias, e.g. soma."
          },
          {
            "name": "geography",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "strict",
                "nearby"
              ],
              "default": "strict"
            },
            "description": "strict preserves the requested geography; nearby includes labelled neighboring salons and requires neighborhood."
          },
          {
            "name": "selected_addons",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "enum": [
                  "blow_dry",
                  "haircut",
                  "toner",
                  "shampoo",
                  "deep_conditioning",
                  "iron_finish",
                  "extensions"
                ]
              },
              "maxItems": 7
            },
            "description": "Selected extras; repeated query values or comma-separated values accepted. Already-included extras are not charged twice.",
            "style": "form",
            "explode": true
          },
          {
            "name": "salon",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Optional known salon slug/ref. Neighborhood constraints still apply."
          },
          {
            "name": "require_availability",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Requires date; no current source can produce a confirmed match."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            },
            "description": "Maximum decisions across all three groups, not per group."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Continue with next_offset. Zero matches is a coverage limitation, not proof no salon offers the service."
          }
        ],
        "responses": {
          "200": {
            "description": "Decision result with explicit uncertainty",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DecisionResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid/duplicate/unknown query parameter, unsupported intent, unknown neighborhood or salon",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "openApiAlias",
        "summary": "Permanent redirect to /api/v1/openapi.json",
        "responses": {
          "308": {
            "description": "Same specification at the canonical URL",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "/api/v1/openapi.json"
              }
            }
          }
        }
      }
    },
    "/api/v1/search": {
      "get": {
        "operationId": "search",
        "summary": "Two modes on one URL. offerings (the v1 default, unchanged contract; same as MCP search_haircuts): haircut constraint search, one row per offering × stylist; hard constraints are never relaxed; empty results include noMatch.reason; with max_price, prices not known to be within the cap are under `uncertain`. merchants: one row per salon, summary-first (agent-discovery semantics; same as MCP search_merchants) — selected by mode=merchants or by any merchant-only parameter (include_nearby, service, day, open_after, time_after, time_before, open_on_date, has_booking, view, offset). q, neighborhood, date and limit alone stay in offerings mode. In merchant mode the offering constraints (haircut_type, hair_length, max_price, stylist, specialty, require_availability, after, before) still apply: only salons with an offering meeting all of them are returned, and only those offerings are shown. Booleans accept true/false, 1/0, yes/no. Booking links are absolute /out URLs (measured redirects to the salon’s own page) with the provider URL in targetUrl.",
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "offerings",
                "merchants"
              ],
              "default": "offerings"
            },
            "description": "offerings (default) or merchants; merchant-only parameters imply merchants."
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "merchants: business name or street, fuzzy (\"cinta\", \"23 grant\"). offerings: substring of the service name.",
            "example": "cinta"
          },
          {
            "name": "neighborhood",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Neighborhood or area slug, label or alias. Neighborhoods: bayview-hunters-point, bernal-heights, castro-upper-market, chinatown, excelsior, financial-district-south-beach, glen-park, golden-gate-park, …; areas: union-square, dogpatch, nopa, cole-valley, lower-haight, jackson-square, mid-market, yerba-buena, rincon-hill, polk-gulch, lower-pacific-heights, duboce-triangle, fillmore, cow-hollow, embarcadero. Old slugs (soma, financial-district) resolve.",
            "example": "soma"
          },
          {
            "name": "include_nearby",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "merchants: with neighborhood, also return salons within ~400 m (match=\"nearby\", distance_label)."
          },
          {
            "name": "service",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "merchants: intent slug (hair-color, balayage, highlights, mens-haircut, womens-haircut, curly-hair, …), vocabulary tag, synonym or free text.",
            "example": "mens-haircut"
          },
          {
            "name": "day",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "merchants: open on this weekday per published hours (Mo..Su or e.g. \"saturday\")."
          },
          {
            "name": "open_after",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^\\d{2}:\\d{2}$"
            },
            "description": "merchants: closes after HH:MM (on `day` if given)."
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Salon-local date. merchants: hours_for_day is computed for it (default today). offerings: candidate start times on that date."
          },
          {
            "name": "time_after",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^\\d{2}:\\d{2}$"
            },
            "description": "merchants: with date, only intervals open after this time count."
          },
          {
            "name": "time_before",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^\\d{2}:\\d{2}$"
            },
            "description": "merchants: with date, only intervals open before this time count."
          },
          {
            "name": "open_on_date",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "merchants: only salons open on `date` per published hours (not live slots)."
          },
          {
            "name": "has_booking",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "merchants: only salons with (true) / without (false) an online booking link."
          },
          {
            "name": "depth",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "profiled",
                "detailed",
                "listed"
              ]
            },
            "description": "merchants: only salons of this depth (1.2.0). Merchant-only parameter."
          },
          {
            "name": "view",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "summary",
                "full"
              ],
              "default": "summary"
            },
            "description": "merchants: summary = compact task view; full = every field with provenance."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "merchants: pagination offset (next_offset in the response)."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100
            },
            "description": "merchants: default 20, max 100. offerings: default 10, max 50."
          },
          {
            "name": "haircut_type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "cut",
                "cut_wash",
                "cut_wash_style",
                "buzz",
                "beard_trim",
                "blow_dry",
                "other"
              ]
            },
            "description": "offerings: normalized haircut type (profiled salons: hand-normalized; listed salons: inferred from the service name — check offering.comparable)."
          },
          {
            "name": "hair_length",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "short",
                "medium",
                "long",
                "any"
              ]
            },
            "description": "offerings"
          },
          {
            "name": "max_price",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "offerings: USD, inclusive"
          },
          {
            "name": "stylist",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "offerings: stylist display name or slug"
          },
          {
            "name": "specialty",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "offerings: e.g. asian-mens-hair, curly-hair"
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^\\d{2}:\\d{2}$"
            },
            "description": "offerings: earliest candidate start, salon-local HH:MM"
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^\\d{2}:\\d{2}$"
            },
            "description": "offerings: latest candidate start, salon-local HH:MM"
          },
          {
            "name": "require_availability",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "offerings and merchants: require_availability=true is satisfied only by provider-confirmed availability (the booking provider’s own open times, existing bookings subtracted). No salon on this site has provider-confirmed availability today: profiled salons have candidate start times from the published staff schedule (existing bookings not subtracted, so they may already be taken) and listed salons have published opening hours only. Offerings with matching candidate start times are listed under `unconfirmed`; confirm on the booking page. In merchant mode the salons with only candidates are under `unconfirmed` (with unconfirmed_reason)."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/SearchResponse"
                    },
                    {
                      "$ref": "#/components/schemas/OfferingSearchResponse"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid query",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/availability": {
      "get": {
        "operationId": "getAvailability",
        "summary": "Two contracts. ref (agent-discovery): published opening hours on a date, offerings matching a service, booking link and step-by-step instructions; profiled salons add candidate_start_times. salon+offering: candidate start times for one offering — candidate start times from the salon's published staff schedule; existing bookings are not subtracted, so some may already be taken — confirm on the booking page (slotBasis=schedule_candidates, bookingsSubtracted=false).",
        "parameters": [
          {
            "name": "ref",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "\"<city>/<vertical>/<slug>\" from search results",
            "example": "san-francisco/hair-salon/hairshaper"
          },
          {
            "name": "service",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "with ref: e.g. \"balayage\", \"mens-haircut\" or free text"
          },
          {
            "name": "time_after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "with ref"
          },
          {
            "name": "time_before",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "with ref"
          },
          {
            "name": "salon",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "salon slug (with offering)"
          },
          {
            "name": "offering",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "offering id or slug"
          },
          {
            "name": "stylist",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "with offering"
          },
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "salon-local date"
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "with offering"
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "with offering"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "with offering"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/AvailabilityResponse"
                    },
                    {
                      "$ref": "#/components/schemas/CandidateTimesResponse"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/merchants/{city}/{vertical}/{slug}": {
      "get": {
        "operationId": "getMerchant",
        "summary": "One salon with every field, crawl_log + last_verified_at (1.2.0), depth + depth_info (what an agent can and cannot rely on), availability_basis, per-field provenance (verification), how_verified, not_verified, offerings, booking_cta and instructions.",
        "parameters": [
          {
            "name": "city",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "vertical",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/salons": {
      "get": {
        "operationId": "listSalons",
        "summary": "List salons (paginated: limit ≤ 500, offset, depth=profiled|detailed|listed). Each row has last_verified_at.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "default 100"
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": ""
          },
          {
            "name": "depth",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "profiled",
                "detailed",
                "listed"
              ]
            },
            "description": ""
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/api/v1/salons/{slug}": {
      "get": {
        "operationId": "getSalon",
        "summary": "Full salon record (our schema) plus booking_cta, verification and not_verified.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "salon": {
                      "$ref": "#/components/schemas/SalonRecordSalon"
                    },
                    "offerings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SalonRecordOffering"
                      }
                    },
                    "depth": {
                      "type": "string",
                      "enum": [
                        "profiled",
                        "detailed",
                        "listed"
                      ]
                    },
                    "depth_info": {
                      "$ref": "#/components/schemas/DepthInfo"
                    },
                    "last_verified_at": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    },
                    "crawl_log": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CrawlEntry"
                      }
                    }
                  }
                }
              }
            }
          },
          "301": {
            "description": "Old id; see Location"
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/api/v1/stylists/{slug}": {
      "get": {
        "operationId": "getStylist",
        "summary": "Stylist (display name as the booking system shows it) with candidate start times. Retired full-name slugs answer 410 Gone.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "Not found"
          },
          "410": {
            "description": "Retired"
          }
        }
      }
    },
    "/api/v1/status": {
      "get": {
        "operationId": "getStatus",
        "summary": "Crawl freshness, verification accuracy, slot basis, the per-provider capture permission record (unlisted providers default to captureAllowed=no) and the dataset revision.",
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/out": {
      "get": {
        "operationId": "outbound",
        "summary": "Logged redirect to a salon’s own booking page or website (allow-listed targets only).",
        "parameters": [
          {
            "name": "m",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "ref or slug"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "target URL; must be the salon’s recorded booking or website URL"
          }
        ],
        "responses": {
          "302": {
            "description": "Redirect"
          },
          "400": {
            "description": "Not an allowed target"
          },
          "404": {
            "description": "Unknown salon"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "DecisionEvidence": {
        "type": "object",
        "required": [
          "source",
          "url",
          "checked_at",
          "status",
          "note"
        ],
        "properties": {
          "source": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          },
          "checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status": {
            "type": "string"
          },
          "note": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "DecisionCharge": {
        "type": "object",
        "required": [
          "id",
          "label",
          "amount_min",
          "amount_max",
          "included_in_base",
          "required",
          "evidence"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable charge identifier; charge appearing in both lists is counted once"
          },
          "label": {
            "type": "string",
            "description": ""
          },
          "amount_min": {
            "type": [
              "number",
              "null"
            ]
          },
          "amount_max": {
            "type": [
              "number",
              "null"
            ]
          },
          "included_in_base": {
            "type": "boolean"
          },
          "required": {
            "type": "boolean"
          },
          "evidence": {
            "$ref": "#/components/schemas/DecisionEvidence"
          }
        }
      },
      "DecisionCondition": {
        "type": "object",
        "required": [
          "value",
          "status",
          "evidence"
        ],
        "properties": {
          "value": {
            "type": [
              "string",
              "null"
            ],
            "description": "null means unknown; retain qualifications in published text"
          },
          "status": {
            "type": "string",
            "enum": [
              "unknown",
              "published"
            ]
          },
          "evidence": {
            "$ref": "#/components/schemas/DecisionEvidence"
          },
          "required": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "Consultation only; null means not established"
          },
          "treatment": {
            "const": "advance_not_added_to_total",
            "description": "Deposit only"
          },
          "added_to_subtotal": {
            "const": false,
            "description": "Deposit only"
          }
        }
      },
      "DecisionOption": {
        "type": "object",
        "required": [
          "salon",
          "offering",
          "fit",
          "evidence",
          "price",
          "duration",
          "hours",
          "availability",
          "conditions",
          "next_action"
        ],
        "properties": {
          "salon": {
            "type": "object",
            "properties": {
              "slug": {
                "type": "string",
                "description": ""
              },
              "name": {
                "type": "string",
                "description": ""
              },
              "neighborhood": {
                "type": "string",
                "description": "Actual neighborhood slug"
              },
              "geographic_match": {
                "type": "string",
                "enum": [
                  "in",
                  "nearby"
                ]
              }
            }
          },
          "offering": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": ""
              },
              "name": {
                "type": "string",
                "description": "Exact published service"
              }
            }
          },
          "fit": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "match",
                  "conditional",
                  "excluded"
                ]
              },
              "reasons": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "inclusions": {
                "type": "object",
                "additionalProperties": {
                  "type": "object",
                  "properties": {
                    "included": {
                      "type": [
                        "boolean",
                        "null"
                      ]
                    },
                    "evidence": {
                      "$ref": "#/components/schemas/DecisionEvidence"
                    }
                  }
                }
              },
              "service_match": {
                "type": "string",
                "description": ""
              },
              "availability_confirmed": {
                "type": "boolean"
              }
            }
          },
          "evidence": {
            "type": "object",
            "properties": {
              "service": {
                "$ref": "#/components/schemas/DecisionEvidence"
              },
              "price": {
                "$ref": "#/components/schemas/DecisionEvidence"
              },
              "hours": {
                "$ref": "#/components/schemas/DecisionEvidence"
              },
              "inclusions": {
                "type": "object",
                "additionalProperties": {
                  "$ref": "#/components/schemas/DecisionEvidence"
                }
              }
            }
          },
          "price": {
            "type": "object",
            "properties": {
              "currency": {
                "const": "USD"
              },
              "kind": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "fixed",
                  "range",
                  "from",
                  "consultation_required",
                  null
                ]
              },
              "base": {
                "type": "object",
                "properties": {
                  "min": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "max": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "evidence": {
                    "$ref": "#/components/schemas/DecisionEvidence"
                  }
                }
              },
              "mandatory_extras": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DecisionCharge"
                }
              },
              "selected_addons": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/DecisionCharge"
                }
              },
              "known_subtotal_min": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "known_subtotal_max": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "unresolved_charges": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "budget_status": {
                "type": "string",
                "enum": [
                  "within",
                  "over",
                  "unknown",
                  "not_requested"
                ]
              },
              "subtotal_note": {
                "type": "string",
                "description": "Known subtotal is not a provider-confirmed final quote; optional gratuity excluded; deposit not added again"
              }
            }
          },
          "duration": {
            "type": "object",
            "required": [
              "min_minutes",
              "max_minutes",
              "base_service_minutes",
              "base_service_max_minutes",
              "status",
              "evidence",
              "note"
            ],
            "properties": {
              "min_minutes": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Full requested service duration; null when a selected extra has unknown duration"
              },
              "max_minutes": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "base_service_minutes": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "base_service_max_minutes": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "status": {
                "type": "string",
                "enum": [
                  "unknown",
                  "published"
                ]
              },
              "evidence": {
                "$ref": "#/components/schemas/DecisionEvidence"
              },
              "note": {
                "type": "string",
                "description": ""
              }
            }
          },
          "hours": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "object"
                }
              },
              "for_date": {
                "type": [
                  "object",
                  "null"
                ]
              },
              "source": {
                "type": "string",
                "description": ""
              },
              "checked_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "unconfirmed": {
                "type": "boolean"
              },
              "label": {
                "type": "string",
                "description": ""
              },
              "recheck": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Latest first-party hours recheck, including failed/withdrawn source notes",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": ""
                  },
                  "checked_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "note": {
                    "type": "string",
                    "description": ""
                  }
                }
              },
              "conflict": {
                "type": "boolean"
              },
              "conflicts": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "value": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "provenance": {
                      "$ref": "#/components/schemas/Provenance"
                    }
                  }
                }
              }
            }
          },
          "availability": {
            "type": "object",
            "properties": {
              "basis": {
                "$ref": "#/components/schemas/AvailabilityBasis"
              },
              "checked_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              },
              "confirmed": {
                "type": "boolean",
                "description": "Always false in current data"
              },
              "note": {
                "type": "string",
                "description": ""
              }
            }
          },
          "conditions": {
            "type": "object",
            "required": [
              "consultation",
              "deposit",
              "cancellation",
              "patch_test"
            ],
            "properties": {
              "consultation": {
                "$ref": "#/components/schemas/DecisionCondition"
              },
              "deposit": {
                "$ref": "#/components/schemas/DecisionCondition"
              },
              "cancellation": {
                "$ref": "#/components/schemas/DecisionCondition"
              },
              "patch_test": {
                "$ref": "#/components/schemas/DecisionCondition"
              }
            }
          },
          "next_action": {
            "type": "object",
            "properties": {
              "url": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Provider handoff or published contact/research destination"
              },
              "precision": {
                "type": "string",
                "description": ""
              },
              "kind": {
                "type": "string",
                "description": ""
              },
              "remaining_steps": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "DecisionResponse": {
        "type": "object",
        "required": [
          "options",
          "uncertain",
          "excluded",
          "query",
          "ranking",
          "availability_note",
          "total",
          "count",
          "offset",
          "next_offset",
          "dataset_revision",
          "decision_facts_revision",
          "no_match_reason",
          "documentation"
        ],
        "properties": {
          "query": {
            "type": "object"
          },
          "options": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DecisionOption"
            }
          },
          "uncertain": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DecisionOption"
            }
          },
          "excluded": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DecisionOption"
            }
          },
          "ranking": {
            "type": "string",
            "description": "Geography then alphabetical; no quality claim"
          },
          "availability_note": {
            "type": "string",
            "description": ""
          },
          "total": {
            "type": "integer"
          },
          "count": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "next_offset": {
            "type": [
              "integer",
              "null"
            ]
          },
          "no_match_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "dataset_revision": {
            "type": "string",
            "description": ""
          },
          "decision_facts_revision": {
            "type": "string",
            "description": ""
          },
          "documentation": {
            "type": "string",
            "description": ""
          }
        }
      },
      "ClaimStatus": {
        "type": "string",
        "enum": [
          "verified",
          "reported",
          "inferred",
          "unknown",
          "stale"
        ]
      },
      "SalonRecordSalon": {
        "type": "object",
        "description": "The `salon` object of /api/v1/salons/{slug} and MCP get_salon (json). 1.1.0: fields marked nullable may be null for listed salons; profiled salons keep 1.0 types.",
        "properties": {
          "depth": {
            "type": "string",
            "enum": [
              "profiled",
              "detailed",
              "listed"
            ]
          },
          "neighborhood": {
            "type": "string",
            "description": "DataSF Analysis Neighborhood slug (1.1.0; 1.0 enum slugs accepted as input)"
          },
          "address": {
            "type": "object",
            "properties": {
              "street": {
                "type": [
                  "string",
                  "null"
                ],
                "nullable": true,
                "description": "null for listed salons when the business has not published it (1.1.0)"
              },
              "postalCode": {
                "type": [
                  "string",
                  "null"
                ],
                "nullable": true,
                "description": "null for listed salons when the business has not published it (1.1.0)"
              },
              "city": {
                "type": "string",
                "description": ""
              },
              "state": {
                "type": "string",
                "description": ""
              }
            }
          },
          "phone": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ],
                "nullable": true,
                "description": "null for listed salons when the business has not published it (1.1.0)"
              },
              "provenance": {
                "$ref": "#/components/schemas/Provenance"
              }
            }
          },
          "website": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "nullable": true,
                "description": "null for listed salons when the business has not published it (1.1.0)"
              },
              "provenance": {
                "$ref": "#/components/schemas/Provenance"
              }
            }
          },
          "hours": {
            "type": "object",
            "properties": {
              "value": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "object"
                },
                "nullable": true,
                "description": "null for listed salons when the business has not published it (1.1.0)"
              },
              "provenance": {
                "$ref": "#/components/schemas/Provenance"
              }
            }
          },
          "policies": {
            "type": [
              "object",
              "null"
            ],
            "nullable": true,
            "description": "null = not researched for this listing (1.1.0)"
          },
          "attributes": {
            "type": [
              "object",
              "null"
            ],
            "nullable": true,
            "description": "null = not researched for this listing (1.1.0)"
          },
          "bookingSources": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "capabilities": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "nullable": true,
                  "description": "null = not researched (1.1.0)"
                }
              }
            }
          }
        }
      },
      "SalonRecordOffering": {
        "type": "object",
        "description": "An offering in /api/v1/salons/{slug}, /api/v1/search (offerings mode) and MCP. 1.1.0: imported menus may carry nulls; profiled salons keep 1.0 types.",
        "properties": {
          "haircutType": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "cut",
              "cut_wash",
              "cut_wash_style",
              "buzz",
              "beard_trim",
              "blow_dry",
              "other",
              null
            ],
            "nullable": true,
            "description": "null = not normalized (1.1.0)"
          },
          "hairLength": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "short",
              "medium",
              "long",
              "any",
              null
            ],
            "nullable": true,
            "description": "null = not stated (1.1.0)"
          },
          "audience": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "any",
              "men",
              "women",
              "kids",
              null
            ],
            "nullable": true,
            "description": "null = not stated (1.1.0)"
          },
          "includes": {
            "type": "object",
            "properties": {
              "shampoo": {
                "type": [
                  "boolean",
                  "null"
                ],
                "nullable": true,
                "description": "true/false only when the salon says so; null = not published (1.1.0)"
              },
              "rinse": {
                "type": [
                  "boolean",
                  "null"
                ],
                "nullable": true,
                "description": "true/false only when the salon says so; null = not published (1.1.0)"
              },
              "blowDry": {
                "type": [
                  "boolean",
                  "null"
                ],
                "nullable": true,
                "description": "true/false only when the salon says so; null = not published (1.1.0)"
              },
              "style": {
                "type": [
                  "boolean",
                  "null"
                ],
                "nullable": true,
                "description": "true/false only when the salon says so; null = not published (1.1.0)"
              },
              "consultation": {
                "type": [
                  "boolean",
                  "null"
                ],
                "nullable": true,
                "description": "true/false only when the salon says so; null = not published (1.1.0)"
              }
            }
          },
          "comparable": {
            "type": "boolean"
          },
          "comparisonNote": {
            "type": "string",
            "description": ""
          },
          "price": {
            "type": "object",
            "properties": {
              "kind": {
                "type": "string",
                "enum": [
                  "fixed",
                  "starting_at",
                  "range",
                  "varies_by_stylist",
                  "consultation",
                  "unknown"
                ]
              },
              "amount": {
                "type": [
                  "number",
                  "null"
                ]
              },
              "amountMax": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "1.1.0, range only"
              },
              "discrepancyNote": {
                "type": "string",
                "description": ""
              }
            }
          }
        }
      },
      "Provenance": {
        "type": "object",
        "properties": {
          "status": {
            "$ref": "#/components/schemas/ClaimStatus"
          },
          "source": {
            "type": "object",
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "salon_website",
                  "booking_provider",
                  "review_on_salon_site",
                  "manual",
                  "derived",
                  "business_registry",
                  "state_license",
                  "openstreetmap",
                  "link_check"
                ]
              },
              "url": {
                "type": "string"
              }
            }
          },
          "observedAt": {
            "type": "string",
            "format": "date-time"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "Freshness": {
        "type": "string",
        "enum": [
          "fresh",
          "stale",
          "unknown"
        ]
      },
      "SlotBasis": {
        "type": "string",
        "enum": [
          "schedule_candidates",
          "bookings_subtracted"
        ],
        "description": "schedule_candidates: candidate start times from the salon's published staff schedule; existing bookings are not subtracted, so some may already be taken — confirm on the booking page."
      },
      "AvailabilityBasis": {
        "type": "string",
        "enum": [
          "provider_confirmed",
          "schedule_candidates",
          "published_hours",
          "none"
        ],
        "description": "provider_confirmed: open times confirmed by the salon’s booking provider (existing bookings subtracted), captured within the freshness window. schedule_candidates: UNCONFIRMED — candidate start times from the salon's published staff schedule; existing bookings are not subtracted, so some may already be taken — confirm on the booking page. published_hours: published opening hours only — the salon is open then, which does not mean an appointment is free. none: no availability information captured for this salon. Only provider_confirmed satisfies require_availability=true; none exists today."
      },
      "Depth": {
        "type": "string",
        "enum": [
          "profiled",
          "detailed",
          "listed"
        ],
        "description": "Profiled: researched in depth from the salon’s own website and online booking system — normalized haircut offerings, online-booking prices, stylists (booking-system names), booking rules and candidate start times. Detailed: imported, and the business itself publishes a priced menu, an online booking link and its opening hours or service durations — every published service, price and duration is shown; not profiled in depth. Listed: imported from public records (CA licence data, SF business registry, OpenStreetMap) and the business’s own website; not profiled in depth. Detailed = the business’s own website publishes a menu with prices, it has an online booking link, and the business itself publishes its opening hours (on its website or booking page, or confirmed by a manual check — map data does not count) or its service durations. (\"detailed\" added in 1.2.0.)"
      },
      "CrawlEntry": {
        "type": "object",
        "description": "One source we fetched or checked (1.2.0). Built only from recorded provenance; a failed fetch says so.",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "salon_website",
              "price_page",
              "booking_page",
              "booking_system",
              "registry",
              "licence",
              "openstreetmap",
              "manual",
              "other"
            ]
          },
          "label": {
            "type": "string",
            "description": "What it is"
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          },
          "host": {
            "type": [
              "string",
              "null"
            ]
          },
          "ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "Dataset record reference, e.g. datasf:…, ca-dca-bbc:…, node/…"
          },
          "checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Recorded fetch/check time; null = not recorded (never counts toward last_verified_at)"
          },
          "data_as_of": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Registries and licence data: the dataset as-of date (not a check time)"
          },
          "result": {
            "type": "string",
            "description": "\"HTTP 200\", \"read\", \"could not open (…)\", \"no matching establishment licence (matching can miss)\", \"dead link\""
          },
          "ok": {
            "type": [
              "boolean",
              "null"
            ],
            "description": "null = read, HTTP status not recorded"
          },
          "facts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "DepthInfo": {
        "type": "object",
        "properties": {
          "depth": {
            "$ref": "#/components/schemas/Depth"
          },
          "label": {
            "type": "string",
            "description": ""
          },
          "note": {
            "type": "string",
            "description": ""
          },
          "can_rely_on": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "cannot_rely_on": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Slot": {
        "type": "object",
        "properties": {
          "salonId": {
            "type": "string"
          },
          "stylistId": {
            "type": "string"
          },
          "offeringId": {
            "type": "string"
          },
          "start": {
            "type": "string",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "format": "date-time"
          },
          "freshness": {
            "$ref": "#/components/schemas/Freshness"
          },
          "capturedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "slotBasis": {
            "$ref": "#/components/schemas/SlotBasis"
          },
          "bookingsSubtracted": {
            "type": "boolean"
          },
          "bookingUrl": {
            "type": "string"
          }
        }
      },
      "Offering": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": ""
          },
          "name": {
            "type": "string",
            "description": "The business’s own wording"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "service_category": {
            "type": "string",
            "description": "haircut | color | styling | treatment | beard | extensions | other"
          },
          "price_min": {
            "type": [
              "number",
              "null"
            ]
          },
          "price_max": {
            "type": [
              "number",
              "null"
            ],
            "description": "null = open-ended (\"from $X\")"
          },
          "currency": {
            "type": "string",
            "description": "USD"
          },
          "price_text": {
            "type": "string",
            "description": ""
          },
          "duration_min": {
            "type": [
              "integer",
              "null"
            ]
          },
          "duration_max": {
            "type": [
              "integer",
              "null"
            ]
          },
          "source_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "checked_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "status": {
            "$ref": "#/components/schemas/ClaimStatus"
          }
        },
        "example": {
          "id": "sf-example:svc:1",
          "name": "Balayage",
          "tags": [
            "balayage"
          ],
          "service_category": "color",
          "price_min": 240,
          "price_max": null,
          "currency": "USD",
          "price_text": "from $240",
          "duration_min": 150,
          "duration_max": 180,
          "source_url": "https://salon.example/menu",
          "checked_at": "2026-10-22T10:00:00Z",
          "status": "verified"
        }
      },
      "Availability": {
        "type": "object",
        "description": "Published opening hours on one date — not live appointment slots.",
        "properties": {
          "date": {
            "type": "string",
            "description": ""
          },
          "weekday": {
            "type": "string",
            "description": "Mo..Su"
          },
          "weekday_name": {
            "type": "string",
            "description": ""
          },
          "status": {
            "type": "string",
            "enum": [
              "open_per_published_hours",
              "closed_per_published_hours",
              "hours_unknown"
            ]
          },
          "open_intervals": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "opens": {
                  "type": "string",
                  "description": ""
                },
                "closes": {
                  "type": "string",
                  "description": ""
                }
              }
            }
          },
          "note": {
            "type": "string",
            "description": ""
          },
          "source": {
            "type": [
              "object",
              "null"
            ]
          }
        },
        "example": {
          "date": "2026-10-24",
          "weekday": "Sa",
          "weekday_name": "Saturday",
          "status": "open_per_published_hours",
          "open_intervals": [
            {
              "opens": "09:00",
              "closes": "17:00"
            }
          ],
          "note": "Published opening hours, not live appointment slots.",
          "source": {
            "source": "openstreetmap",
            "source_url": "https://www.openstreetmap.org/node/1",
            "checked_at": "2026-10-22T09:00:00Z"
          }
        }
      },
      "BookingInstructions": {
        "type": "object",
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "online",
              "phone",
              "none"
            ]
          },
          "platform": {
            "type": [
              "string",
              "null"
            ]
          },
          "via": {
            "type": [
              "string",
              "null"
            ]
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "account": {
            "type": [
              "string",
              "null"
            ]
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "example": {
          "method": "online",
          "platform": "fresha",
          "via": "Fresha",
          "steps": [
            "Open the Fresha booking link.",
            "Choose the service and, optionally, a team member.",
            "Pick an open time slot.",
            "Confirm with a Fresha login or your contact details."
          ],
          "account": "May require a Fresha account.",
          "notes": []
        }
      },
      "MerchantSummary": {
        "type": "object",
        "properties": {
          "ref": {
            "type": "string",
            "description": "<city>/<vertical>/<slug>"
          },
          "name": {
            "type": "string",
            "description": ""
          },
          "depth": {
            "$ref": "#/components/schemas/Depth"
          },
          "depth_note": {
            "type": "string",
            "description": "One-line explanation of the depth"
          },
          "last_verified_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Newest successful check of any source (1.2.0)"
          },
          "availability_basis": {
            "$ref": "#/components/schemas/AvailabilityBasis"
          },
          "neighborhood": {
            "type": "string",
            "description": ""
          },
          "match": {
            "type": "string",
            "enum": [
              "nearby"
            ]
          },
          "distance_label": {
            "type": "string",
            "description": ""
          },
          "address": {
            "type": [
              "string",
              "null"
            ]
          },
          "phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "booking_cta": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "label": {
                "type": "string",
                "description": ""
              },
              "url": {
                "type": "string",
                "description": "Our logged /out redirect to the business’s own booking page"
              }
            }
          },
          "license_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "hours_for_day": {
            "type": "object"
          },
          "matched_offerings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "price_note": {
            "type": "string",
            "description": ""
          },
          "candidate_start_times": {
            "type": "object",
            "description": "Profiled salons only."
          },
          "canonical_url": {
            "type": "string",
            "description": ""
          },
          "freshness": {
            "type": "object"
          },
          "not_verified": {
            "type": "string",
            "description": ""
          }
        },
        "example": {
          "ref": "san-francisco/hair-salon/example-salon",
          "name": "Example Salon",
          "depth": "listed",
          "neighborhood": "Mission",
          "address": "1 Example St, San Francisco, CA 94110",
          "phone": "+14155550100",
          "booking_cta": {
            "label": "Book on Fresha",
            "url": "https://agentsalonsf.com/out?m=san-francisco/hair-salon/example-salon&to=https%3A%2F%2Fwww.fresha.com%2Fa"
          },
          "license_status": "current",
          "hours_for_day": {
            "day": "Sa 2026-10-24",
            "status": "open_per_published_hours",
            "hours": "09:00–17:00"
          },
          "matched_offerings": [
            "Men's cut: from $45"
          ],
          "price_note": "Starting prices as published by the business; the final price may be higher.",
          "canonical_url": "https://agentsalonsf.com/salons/example-salon",
          "freshness": {
            "data_refreshed_at": "2026-10-22T16:40:40Z",
            "prices_checked_at": "2026-10-22T10:00:00Z"
          },
          "not_verified": "live appointment slots; not confirmed by phone"
        }
      },
      "MerchantFull": {
        "type": "object",
        "description": "merchantDetail: salon (our schema, with provenance), offerings, booking_cta, booking_instructions, verification, how_verified, not_verified."
      },
      "SearchResponse": {
        "type": "object",
        "properties": {
          "mode": {
            "const": "merchants"
          },
          "query": {
            "type": "object"
          },
          "total": {
            "type": "integer"
          },
          "count": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "next_offset": {
            "type": [
              "integer",
              "null"
            ]
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantSummary"
            }
          },
          "unconfirmed": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MerchantSummary"
            },
            "description": "require_availability=true only: salons whose matching offerings have only schedule-derived candidate start times in the window. Not matches."
          },
          "unconfirmed_reason": {
            "type": "string",
            "description": ""
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "notes": {
            "type": "string",
            "description": ""
          },
          "dataset_revision": {
            "type": "string",
            "description": ""
          },
          "attribution": {
            "type": "string",
            "description": ""
          }
        },
        "example": {
          "mode": "merchants",
          "total": 1,
          "count": 1,
          "limit": 20,
          "offset": 0,
          "next_offset": null,
          "results": [],
          "warnings": [],
          "notes": "…",
          "dataset_revision": "b-merchants@2026-10-23T06:01:43Z#65c5866d73d1",
          "attribution": "Data © OpenStreetMap contributors (ODbL) …"
        }
      },
      "OfferingSearchResponse": {
        "type": "object",
        "properties": {
          "mode": {
            "const": "offerings"
          },
          "query": {
            "type": "object"
          },
          "results": {
            "type": "array",
            "description": "Each result: salon {depth, depth_note, …}, offering (comparable, comparisonNote, price.discrepancyNote), reasons, slots (unconfirmed candidates), availability_basis, availability {availability_basis, confirmed, slotBasis, …}, booking."
          },
          "uncertain": {
            "type": "array",
            "description": "max_price: final price not known to be within the cap"
          },
          "unconfirmed": {
            "type": "array",
            "description": "require_availability=true: only schedule-derived candidate start times match the window (not confirmed)"
          },
          "noMatch": {
            "type": "object"
          },
          "caveats": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "AvailabilityResponse": {
        "type": "object",
        "properties": {
          "merchant": {
            "type": "object"
          },
          "date": {
            "type": "string",
            "description": ""
          },
          "timezone": {
            "type": "string",
            "description": ""
          },
          "availability_basis": {
            "$ref": "#/components/schemas/AvailabilityBasis"
          },
          "confirmed": {
            "type": "boolean",
            "description": "Always false today"
          },
          "availability": {
            "$ref": "#/components/schemas/Availability"
          },
          "candidate_start_times": {
            "type": "object"
          },
          "offerings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Offering"
            }
          },
          "booking_cta": {
            "type": [
              "object",
              "null"
            ]
          },
          "booking_instructions": {
            "$ref": "#/components/schemas/BookingInstructions"
          },
          "license_status": {
            "type": [
              "string",
              "null"
            ]
          },
          "freshness": {
            "type": "object"
          },
          "not_verified": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "note": {
            "type": "string",
            "description": ""
          }
        },
        "example": {
          "date": "2026-10-24",
          "timezone": "America/Los_Angeles",
          "availability": {
            "status": "open_per_published_hours"
          },
          "offerings": [],
          "booking_cta": null,
          "license_status": "current",
          "note": "Hours-based answer from published opening hours — we do not see live appointment slots."
        }
      },
      "PriceView": {
        "type": "object",
        "description": "Shared price shape; a range or starting price is never a bare number.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "fixed",
              "starting_at",
              "range",
              "varies_by_stylist",
              "consultation",
              "unknown"
            ]
          },
          "amount": {
            "type": [
              "number",
              "null"
            ]
          },
          "amountMax": {
            "type": [
              "number",
              "null"
            ],
            "description": "null = open-ended"
          },
          "currency": {
            "type": "string",
            "description": "USD"
          },
          "label": {
            "type": "string",
            "description": "\"$45\", \"$45–$65\", \"from $45\""
          }
        }
      },
      "CandidateTimesResponse": {
        "type": "object",
        "properties": {
          "salon": {
            "type": "object"
          },
          "offering": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": ""
              },
              "name": {
                "type": "string",
                "description": ""
              },
              "durationMinutes": {
                "type": [
                  "integer",
                  "null"
                ]
              },
              "price": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "v1, unchanged: the lower (or only) published amount. Not the final price unless priceKind is \"fixed\"; see priceKind / priceMax / priceView."
              },
              "priceKind": {
                "type": "string",
                "enum": [
                  "fixed",
                  "starting_at",
                  "range",
                  "varies_by_stylist",
                  "consultation",
                  "unknown"
                ],
                "description": "fixed = price is the price; range = price..priceMax; starting_at = from price, final price not published"
              },
              "priceMax": {
                "type": [
                  "number",
                  "null"
                ],
                "description": "Upper bound (equals price when fixed; null when open-ended or unknown)"
              },
              "priceView": {
                "$ref": "#/components/schemas/PriceView"
              }
            }
          },
          "freshness": {
            "$ref": "#/components/schemas/Freshness"
          },
          "availability_basis": {
            "$ref": "#/components/schemas/AvailabilityBasis"
          },
          "confirmed": {
            "type": "boolean"
          },
          "slotBasis": {
            "$ref": "#/components/schemas/SlotBasis"
          },
          "bookingsSubtracted": {
            "type": "boolean"
          },
          "slotBasisLabel": {
            "type": "string",
            "description": ""
          },
          "count": {
            "type": "integer"
          },
          "slots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Slot"
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": ""
          }
        }
      }
    }
  }
}