{
  "openapi": "3.1.0",
  "info": {
    "title": "Fron Research History API",
    "version": "1.0.0-preview",
    "summary": "Historical booking inventory, prices and demand for recreation venues, one schema across every venue type.",
    "description": "Fron Research observes inferred booking signals in the public booking data of recreation venues (saunas and bathhouses, trampoline and adventure parks, bowling centers, golf simulator studios, pickleball and padel clubs, training facilities) and keeps what it sees. Every source is normalized into one set of tables, and this API serves those tables.\n\n## The model\n\nSeven nouns, one fact table.\n\n| Entity | What one row is |\n|---|---|\n| **Market** | A metro area. Venues belong to exactly one. |\n| **Venue type** | The kind of business a venue is (`bowling_center`, `trampoline_park`, `bathhouse_spa`, …). One per venue, never inferred from what it sells. |\n| **Activity** | What a product lets you do (`lanes`, `jump`, `sauna`, `courts`, `bays`, …). An activity lives on a product; a venue's activities are the distinct activities of its products. |\n| **Venue** | One location of one business, with its brand, market, venue type, source and inventory grain. |\n| **Product** | One thing a venue sells: one activity, a unit (lane, court, jumper, seat, bay, session), a duration, a party size and a list price. |\n| **Resource** | What a product is booked against: a named unit (lane 7, court 2) or a pool (open-jump capacity). |\n| **Slot** | **The fact table.** One product on one resource over one start-end window: capacity, booked, how booked was measured, the price and what that price is evidence of, and the final status. Works for granular inventory (a lane at 7:00 pm) and pooled inventory (jump capacity for the 2:00 pm hour) alike. |\n| **Observation** | One look at one slot at one instant: remaining and price. The sequence of observations for a slot is its booking curve. |\n\nRollups (a venue's day, a market, a brand) are computed from slots by `GET /metrics`. They are never stored as a separate truth.\n\n## Provenance\n\nEvery number says how it was measured. `booked_kind` is `count` when the public listing showed how many were sold, `bounds` when only a floor and ceiling can be proven, `null` when the slot was never observed before it started. `price_evidence` is `quoted` when the price was shown for that slot, `list` when it comes from the venue's rate card, `researched` when it was researched and dated by us. Unknown is never zero.\n\n## Depth\n\nDepth differs by venue. Each venue carries a `coverage` object stating its own floors (`slots_from`, `observations_from`); read it before you compare venues.\n\n## Access\n\n**Free** is one frozen day of real data nationwide, every endpoint, that day only, no key. **Pro** is every market and the full slot history. **Data** adds `GET /observations`, bulk exports and evidence paths. This origin is not serving yet; the contract below is the one the data is being built to.\n",
    "contact": {
      "name": "Fron Research",
      "url": "https://fronresearch.com/#contact",
      "email": "luke@fronresearch.com"
    },
    "license": {
      "name": "Fron Research data terms",
      "url": "https://fronresearch.com/privacy/"
    }
  },
  "servers": [
    {
      "url": "https://api.fronresearch.com/v1",
      "description": "Planned production origin. Not serving yet."
    }
  ],
  "tags": [
    {"name": "Markets", "description": "Metro areas with venues under coverage."},
    {"name": "Venues", "description": "Locations, their products and resources, and the depth of history each one carries."},
    {"name": "Slots", "description": "The fact table: one product on one resource over one time window."},
    {"name": "Metrics", "description": "Rollups computed from slots: sell-through, utilization and revenue for a venue, market, brand, activity or venue type."},
    {"name": "Observations", "description": "Booking curves: every look at a slot before it started. Data tier."}
  ],
  "security": [{"BearerKey": []}, {}],
  "paths": {
    "/markets": {
      "get": {
        "tags": ["Markets"],
        "summary": "List markets",
        "operationId": "listMarkets",
        "description": "Every market with at least one venue under coverage, with venue counts by venue type and activity.",
        "parameters": [
          {"$ref": "#/components/parameters/Activity"},
          {"$ref": "#/components/parameters/VenueType"},
          {"$ref": "#/components/parameters/Limit"},
          {"$ref": "#/components/parameters/Cursor"}
        ],
        "responses": {
          "200": {
            "description": "A page of markets.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MarketPage"}, "examples": {"default": {"$ref": "#/components/examples/MarketPage"}}}}
          },
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "429": {"$ref": "#/components/responses/TooManyRequests"}
        }
      }
    },
    "/venues": {
      "get": {
        "tags": ["Venues"],
        "summary": "List venues",
        "operationId": "listVenues",
        "description": "Venues under coverage, filterable by market, brand, venue type and activity. Each row carries its `coverage` object so you know how far back its slots and observations go before you query them.",
        "parameters": [
          {"$ref": "#/components/parameters/Market"},
          {"$ref": "#/components/parameters/Brand"},
          {"$ref": "#/components/parameters/VenueType"},
          {"$ref": "#/components/parameters/Activity"},
          {"name": "inventory_grain", "in": "query", "description": "Keep venues of one inventory grain.", "schema": {"$ref": "#/components/schemas/InventoryGrain"}},
          {"$ref": "#/components/parameters/Limit"},
          {"$ref": "#/components/parameters/Cursor"}
        ],
        "responses": {
          "200": {
            "description": "A page of venues.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VenuePage"}, "examples": {"default": {"$ref": "#/components/examples/VenuePage"}}}}
          },
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "429": {"$ref": "#/components/responses/TooManyRequests"}
        }
      }
    },
    "/venues/{venue_id}": {
      "get": {
        "tags": ["Venues"],
        "summary": "Get a venue with its products and resources",
        "operationId": "getVenue",
        "description": "One venue, its product catalog (what it sells, at what list price), its resources (what those products are booked against) and its coverage floors.",
        "parameters": [{"$ref": "#/components/parameters/VenueId"}],
        "responses": {
          "200": {
            "description": "The venue.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VenueDetail"}, "examples": {"default": {"$ref": "#/components/examples/VenueDetail"}}}}
          },
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "404": {"$ref": "#/components/responses/NotFound"},
          "429": {"$ref": "#/components/responses/TooManyRequests"}
        }
      }
    },
    "/slots": {
      "get": {
        "tags": ["Slots"],
        "summary": "List slots",
        "operationId": "listSlots",
        "description": "Slot rows for one venue over a date range: the end state of every product-resource-window the venue offered. On the Free tier the range is clamped to the frozen sample day. Rows are ordered by `start`, then `resource_id`, then `product_id`.",
        "parameters": [
          {"name": "venue", "in": "query", "required": true, "description": "Venue id.", "schema": {"type": "string", "examples": ["ven_bowlero-times-square"]}},
          {"name": "from", "in": "query", "required": true, "description": "First local service date, inclusive.", "schema": {"type": "string", "format": "date"}},
          {"name": "to", "in": "query", "required": true, "description": "Last local service date, inclusive. At most 92 days after `from`.", "schema": {"type": "string", "format": "date"}},
          {"name": "product", "in": "query", "description": "Keep slots of one product.", "schema": {"type": "string"}},
          {"name": "resource", "in": "query", "description": "Keep slots on one resource.", "schema": {"type": "string"}},
          {"name": "status", "in": "query", "description": "Keep slots in one status.", "schema": {"$ref": "#/components/schemas/SlotStatus"}},
          {"$ref": "#/components/parameters/Limit"},
          {"$ref": "#/components/parameters/Cursor"}
        ],
        "responses": {
          "200": {
            "description": "A page of slots.",
            "content": {
              "application/json": {"schema": {"$ref": "#/components/schemas/SlotPage"}, "examples": {"default": {"$ref": "#/components/examples/SlotPage"}}},
              "text/csv": {"schema": {"type": "string"}, "example": "slot_id,venue_id,product_id,resource_id,start,end,date,capacity,booked,booked_kind,booked_low,booked_high,price_minor,currency,price_evidence,status\nslt_01J9Z1…,ven_bowlero-times-square,prd_lane-60-standard,res_lane-07,2026-10-24T23:00:00Z,2026-10-25T00:00:00Z,2026-10-24,1,1,count,,,6900,USD,quoted,sold_out\n"}
            }
          },
          "400": {"$ref": "#/components/responses/BadRequest"},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "403": {"$ref": "#/components/responses/Forbidden"},
          "404": {"$ref": "#/components/responses/NotFound"},
          "429": {"$ref": "#/components/responses/TooManyRequests"}
        }
      }
    },
    "/metrics": {
      "get": {
        "tags": ["Metrics"],
        "summary": "Roll slots up",
        "operationId": "getMetrics",
        "description": "Sell-through, utilization and revenue computed from slot rows, grouped the way you ask. Revenue is an interval: `low` prices only sold capacity at proven prices, `mid` is the central estimate, `high` the ceiling. Grouping by `day` returns one row per local service date within the filter; every other grouping returns one row per group over the whole range.",
        "parameters": [
          {"name": "group_by", "in": "query", "required": true, "description": "What one row is.", "schema": {"type": "string", "enum": ["market", "brand", "venue", "venue_type", "activity", "day"]}},
          {"name": "from", "in": "query", "required": true, "schema": {"type": "string", "format": "date"}},
          {"name": "to", "in": "query", "required": true, "schema": {"type": "string", "format": "date"}},
          {"$ref": "#/components/parameters/Market"},
          {"$ref": "#/components/parameters/Brand"},
          {"$ref": "#/components/parameters/VenueType"},
          {"$ref": "#/components/parameters/Activity"},
          {"name": "venue", "in": "query", "description": "Restrict to one venue (required when `group_by=day` on the Free tier).", "schema": {"type": "string"}},
          {"$ref": "#/components/parameters/Limit"},
          {"$ref": "#/components/parameters/Cursor"}
        ],
        "responses": {
          "200": {
            "description": "A page of metric rows.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MetricsPage"}, "examples": {"default": {"$ref": "#/components/examples/MetricsPage"}}}}
          },
          "400": {"$ref": "#/components/responses/BadRequest"},
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "429": {"$ref": "#/components/responses/TooManyRequests"}
        }
      }
    },
    "/observations": {
      "get": {
        "tags": ["Observations"],
        "summary": "Get a slot's booking curve",
        "operationId": "listObservations",
        "description": "Every observation of one slot, oldest first: when we looked, how many were left and what price was shown. Data tier only. The first observation's `observed_at` is where the curve starts; nothing before it is inferred.",
        "security": [{"BearerKey": []}],
        "parameters": [
          {"name": "slot", "in": "query", "required": true, "description": "Slot id.", "schema": {"type": "string"}},
          {"$ref": "#/components/parameters/Limit"},
          {"$ref": "#/components/parameters/Cursor"}
        ],
        "responses": {
          "200": {
            "description": "A page of observations.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ObservationPage"}, "examples": {"default": {"$ref": "#/components/examples/ObservationPage"}}}}
          },
          "401": {"$ref": "#/components/responses/Unauthorized"},
          "403": {"$ref": "#/components/responses/Forbidden"},
          "404": {"$ref": "#/components/responses/NotFound"},
          "429": {"$ref": "#/components/responses/TooManyRequests"}
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key issued with a Pro or Data subscription, sent as `Authorization: Bearer <key>`. Requests without a key see the Free tier: the frozen sample day only."
      }
    },
    "parameters": {
      "VenueId": {"name": "venue_id", "in": "path", "required": true, "schema": {"type": "string"}, "example": "ven_bowlero-times-square"},
      "Market": {"name": "market", "in": "query", "description": "Market id.", "schema": {"type": "string"}, "example": "mkt_new-york"},
      "Brand": {"name": "brand", "in": "query", "description": "Brand slug, exact match.", "schema": {"type": "string"}, "example": "bowlero"},
      "VenueType": {"name": "venue_type", "in": "query", "description": "Keep venues of one venue type.", "schema": {"$ref": "#/components/schemas/VenueType"}},
      "Activity": {"name": "activity", "in": "query", "description": "Keep venues that sell at least one product with this activity.", "schema": {"$ref": "#/components/schemas/Activity"}},
      "Limit": {"name": "limit", "in": "query", "description": "Rows per page.", "schema": {"type": "integer", "minimum": 1, "maximum": 1000, "default": 200}},
      "Cursor": {"name": "cursor", "in": "query", "description": "Opaque cursor from the previous page's `next_cursor`.", "schema": {"type": "string"}}
    },
    "responses": {
      "BadRequest": {"description": "A parameter is missing or malformed.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "bad_request", "message": "to must be at most 92 days after from"}}}},
      "Unauthorized": {"description": "The key is missing, expired or revoked.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unauthorized", "message": "Unknown API key"}}}},
      "Forbidden": {"description": "The key's tier does not include this endpoint or range.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "forbidden", "message": "Observations require the Data tier", "tier": "pro"}}}},
      "NotFound": {"description": "No such venue or slot.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not_found", "message": "No venue ven_example"}}}},
      "TooManyRequests": {"description": "Rate limit reached. `Retry-After` says when to try again.", "headers": {"Retry-After": {"schema": {"type": "integer"}, "description": "Seconds to wait."}}, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate_limited", "message": "600 requests per minute"}}}}
    },
    "schemas": {
      "VenueType": {
        "type": "string",
        "description": "The kind of business a venue is. One value per venue, set from the venue's own description, never inferred from its products. The list is append-only.",
        "enum": ["family_entertainment_center", "bowling_center", "trampoline_park", "adventure_park", "golf_entertainment", "golf_sim_studio", "bathhouse_spa", "sauna_studio", "recovery_studio", "fitness_studio", "racquet_club", "sports_facility", "campground", "rec_center", "other"]
      },
      "Activity": {
        "type": "string",
        "description": "What a product lets you do. Open vocabulary, lower snake case, documented values below; new values are added as products appear.",
        "examples": ["lanes", "bays", "courts", "jump", "party", "sauna", "cold_plunge", "bathhouse", "class", "recovery", "cages", "climb", "laser_tag", "arcade", "campsite"]
      },
      "InventoryGrain": {
        "type": "string",
        "description": "How finely the venue's inventory is exposed, derived from its resources on every build (like `activities`, never set by hand). `resource_slot`: every resource is a unit (lane, court, bay) with its own slots. `pooled_slot`: every resource is a shared capacity per window (open jump, a class). `mixed`: the venue has both. `none`: the venue is under coverage but exposes no bookable inventory, so it has products and prices only.",
        "enum": ["resource_slot", "pooled_slot", "mixed", "none"]
      },
      "BookedKind": {
        "type": ["string", "null"],
        "description": "How `booked` was measured. `count`: the venue's public listing showed sold or remaining as a number. `bounds`: only a floor and ceiling are provable (`booked_low`, `booked_high`); `booked` is their midpoint. `null`: the slot was never observed before it started.",
        "enum": ["count", "bounds", null]
      },
      "PriceEvidence": {
        "type": ["string", "null"],
        "description": "What the price is evidence of. `quoted`: shown for this slot at the time. `list`: the venue's published rate card for this product. `researched`: researched and dated by Fron Research. `null`: no price.",
        "enum": ["quoted", "list", "researched", null]
      },
      "SlotStatus": {
        "type": "string",
        "description": "Final state of a slot once its start has passed. `sold_out`: booked reached capacity. `open`: ended with inventory left. `closed`: withdrawn before start (venue closed, slot removed). `unknown`: never observed in a way that proves any of the above.",
        "enum": ["sold_out", "open", "closed", "unknown"]
      },
      "Money": {
        "type": "object",
        "required": ["minor", "currency"],
        "properties": {
          "minor": {"type": "integer", "description": "Amount in minor units (cents)."},
          "currency": {"type": "string", "description": "ISO 4217.", "examples": ["USD"]}
        }
      },
      "Coverage": {
        "type": "object",
        "description": "How far back this venue's record goes and what it can prove. Read it before comparing venues.",
        "required": ["slots_from", "observations_from", "price_evidence", "booked_kinds"],
        "properties": {
          "slots_from": {"type": ["string", "null"], "format": "date", "description": "Earliest local service date with a real slot row (template or recomputed pasts excluded)."},
          "slots_to": {"type": ["string", "null"], "format": "date", "description": "Latest service date with a settled slot row."},
          "observations_from": {"type": ["string", "null"], "format": "date-time", "description": "Earliest observation; booking curves and lead times exist from here."},
          "price_evidence": {"type": "object", "description": "Share of slot rows by price evidence over the whole record.", "properties": {"quoted": {"type": "number"}, "list": {"type": "number"}, "researched": {"type": "number"}, "none": {"type": "number"}}},
          "booked_kinds": {"type": "object", "description": "Share of slot rows by booked kind.", "properties": {"count": {"type": "number"}, "bounds": {"type": "number"}, "unknown": {"type": "number"}}}
        }
      },
      "Market": {
        "type": "object",
        "required": ["market_id", "name", "country", "venues"],
        "properties": {
          "market_id": {"type": "string", "examples": ["mkt_new-york"]},
          "name": {"type": "string", "examples": ["New York"]},
          "country": {"type": "string", "description": "ISO 3166-1 alpha-2.", "examples": ["US"]},
          "region": {"type": ["string", "null"], "description": "State or province code.", "examples": ["NY"]},
          "timezone": {"type": "string", "description": "IANA zone used for local dates in this market.", "examples": ["America/New_York"]},
          "centroid": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}},
          "venues": {"type": "integer", "description": "Venues under coverage in this market."},
          "venues_by_type": {"type": "object", "additionalProperties": {"type": "integer"}, "description": "Venue counts keyed by venue type."},
          "activities": {"type": "array", "items": {"$ref": "#/components/schemas/Activity"}, "description": "Distinct activities sold in this market."}
        }
      },
      "Venue": {
        "type": "object",
        "required": ["venue_id", "name", "brand", "market_id", "venue_type", "platform", "inventory_grain", "activities", "coverage"],
        "properties": {
          "venue_id": {"type": "string", "examples": ["ven_bowlero-times-square"]},
          "name": {"type": "string", "examples": ["Bowlero Times Square"]},
          "brand": {"type": ["string", "null"], "description": "Brand slug when the venue belongs to a chain, else null.", "examples": ["bowlero"]},
          "market_id": {"type": "string"},
          "venue_type": {"$ref": "#/components/schemas/VenueType"},
          "platform": {"type": "string", "description": "Opaque source identifier, stable per venue.", "examples": ["platform-a", "platform-b"]},
          "inventory_grain": {"$ref": "#/components/schemas/InventoryGrain"},
          "activities": {"type": "array", "items": {"$ref": "#/components/schemas/Activity"}, "description": "Derived: the distinct activities of this venue's products."},
          "address": {"type": ["string", "null"]},
          "geo": {"type": "object", "properties": {"lat": {"type": "number"}, "lon": {"type": "number"}}},
          "timezone": {"type": "string"},
          "coverage": {"$ref": "#/components/schemas/Coverage"}
        }
      },
      "Product": {
        "type": "object",
        "description": "One thing a venue sells. A product has exactly one activity.",
        "required": ["product_id", "venue_id", "name", "activity", "unit", "duration_minutes", "party_size"],
        "properties": {
          "product_id": {"type": "string", "examples": ["prd_lane-60-standard"]},
          "venue_id": {"type": "string"},
          "name": {"type": "string", "examples": ["Lane, 60 minutes, standard"]},
          "activity": {"$ref": "#/components/schemas/Activity"},
          "unit": {"type": "string", "description": "What one booking buys.", "enum": ["lane", "court", "bay", "jumper", "seat", "session", "room", "party", "campsite", "cage", "other"]},
          "duration_minutes": {"type": ["integer", "null"], "description": "Null when the venue sells an open-ended booking."},
          "party_size": {"type": "object", "description": "Guests one booking covers.", "properties": {"min": {"type": ["integer", "null"]}, "max": {"type": ["integer", "null"]}}},
          "list_price": {"oneOf": [{"$ref": "#/components/schemas/Money"}, {"type": "null"}], "description": "Current published rate for one unit of the product (one lane, bay or court for its duration, or one ticket where bookings count tickets), or null. Never per person: the same per-unit rule as Slot.price."},
          "list_price_evidence": {"$ref": "#/components/schemas/PriceEvidence"},
          "active": {"type": "boolean", "description": "False once the venue stops offering the product; its slot history stays."}
        }
      },
      "Resource": {
        "type": "object",
        "description": "What products are booked against.",
        "required": ["resource_id", "venue_id", "kind", "name"],
        "properties": {
          "resource_id": {"type": "string", "examples": ["res_lane-07"]},
          "venue_id": {"type": "string"},
          "kind": {"type": "string", "description": "`unit`: one named thing (lane 7). `pool`: a shared capacity (open jump).", "enum": ["unit", "pool"]},
          "name": {"type": "string", "examples": ["Lane 7"]},
          "capacity": {"type": ["integer", "null"], "description": "Units a pool holds per window; 1 for a unit."}
        }
      },
      "VenueDetail": {
        "allOf": [
          {"$ref": "#/components/schemas/Venue"},
          {"type": "object", "required": ["products", "resources"], "properties": {
            "products": {"type": "array", "items": {"$ref": "#/components/schemas/Product"}},
            "resources": {"type": "array", "items": {"$ref": "#/components/schemas/Resource"}}
          }}
        ]
      },
      "Slot": {
        "type": "object",
        "description": "One product on one resource over one window. The fact table every rollup is computed from.",
        "required": ["slot_id", "venue_id", "product_id", "resource_id", "start", "end", "date", "capacity", "booked", "booked_kind", "price", "price_evidence", "status"],
        "properties": {
          "slot_id": {"type": "string"},
          "venue_id": {"type": "string"},
          "product_id": {"type": "string"},
          "resource_id": {"type": "string"},
          "start": {"type": "string", "format": "date-time", "description": "UTC."},
          "end": {"type": "string", "format": "date-time", "description": "UTC."},
          "date": {"type": "string", "format": "date", "description": "Local service date in the venue's timezone."},
          "capacity": {"type": ["integer", "null"], "description": "Units offered in this window. 1 for a unit resource."},
          "booked": {"type": ["integer", "null"], "description": "Units sold by start. Midpoint when `booked_kind` is `bounds`; null when unknown."},
          "booked_kind": {"$ref": "#/components/schemas/BookedKind"},
          "booked_low": {"type": ["integer", "null"], "description": "Proven floor, present when `booked_kind` is `bounds`."},
          "booked_high": {"type": ["integer", "null"], "description": "Proven ceiling, present when `booked_kind` is `bounds`."},
          "price": {"oneOf": [{"$ref": "#/components/schemas/Money"}, {"type": "null"}], "description": "Total price of one unit of this slot (one lane, bay or court for the slot's duration, or one ticket where booked counts tickets). Never per person."},
          "price_evidence": {"$ref": "#/components/schemas/PriceEvidence"},
          "revenue": {
            "oneOf": [
              {"type": "object", "required": ["low", "mid", "high", "currency"], "properties": {
                "low": {"type": "integer", "description": "Minor units."},
                "mid": {"type": "integer", "description": "Minor units."},
                "high": {"type": "integer", "description": "Minor units."},
                "currency": {"type": "string", "description": "ISO 4217."}
              }},
              {"type": "null"}
            ],
            "description": "booked × price. `low` and `high` use `booked_low` and `booked_high` when `booked_kind` is `bounds`; on a pool slot whose products are priced differently they use the cheapest and dearest product price (see `revenue_basis`). Null when `booked` or `price` is null."
          },
          "revenue_basis": {
            "type": ["string", "null"],
            "enum": ["exact", "pool_price_range", null],
            "description": "How `revenue` was computed. `exact`: this slot's own booked × its own price. `pool_price_range`: several products draw from one pool and the venue shows only the pool's remaining capacity, so the pool-level slot (product_id `pool:<resource_id>`) carries capacity and booked once and its revenue spans booked × the cheapest to the dearest product price at that start (mid = the median); the product slots keep their price and no counts. Null when `revenue` is null."
          },
          "status": {"$ref": "#/components/schemas/SlotStatus"},
          "first_observed_at": {"type": ["string", "null"], "format": "date-time"},
          "last_observed_at": {"type": ["string", "null"], "format": "date-time"},
          "sold_out_at": {"type": ["string", "null"], "format": "date-time", "description": "First observation at which remaining reached 0, when a curve exists."}
        }
      },
      "Observation": {
        "type": "object",
        "description": "One look at one slot.",
        "required": ["slot_id", "observed_at", "remaining"],
        "properties": {
          "slot_id": {"type": "string"},
          "observed_at": {"type": "string", "format": "date-time"},
          "remaining": {"type": ["integer", "null"], "description": "Units still bookable at `observed_at`; null when the public listing showed the slot without a count."},
          "price": {"oneOf": [{"$ref": "#/components/schemas/Money"}, {"type": "null"}], "description": "Price shown at `observed_at`, when the public listing shows one per slot."},
          "evidence": {"type": ["string", "null"], "description": "Opaque reference to the stored observation the row came from. Data tier only.", "examples": ["obs/example"]}
        }
      },
      "MetricsRow": {
        "type": "object",
        "description": "A rollup of slot rows over the range for one group.",
        "required": ["group", "slots", "capacity", "booked", "sell_through", "revenue"],
        "properties": {
          "group": {"type": "object", "description": "The key of this row: `market_id`, `brand`, `venue_id`, `venue_type`, `activity` or `date`, matching `group_by`.", "additionalProperties": {"type": "string"}},
          "venues": {"type": "integer", "description": "Venues contributing."},
          "slots": {"type": "integer", "description": "Slot rows contributing."},
          "capacity": {"type": "integer", "description": "Sum of slot capacity."},
          "booked": {"type": "object", "required": ["low", "mid", "high"], "properties": {"low": {"type": "integer"}, "mid": {"type": "integer"}, "high": {"type": "integer"}}},
          "sell_through": {"type": "object", "description": "booked / capacity.", "required": ["low", "mid", "high"], "properties": {"low": {"type": "number"}, "mid": {"type": "number"}, "high": {"type": "number"}}},
          "revenue": {"type": "object", "description": "Sum of booked × price, as an interval. `priced_share` is the share of booked units that had a price of any evidence.", "required": ["low", "mid", "high", "currency", "priced_share"], "properties": {"low": {"type": "integer"}, "mid": {"type": "integer"}, "high": {"type": "integer"}, "currency": {"type": "string"}, "priced_share": {"type": "number"}}},
          "booked_kinds": {"type": "object", "properties": {"count": {"type": "number"}, "bounds": {"type": "number"}, "unknown": {"type": "number"}}, "description": "Share of slot rows by booked kind, so you can see how much of the interval is measured and how much is bounded."}
        }
      },
      "Page": {
        "type": "object",
        "required": ["next_cursor"],
        "properties": {"next_cursor": {"type": ["string", "null"], "description": "Pass as `cursor` for the next page; null on the last page."}, "tier": {"type": "string", "enum": ["free", "pro", "data"], "description": "Tier the response was served under."}, "as_of": {"type": "string", "format": "date-time", "description": "When the serving index was last rebuilt from raw."}}
      },
      "MarketPage": {"allOf": [{"$ref": "#/components/schemas/Page"}, {"type": "object", "required": ["data"], "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/Market"}}}}]},
      "VenuePage": {"allOf": [{"$ref": "#/components/schemas/Page"}, {"type": "object", "required": ["data"], "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/Venue"}}}}]},
      "SlotPage": {"allOf": [{"$ref": "#/components/schemas/Page"}, {"type": "object", "required": ["data"], "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/Slot"}}}}]},
      "MetricsPage": {"allOf": [{"$ref": "#/components/schemas/Page"}, {"type": "object", "required": ["data"], "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/MetricsRow"}}}}]},
      "ObservationPage": {"allOf": [{"$ref": "#/components/schemas/Page"}, {"type": "object", "required": ["data"], "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/Observation"}}}}]},
      "Error": {
        "type": "object",
        "required": ["error", "message"],
        "properties": {"error": {"type": "string", "enum": ["bad_request", "unauthorized", "forbidden", "not_found", "rate_limited"]}, "message": {"type": "string"}, "tier": {"type": "string"}}
      }
    },
    "examples": {
      "MarketPage": {
        "summary": "Two markets",
        "value": {
          "data": [
            {"market_id": "mkt_new-york", "name": "New York", "country": "US", "region": "NY", "timezone": "America/New_York", "centroid": {"lat": 40.7128, "lon": -74.006}, "venues": 61, "venues_by_type": {"bathhouse_spa": 14, "sauna_studio": 9, "recovery_studio": 7, "bowling_center": 12, "trampoline_park": 6, "golf_sim_studio": 8, "racquet_club": 5}, "activities": ["sauna", "cold_plunge", "bathhouse", "recovery", "lanes", "jump", "party", "bays", "courts"]},
            {"market_id": "mkt_chicago", "name": "Chicago", "country": "US", "region": "IL", "timezone": "America/Chicago", "centroid": {"lat": 41.8781, "lon": -87.6298}, "venues": 23, "venues_by_type": {"bowling_center": 9, "trampoline_park": 8, "golf_sim_studio": 4, "racquet_club": 2}, "activities": ["lanes", "jump", "party", "bays", "courts"]}
          ],
          "next_cursor": null,
          "tier": "free",
          "as_of": "2026-11-08T06:00:00Z"
        }
      },
      "VenuePage": {
        "summary": "A granular venue and a pooled venue",
        "value": {
          "data": [
            {"venue_id": "ven_bowlero-times-square", "name": "Bowlero Times Square", "brand": "bowlero", "market_id": "mkt_new-york", "venue_type": "bowling_center", "platform": "platform-a", "inventory_grain": "resource_slot", "activities": ["lanes"], "address": "222 W 44th St, New York, NY 10036", "geo": {"lat": 40.7579, "lon": -73.9881}, "timezone": "America/New_York", "coverage": {"slots_from": "2026-10-04", "slots_to": "2026-11-07", "observations_from": "2026-10-04T13:00:12Z", "price_evidence": {"quoted": 0.96, "list": 0.04, "researched": 0, "none": 0}, "booked_kinds": {"count": 0.98, "bounds": 0, "unknown": 0.02}}},
            {"venue_id": "ven-sky-zone-westbury", "name": "Sky Zone Westbury", "brand": "sky-zone", "market_id": "mkt_new-york", "venue_type": "trampoline_park", "platform": "platform-a", "inventory_grain": "pooled_slot", "activities": ["jump", "party"], "address": "1 Zeckendorf Blvd, Westbury, NY 11590", "geo": {"lat": 40.7417, "lon": -73.5881}, "timezone": "America/New_York", "coverage": {"slots_from": "2026-10-11", "slots_to": "2026-11-07", "observations_from": "2026-10-11T15:00:41Z", "price_evidence": {"quoted": 0.07, "list": 0.93, "researched": 0, "none": 0}, "booked_kinds": {"count": 1, "bounds": 0, "unknown": 0}}}
          ],
          "next_cursor": "eyJ2IjoidmVuLXNreS16b25lLXdlc3RidXJ5In0",
          "tier": "pro",
          "as_of": "2026-11-08T06:00:00Z"
        }
      },
      "VenueDetail": {
        "summary": "A bowling center with two products on eight lanes",
        "value": {
          "venue_id": "ven_bowlero-times-square", "name": "Bowlero Times Square", "brand": "bowlero", "market_id": "mkt_new-york", "venue_type": "bowling_center", "platform": "platform-a", "inventory_grain": "resource_slot", "activities": ["lanes"], "address": "222 W 44th St, New York, NY 10036", "geo": {"lat": 40.7579, "lon": -73.9881}, "timezone": "America/New_York",
          "coverage": {"slots_from": "2026-10-04", "slots_to": "2026-11-07", "observations_from": "2026-10-04T13:00:12Z", "price_evidence": {"quoted": 0.96, "list": 0.04, "researched": 0, "none": 0}, "booked_kinds": {"count": 0.98, "bounds": 0, "unknown": 0.02}},
          "products": [
            {"product_id": "prd_lane-60-standard", "venue_id": "ven_bowlero-times-square", "name": "Lane, 60 minutes, standard", "activity": "lanes", "unit": "lane", "duration_minutes": 60, "party_size": {"min": 1, "max": 6}, "list_price": {"minor": 6900, "currency": "USD"}, "list_price_evidence": "list", "active": true},
            {"product_id": "prd_lane-60-prime", "venue_id": "ven_bowlero-times-square", "name": "Lane, 60 minutes, prime time", "activity": "lanes", "unit": "lane", "duration_minutes": 60, "party_size": {"min": 1, "max": 6}, "list_price": {"minor": 8900, "currency": "USD"}, "list_price_evidence": "list", "active": true}
          ],
          "resources": [
            {"resource_id": "res_lane-01", "venue_id": "ven_bowlero-times-square", "kind": "unit", "name": "Lane 1", "capacity": 1},
            {"resource_id": "res_lane-07", "venue_id": "ven_bowlero-times-square", "kind": "unit", "name": "Lane 7", "capacity": 1}
          ]
        }
      },
      "SlotPage": {
        "summary": "A granular slot and a pooled slot",
        "value": {
          "data": [
            {"slot_id": "slt_01JB2W6N8Q0K4T7Y3F9ZV1XH5D", "venue_id": "ven_bowlero-times-square", "product_id": "prd_lane-60-prime", "resource_id": "res_lane-07", "start": "2026-10-24T23:00:00Z", "end": "2026-10-25T00:00:00Z", "date": "2026-10-24", "capacity": 1, "booked": 1, "booked_kind": "count", "price": {"minor": 8900, "currency": "USD"}, "price_evidence": "quoted", "revenue": {"low": 8900, "mid": 8900, "high": 8900, "currency": "USD"}, "revenue_basis": "exact", "status": "sold_out", "first_observed_at": "2026-10-17T13:05:10Z", "last_observed_at": "2026-10-24T22:55:03Z", "sold_out_at": "2026-10-23T18:25:44Z"},
            {"slot_id": "slt_01JB2W6PQA9R2M4C8E1GHT6KZX", "venue_id": "ven-sky-zone-westbury", "product_id": "prd_open-jump-60", "resource_id": "res_jump-floor", "start": "2026-10-24T18:00:00Z", "end": "2026-10-24T19:00:00Z", "date": "2026-10-24", "capacity": 120, "booked": 84, "booked_kind": "count", "price": {"minor": 2499, "currency": "USD"}, "price_evidence": "list", "revenue": {"low": 209916, "mid": 209916, "high": 209916, "currency": "USD"}, "revenue_basis": "exact", "status": "open", "first_observed_at": "2026-10-11T15:00:41Z", "last_observed_at": "2026-10-24T17:58:09Z", "sold_out_at": null}
          ],
          "next_cursor": null,
          "tier": "pro",
          "as_of": "2026-11-08T06:00:00Z"
        }
      },
      "MetricsPage": {
        "summary": "Bowling by brand in New York over October 2026",
        "value": {
          "data": [
            {"group": {"brand": "bowlero"}, "venues": 7, "slots": 41820, "capacity": 41820, "booked": {"low": 17930, "mid": 18110, "high": 18290}, "sell_through": {"low": 0.429, "mid": 0.433, "high": 0.437}, "revenue": {"low": 136418000, "mid": 139120500, "high": 141804200, "currency": "USD", "priced_share": 0.97}, "booked_kinds": {"count": 0.98, "bounds": 0.01, "unknown": 0.01}},
            {"group": {"brand": "lucky-strike"}, "venues": 2, "slots": 9640, "capacity": 9640, "booked": {"low": 3610, "mid": 3702, "high": 3794}, "sell_through": {"low": 0.374, "mid": 0.384, "high": 0.394}, "revenue": {"low": 25270000, "mid": 26012100, "high": 26754200, "currency": "USD", "priced_share": 0.95}, "booked_kinds": {"count": 0.96, "bounds": 0.02, "unknown": 0.02}}
          ],
          "next_cursor": null,
          "tier": "pro",
          "as_of": "2026-11-08T06:00:00Z"
        }
      },
      "ObservationPage": {
        "summary": "The curve of one lane slot",
        "value": {
          "data": [
            {"slot_id": "slt_01JB2W6N8Q0K4T7Y3F9ZV1XH5D", "observed_at": "2026-10-17T13:05:10Z", "remaining": 1, "price": {"minor": 8900, "currency": "USD"}, "evidence": "obs/example"},
            {"slot_id": "slt_01JB2W6N8Q0K4T7Y3F9ZV1XH5D", "observed_at": "2026-10-23T18:15:31Z", "remaining": 1, "price": {"minor": 8900, "currency": "USD"}, "evidence": "obs/example"},
            {"slot_id": "slt_01JB2W6N8Q0K4T7Y3F9ZV1XH5D", "observed_at": "2026-10-23T18:25:44Z", "remaining": 0, "price": null, "evidence": "obs/example"}
          ],
          "next_cursor": null,
          "tier": "data",
          "as_of": "2026-11-08T06:00:00Z"
        }
      }
    }
  }
}
