openapi: 3.1.0 info: title: Airbtics Public API description: | HTTP surface for the Public API behind API Gateway. All operations require a valid API key. **Authoritative behavior** is implemented in the Python handlers under `apps/public_api/` (AWS Lambda Powertools `APIGatewayRestResolver`). This document is for clients and tooling; when in doubt, refer to the handler and helper modules and markdown readmes in this folder. **Market resolution** (metrics and future pacing): supply `market_id`, or both `search_latitude` and `search_longitude` (floats). Invalid or missing combinations return 400 where noted. **Agent/tooling note**: call concrete paths (for example `/markets/search`) rather than the API root. The API root often returns `403` because no root route is exposed and all operations require `x-api-key`. **Quick example**: `curl -X GET "https://crap0y5bx5.execute-api.us-east-2.amazonaws.com/prod/markets/search?query=london" -H "x-api-key: " -H "Accept: application/json"` **Market metrics filters**: **Preferred** — `POST /markets/summary` and `POST /markets/metrics/{proxy}` with JSON body field `filters` (object). **Deprecated** — same paths as `GET` with `filters` as a **URL-encoded JSON** query param; see `apps/public_api/markets/readme.md` for allowed keys. version: 1.0.0 servers: - url: https://crap0y5bx5.execute-api.us-east-2.amazonaws.com/prod description: Production API Gateway base URL. security: - ApiKeyAuth: [] tags: - name: Markets description: Market search, lookup, metadata, and metrics - name: Listings description: Listing search and listing-level metrics - name: Reports description: Create and fetch reports - name: Search description: Search ranking utilities - name: Live description: Live listing snapshot paths: /markets/search: get: tags: [Markets] operationId: getMarketsSearch summary: Autocomplete market search description: Substring search; up to 50 results. Optional `country_code` for tie-breaking. parameters: - name: query in: query required: true schema: type: string - name: country_code in: query required: false schema: type: string description: ISO-style country code hint (e.g. GB) responses: "200": description: Search results content: application/json: schema: $ref: "#/components/schemas/MarketSearchResponse" "400": description: Missing or invalid query content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /markets/country: get: tags: [Markets] operationId: getMarketsCountry summary: List markets by country (paginated) description: > Returns up to 10 markets per page for a given ISO-style country code, using the same cached S3 list as `/markets/search`. Stable sort by market id then name. parameters: - name: country_code in: query required: true schema: type: string minLength: 2 maxLength: 2 pattern: "^[A-Za-z]{2}$" description: Two-letter country code (e.g. GB, US) - name: page in: query required: false schema: type: integer minimum: 1 default: 1 responses: "200": description: Paginated markets for the country content: application/json: schema: $ref: "#/components/schemas/MarketsByCountryResponse" "400": description: Missing or invalid country_code or page content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /markets/lookup: get: tags: [Markets] operationId: getMarketsLookup summary: Resolve nearest market from coordinates description: Returns the nearest market among verified tiers T1–T3, or 404 if none. parameters: - name: latitude in: query required: true schema: type: string description: Latitude (string accepted by handler; numeric) - name: longitude in: query required: true schema: type: string responses: "200": description: Nearest market content: application/json: schema: $ref: "#/components/schemas/MessageObjectResponse" "400": description: Missing coordinates content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "404": description: No eligible market content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /markets/metadata: get: tags: [Markets] operationId: getMarketsMetadata summary: Market metadata and public submarkets description: > Parent market row plus submarkets. Parent and each submarket must be `visibility=public` and pass hidden-market rules; otherwise 404. parameters: - name: market_id in: query required: true schema: type: integer minimum: 1 responses: "200": description: Metadata payload content: application/json: schema: $ref: "#/components/schemas/MarketMetadataResponse" "400": description: Missing or invalid market_id content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "404": description: Market not found or not eligible content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /markets/summary: post: tags: [Markets] operationId: postMarketsSummary summary: Market summary KPIs (preferred) description: > Same response as GET. JSON body: `market_id` or `search_latitude` + `search_longitude`, optional `filters` object. Do not send top-level `bedrooms` — use `filters.bedrooms`. Filter surcharge uses non-empty `filters` in the body (see `authorizePublicApiUser`). requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MarketMetricsPostRequest" responses: "200": description: Summary, metadata, optional market_info when resolved by coordinates content: application/json: schema: $ref: "#/components/schemas/MarketMetricsEnvelope" "400": description: Missing market, invalid body, invalid filters, etc. content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" get: tags: [Markets] operationId: getMarketsSummaryDeprecated summary: Market summary KPIs (deprecated — use POST) deprecated: true parameters: - $ref: "#/components/parameters/MarketIdQuery" - $ref: "#/components/parameters/SearchLatitudeQuery" - $ref: "#/components/parameters/SearchLongitudeQuery" - name: bedrooms in: query required: false schema: type: string description: Legacy single-select; superseded by `filters` when present. - $ref: "#/components/parameters/FiltersQueryJson" responses: "200": description: Summary, metadata, optional market_info when resolved by coordinates content: application/json: schema: $ref: "#/components/schemas/MarketMetricsEnvelope" "400": description: Missing market, invalid filters, etc. content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /markets/metrics/{proxy}: post: tags: [Markets] operationId: postMarketsMetricsByProxy summary: Market time-series metrics (preferred) description: > Same response as GET. JSON body includes `proxy` path segment implicitly; body fields `market_id` or coordinates, optional `number_of_months` (1–36, default 12), optional `filters` object. Do not send top-level `bedrooms`. Filter surcharge uses non-empty `filters` in the body. parameters: - name: proxy in: path required: true schema: type: string enum: - all - occupancy - average-daily-rate - revenue - active-listings requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/MarketMetricsPostRequest" responses: "200": description: Metrics payload content: application/json: schema: $ref: "#/components/schemas/MarketMetricsEnvelope" "400": description: Invalid metric, months, filters, or missing market/coordinates content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" get: tags: [Markets] operationId: getMarketsMetricsByProxyDeprecated summary: Market time-series metrics (deprecated — use POST) deprecated: true description: Monthly metrics for the requested slice (`proxy`). parameters: - name: proxy in: path required: true schema: type: string enum: - all - occupancy - average-daily-rate - revenue - active-listings - $ref: "#/components/parameters/MarketIdQuery" - $ref: "#/components/parameters/SearchLatitudeQuery" - $ref: "#/components/parameters/SearchLongitudeQuery" - name: number_of_months in: query required: false schema: type: integer minimum: 1 maximum: 36 default: 12 - name: bedrooms in: query required: false schema: type: string - $ref: "#/components/parameters/FiltersQueryJson" responses: "200": description: Metrics payload content: application/json: schema: $ref: "#/components/schemas/MarketMetricsEnvelope" "400": description: Invalid metric, months, filters, or missing market/coordinates content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /markets/metrics/future/pacing: get: tags: [Markets] operationId: getMarketsMetricsFuturePacing summary: Future pacing metrics for a market parameters: - $ref: "#/components/parameters/MarketIdQuery" - $ref: "#/components/parameters/SearchLatitudeQuery" - $ref: "#/components/parameters/SearchLongitudeQuery" responses: "200": description: Future pacing data; may include `market_info` when resolved by coordinates content: application/json: schema: $ref: "#/components/schemas/MessageObjectResponse" "400": description: Missing market_id and coordinate pair content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "404": description: Market not found content: application/json: schema: $ref: "#/components/schemas/ErrorBodyErrorKey" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /listings/search/{proxy}: post: tags: [Listings] operationId: postListingsSearchByProxy summary: Search listings by bounds, market, or radius description: | Page size is fixed at 50. `page` defaults to 1. **Legacy response**: `message.listings` is a JSON **string** whose value is itself a JSON object `{"message":[...]}` (double-encoded for backward compatibility). parameters: - name: proxy in: path required: true schema: type: string enum: [bounds, market, radius] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ListingSearchRequest" responses: "200": description: Listings payload and total count content: application/json: schema: $ref: "#/components/schemas/ListingSearchResponse" "400": description: Invalid proxy, body, bounds, market_id, radius inputs, page, or filters content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /listings/metrics/{proxy}: get: tags: [Listings] operationId: getListingsMetricsByProxy summary: Metrics for a single listing parameters: - name: proxy in: path required: true schema: type: string enum: [all] - name: listing_id in: query required: true schema: type: string - name: granularity in: query required: false schema: type: string enum: [monthly, yearly] default: yearly responses: "200": description: Listing metrics content: application/json: schema: $ref: "#/components/schemas/MessageObjectResponse" "400": description: Missing listing_id, invalid metric or granularity content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /report/{proxy}: post: tags: [Reports] operationId: postReportByProxy summary: Create a report job parameters: - name: proxy in: path required: true schema: type: string enum: [all, summary] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateReportRequest" responses: "200": description: Report created content: application/json: schema: $ref: "#/components/schemas/CreateReportResponse" "400": description: Invalid report type or body content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /report: get: tags: [Reports] operationId: getReport summary: Fetch a report by id parameters: - name: id in: query required: true schema: type: string description: Report identifier returned from create. responses: "200": description: Formatted report (shape depends on report type) content: application/json: schema: $ref: "#/components/schemas/MessageObjectResponse" "400": description: Missing id content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /search/ranking: post: tags: [Search] operationId: postSearchRanking summary: Rank listings inside a polygon (Airbnb / Vrbo) description: > Forwards to the search-ranking worker. Requires valid `bounds`. Optional `site` defaults in downstream logic; optional `filters` and `country_code` are passed through. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SearchRankingRequest" responses: "200": description: Ranked listing ids content: application/json: schema: $ref: "#/components/schemas/SearchRankingResponse" "400": description: Missing or invalid bounds content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" /live/listing/search: get: tags: [Live] operationId: getLiveListingSearch summary: Live snapshot for a listing parameters: - name: listing_id in: query required: true schema: type: string responses: "200": description: Listing payload content: application/json: schema: $ref: "#/components/schemas/MessageObjectResponse" "400": description: Missing listing_id content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" "404": description: Listing not found content: application/json: schema: $ref: "#/components/schemas/ErrorBodyErrorKey" "500": description: Server error content: application/json: schema: $ref: "#/components/schemas/ErrorMessageBody" components: securitySchemes: ApiKeyAuth: type: apiKey in: header name: x-api-key description: Public API key (see `authorizePublicApiUser`). parameters: MarketIdQuery: name: market_id in: query required: false schema: type: integer description: Numeric market ID. Use this **or** search_latitude + search_longitude. SearchLatitudeQuery: name: search_latitude in: query required: false schema: type: number format: float SearchLongitudeQuery: name: search_longitude in: query required: false schema: type: number format: float FiltersQueryJson: name: filters in: query required: false schema: type: string description: > URL-encoded JSON object of listing filters for market metrics/summary. Invalid JSON or validation errors return 400 with a string `message`. See `apps/public_api/markets/readme.md` for allowed keys. **Billing (GET, deprecated)**: non-empty `filters` query or legacy `bedrooms` query doubles the price of `/markets/summary` and `/markets/metrics/` (same amounts as POST body `filters`). **POST** uses a non-empty JSON `filters` object in the request body for surcharge (see `authorizePublicApiUser`). `/markets/metrics/future/pacing` is not surcharged. schemas: MarketMetricsPostRequest: type: object description: > Request body for `POST /markets/summary` and `POST /markets/metrics/{proxy}`. Do not send top-level `bedrooms` — nest under `filters` (e.g. `{"filters":{"bedrooms":["1"]}}`). properties: market_id: type: integer description: Numeric market ID, or omit and use search_latitude + search_longitude. search_latitude: type: number format: float search_longitude: type: number format: float number_of_months: type: integer minimum: 1 maximum: 36 default: 12 description: Only used by metrics POST; ignored on summary POST. filters: type: object additionalProperties: true description: > Optional. Same keys as GET `filters` (see `apps/public_api/markets/readme.md`). When present and non-empty, uses the filtered metrics path and triggers surcharge. additionalProperties: true ErrorMessageBody: type: object properties: message: description: Human-readable error or structured error payload oneOf: - type: string - type: object additionalProperties: true hint: type: string description: > Optional. Present on some 400 responses when `filters` use keys from the wrong public API surface (e.g. `adr` on market metrics, or `prof_mgmt` on listings search). See `packages/utility/python/public_api_handler_failure.py`. ErrorBodyErrorKey: type: object properties: error: type: string MessageObjectResponse: type: object required: [message] properties: message: type: object additionalProperties: true description: Arbitrary JSON object; see handler helpers for field lists. MarketSearchResponse: type: object required: [message] properties: message: type: array items: $ref: "#/components/schemas/MarketSearchResultItem" MarketsByCountryResponse: type: object required: [message] properties: message: type: object required: [markets, page, page_size, total_count, has_more] properties: markets: type: array items: $ref: "#/components/schemas/MarketSearchResultItem" page: type: integer minimum: 1 page_size: type: integer enum: [10] total_count: type: integer minimum: 0 has_more: type: boolean MarketSearchResultItem: type: object description: Same shape as `/markets/search` result rows. properties: id: type: integer name: type: string region: type: string country: type: string country_code: type: string verified: type: boolean MarketMetadataResponse: type: object required: [message] properties: message: type: object required: [district, country_code, verified_tier, submarkets] properties: district: type: string country_code: type: string verified_tier: type: string submarkets: type: array items: type: object required: [market_id, district, country_code] properties: market_id: type: integer district: type: string country_code: type: string MarketMetricsEnvelope: type: object description: > Typical shape includes `message`, `metadata`, and optionally `market_info`. Exact KPI keys vary by endpoint; see `marketMetrics/helper.py` and markets readme. properties: message: type: object additionalProperties: true metadata: type: object additionalProperties: true market_info: type: object additionalProperties: true BoundsBox: type: object required: [ne_lat, ne_lng, sw_lat, sw_lng] properties: ne_lat: type: number ne_lng: type: number sw_lat: type: number sw_lng: type: number ListingFilters: type: object properties: property_type: type: array items: type: string description: Optional property-type filters. bedrooms: type: array items: oneOf: - type: integer - type: string description: Optional bedroom buckets (for example `2`, `3`, `6+`). bathrooms: type: array items: oneOf: - type: integer - type: string description: Optional bathroom buckets (for example `1`, `2`, `6+`). sleeps: type: object properties: min: type: number max: type: number additionalProperties: false minstay: type: object properties: min: type: number max: type: number additionalProperties: false rating: type: object properties: min: type: number max: type: number additionalProperties: false reviews: type: string description: Optional review-count threshold string such as `10+`. adr: type: object properties: min: type: number max: type: number additionalProperties: false revenue: type: object properties: min: type: number max: type: number additionalProperties: false additionalProperties: true description: > Optional filters object. Supported keys and semantics are documented in `apps/public_api/listings/listingSearch/readme.md`. Unknown keys are ignored by the handler. ListingSearchRequest: type: object description: > Fields used depend on `proxy` path segment. `filters` must be an object if present. properties: page: type: integer minimum: 1 default: 1 bounds: $ref: "#/components/schemas/BoundsBox" market_id: type: integer minimum: 1 center_lat: type: number center_lng: type: number distance_in_meters: type: integer minimum: 1 sort_by: type: - string - "null" description: Optional sort key; passed through when supported. filters: $ref: "#/components/schemas/ListingFilters" ListingSearchResponse: type: object required: [message] properties: message: type: object required: [listings, total_count] properties: listings: type: string description: > JSON string containing an object `{"message":[...]}` where the array is listing rows. total_count: type: integer minimum: 0 CreateReportRequest: type: object required: [latitude, longitude, bedrooms, bathrooms, accommodates] properties: latitude: type: number longitude: type: number bedrooms: type: integer bathrooms: type: integer accommodates: type: integer CreateReportResponse: type: object required: [message] properties: message: type: object required: [report_id] properties: report_id: type: string SearchRankingRequest: type: object required: [bounds] properties: bounds: $ref: "#/components/schemas/BoundsBox" site: type: string enum: [airbnb, vrbo] description: Optional; downstream defaults if omitted. filters: type: object additionalProperties: true description: Optional filter object validated by the worker. country_code: type: string description: Optional proxy / session hint. SearchRankingResponse: type: object required: [message] properties: message: type: array items: type: object additionalProperties: true description: Array of ranked rows (e.g. listing_id, rank, page).