{
  "openapi": "3.1.0",
  "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
  "info": {
    "title": "Play Map LA Public API",
    "version": "1.0.0",
    "description": "Read-only, anonymous JSON APIs for discovering Los Angeles family events and Play Map LA guides. No API key is required. Responses are rate limited by IP and may be cached; do not treat listings as a substitute for the organizer's official details.",
    "contact": {
      "name": "Play Map LA",
      "url": "https://playmapla.com/contact"
    },
    "license": {
      "name": "Play Map LA Terms",
      "url": "https://playmapla.com/terms"
    }
  },
  "servers": [
    {
      "url": "/",
      "description": "Current origin (production, preview, or local)"
    },
    {
      "url": "https://playmapla.com",
      "description": "Canonical production"
    }
  ],
  "security": [],
  "x-versioning": {
    "strategy": "path-major",
    "currentVersion": "v1",
    "policyUrl": "/developers#versioning",
    "deprecationHeader": "Deprecation",
    "sunsetHeader": "Sunset",
    "minimumSunsetNoticeDays": 180,
    "currentSunset": null
  },
  "externalDocs": {
    "description": "Human-readable developer quickstart",
    "url": "https://playmapla.com/developers"
  },
  "paths": {
    "/api/v1/llm-events": {
      "get": {
        "operationId": "listLlmEvents",
        "security": [],
        "summary": "List public family events",
        "description": "Returns the public event feed in an agent-friendly JSON shape. Use id for one event or combine search, location, category, and ISO date filters for a feed. Pagination is controlled by limit and offset. Anonymous requests are rate limited; the response includes cache and pagination metadata.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EventId"
          },
          {
            "$ref": "#/components/parameters/EventTitle"
          },
          {
            "$ref": "#/components/parameters/EventSearch"
          },
          {
            "$ref": "#/components/parameters/EventLocation"
          },
          {
            "$ref": "#/components/parameters/EventCategory"
          },
          {
            "$ref": "#/components/parameters/DateFrom"
          },
          {
            "$ref": "#/components/parameters/DateTo"
          },
          {
            "$ref": "#/components/parameters/FeedLimit"
          },
          {
            "$ref": "#/components/parameters/FeedOffset"
          }
        ],
        "responses": {
          "200": {
            "description": "The matching public events and feed metadata.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LlmEventFeed"
                }
              }
            }
          },
          "429": {
            "description": "The anonymous IP rate limit was exceeded. Honor Retry-After before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is likely to be accepted.",
                "schema": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 1
                }
              },
              "RateLimit-Policy": {
                "description": "Fixed-window policy name, quota, and window in seconds.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Current remaining quota and seconds until reset for each applied policy.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                },
                "example": {
                  "type": "https://playmapla.com/developers#errors",
                  "title": "Too Many Requests",
                  "status": 429,
                  "code": "rate_limit_exceeded",
                  "message": "Too many requests",
                  "detail": "Too many requests",
                  "resolution": "Wait for the Retry-After interval, then retry with a narrower request if possible.",
                  "retryAfter": 60
                }
              }
            }
          },
          "500": {
            "description": "The API could not complete the read operation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/llm-events": {
      "get": {
        "operationId": "listLlmEventsLegacyAlias",
        "security": [],
        "deprecated": true,
        "summary": "List public family events (legacy alias)",
        "description": "Compatibility alias for GET /api/v1/llm-events. New integrations should use the versioned path.",
        "parameters": [
          {
            "$ref": "#/components/parameters/EventId"
          },
          {
            "$ref": "#/components/parameters/EventTitle"
          },
          {
            "$ref": "#/components/parameters/EventSearch"
          },
          {
            "$ref": "#/components/parameters/EventLocation"
          },
          {
            "$ref": "#/components/parameters/EventCategory"
          },
          {
            "$ref": "#/components/parameters/DateFrom"
          },
          {
            "$ref": "#/components/parameters/DateTo"
          },
          {
            "$ref": "#/components/parameters/FeedLimit"
          },
          {
            "$ref": "#/components/parameters/FeedOffset"
          }
        ],
        "responses": {
          "200": {
            "description": "The matching public events and feed metadata.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LlmEventFeed"
                }
              }
            }
          },
          "429": {
            "description": "The anonymous IP rate limit was exceeded. Honor Retry-After before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is likely to be accepted.",
                "schema": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 1
                }
              },
              "RateLimit-Policy": {
                "description": "Fixed-window policy name, quota, and window in seconds.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Current remaining quota and seconds until reset for each applied policy.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                },
                "example": {
                  "type": "https://playmapla.com/developers#errors",
                  "title": "Too Many Requests",
                  "status": 429,
                  "code": "rate_limit_exceeded",
                  "message": "Too many requests",
                  "detail": "Too many requests",
                  "resolution": "Wait for the Retry-After interval, then retry with a narrower request if possible.",
                  "retryAfter": 60
                }
              }
            }
          },
          "500": {
            "description": "The API could not complete the read operation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/events/search": {
      "get": {
        "operationId": "searchEvents",
        "security": [],
        "summary": "Search and paginate public events",
        "description": "Searches public event listings with keyword, date, category, age, cost, location-type, and series-collapse filters. Query booleans are sent as the strings true or false. date_from defaults to the current server date when omitted. The route is anonymous and has both hourly and per-minute IP rate limits.",
        "parameters": [
          {
            "$ref": "#/components/parameters/SearchLimit"
          },
          {
            "$ref": "#/components/parameters/SearchOffset"
          },
          {
            "$ref": "#/components/parameters/CollapseSeries"
          },
          {
            "$ref": "#/components/parameters/EventSearch"
          },
          {
            "$ref": "#/components/parameters/EventCategory"
          },
          {
            "$ref": "#/components/parameters/AgeGroup"
          },
          {
            "$ref": "#/components/parameters/DateFrom"
          },
          {
            "$ref": "#/components/parameters/DateTo"
          },
          {
            "$ref": "#/components/parameters/SortBy"
          },
          {
            "$ref": "#/components/parameters/LocationType"
          },
          {
            "$ref": "#/components/parameters/ExcludeLibrary"
          },
          {
            "$ref": "#/components/parameters/IsFree"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of events with total count and a continuation flag.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventSearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "One or more event search query parameters are invalid.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                },
                "example": {
                  "type": "https://playmapla.com/developers#errors",
                  "title": "Bad Request",
                  "status": 400,
                  "code": "invalid_query",
                  "message": "Expected YYYY-MM-DD",
                  "detail": "Expected YYYY-MM-DD",
                  "resolution": "Check the documented query parameter types and ranges, then retry."
                }
              }
            }
          },
          "429": {
            "description": "The anonymous IP rate limit was exceeded. Honor Retry-After before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is likely to be accepted.",
                "schema": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 1
                }
              },
              "RateLimit-Policy": {
                "description": "Fixed-window policy name, quota, and window in seconds.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Current remaining quota and seconds until reset for each applied policy.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                },
                "example": {
                  "type": "https://playmapla.com/developers#errors",
                  "title": "Too Many Requests",
                  "status": 429,
                  "code": "rate_limit_exceeded",
                  "message": "Too many requests",
                  "detail": "Too many requests",
                  "resolution": "Wait for the Retry-After interval, then retry with a narrower request if possible.",
                  "retryAfter": 60
                }
              }
            }
          },
          "500": {
            "description": "The API could not complete the read operation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/guides": {
      "get": {
        "operationId": "listGuides",
        "security": [],
        "summary": "List current Play Map LA guides and plans",
        "description": "Returns published parent guides, ready-to-go outing plans, quick pills, and the current weekend event highlights. This is a cacheable, anonymous read endpoint; use the returned slugs and ids with the detail endpoints.",
        "responses": {
          "200": {
            "description": "Published guides and outing plans.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GuideHome"
                }
              }
            }
          },
          "429": {
            "description": "The anonymous IP rate limit was exceeded. Honor Retry-After before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is likely to be accepted.",
                "schema": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 1
                }
              },
              "RateLimit-Policy": {
                "description": "Fixed-window policy name, quota, and window in seconds.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Current remaining quota and seconds until reset for each applied policy.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                },
                "example": {
                  "type": "https://playmapla.com/developers#errors",
                  "title": "Too Many Requests",
                  "status": 429,
                  "code": "rate_limit_exceeded",
                  "message": "Too many requests",
                  "detail": "Too many requests",
                  "resolution": "Wait for the Retry-After interval, then retry with a narrower request if possible.",
                  "retryAfter": 60
                }
              }
            }
          },
          "500": {
            "description": "The API could not complete the read operation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/guides/{slug}": {
      "get": {
        "operationId": "getGuide",
        "security": [],
        "summary": "Get one published guide",
        "description": "Returns the full published guide content for a slug discovered from GET /api/v1/guides. Draft and unknown slugs return a structured 404 problem response.",
        "parameters": [
          {
            "$ref": "#/components/parameters/GuideSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "The published guide.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "article"
                  ],
                  "properties": {
                    "article": {
                      "$ref": "#/components/schemas/GuideArticleDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No published guide matches the requested slug.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "The anonymous IP rate limit was exceeded. Honor Retry-After before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is likely to be accepted.",
                "schema": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 1
                }
              },
              "RateLimit-Policy": {
                "description": "Fixed-window policy name, quota, and window in seconds.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Current remaining quota and seconds until reset for each applied policy.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                },
                "example": {
                  "type": "https://playmapla.com/developers#errors",
                  "title": "Too Many Requests",
                  "status": 429,
                  "code": "rate_limit_exceeded",
                  "message": "Too many requests",
                  "detail": "Too many requests",
                  "resolution": "Wait for the Retry-After interval, then retry with a narrower request if possible.",
                  "retryAfter": 60
                }
              }
            }
          },
          "500": {
            "description": "The API could not complete the read operation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/guides/plans/{id}": {
      "get": {
        "operationId": "getGuidePlan",
        "security": [],
        "summary": "Get one ready-to-go outing plan",
        "description": "Returns the full itinerary for a published ready-to-go plan id discovered from GET /api/v1/guides. Unknown ids return a structured 404 problem response.",
        "parameters": [
          {
            "$ref": "#/components/parameters/PlanId"
          }
        ],
        "responses": {
          "200": {
            "description": "The published outing plan.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "plan"
                  ],
                  "properties": {
                    "plan": {
                      "$ref": "#/components/schemas/GuidePlanDetail"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No published plan matches the requested id.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "The anonymous IP rate limit was exceeded. Honor Retry-After before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is likely to be accepted.",
                "schema": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 1
                }
              },
              "RateLimit-Policy": {
                "description": "Fixed-window policy name, quota, and window in seconds.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Current remaining quota and seconds until reset for each applied policy.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                },
                "example": {
                  "type": "https://playmapla.com/developers#errors",
                  "title": "Too Many Requests",
                  "status": 429,
                  "code": "rate_limit_exceeded",
                  "message": "Too many requests",
                  "detail": "Too many requests",
                  "resolution": "Wait for the Retry-After interval, then retry with a narrower request if possible.",
                  "retryAfter": 60
                }
              }
            }
          },
          "500": {
            "description": "The API could not complete the read operation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/search/semantic": {
      "post": {
        "operationId": "semanticSearch",
        "security": [],
        "summary": "Search events, places, or classes semantically",
        "description": "Embeds a natural-language query and returns semantic matches for one of the supported kinds: events, places, or classes. No API key is required. This endpoint is intentionally small and anonymous; it has a 64 KiB JSON body limit plus per-minute and hourly IP rate limits. If vector infrastructure is unavailable, the route may return an explicit degraded response with an empty matches array.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SemanticSearchRequest"
              },
              "examples": {
                "events": {
                  "summary": "Find toddler activities",
                  "value": {
                    "kind": "events",
                    "query": "outdoor storytime for toddlers",
                    "limit": 12,
                    "dateFromIso": "2026-08-22"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Semantic matches, or an explicit degraded response when vector search is unavailable.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SemanticSearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "The JSON body is malformed or does not satisfy the documented schema.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "405": {
            "description": "The endpoint only accepts the documented HTTP method.",
            "headers": {
              "Allow": {
                "description": "The allowed method.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "413": {
            "description": "The JSON request body exceeds the 64 KiB endpoint limit.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "415": {
            "description": "The request is not JSON.",
            "headers": {
              "Allow": {
                "description": "The required content type.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "The anonymous IP rate limit was exceeded. Honor Retry-After before retrying.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until a retry is likely to be accepted.",
                "schema": {
                  "type": "integer",
                  "format": "int32",
                  "minimum": 1
                }
              },
              "RateLimit-Policy": {
                "description": "Fixed-window policy name, quota, and window in seconds.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit": {
                "description": "Current remaining quota and seconds until reset for each applied policy.",
                "schema": {
                  "type": "string"
                }
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                },
                "example": {
                  "type": "https://playmapla.com/developers#errors",
                  "title": "Too Many Requests",
                  "status": 429,
                  "code": "rate_limit_exceeded",
                  "message": "Too many requests",
                  "detail": "Too many requests",
                  "resolution": "Wait for the Retry-After interval, then retry with a narrower request if possible.",
                  "retryAfter": 60
                }
              }
            }
          },
          "500": {
            "description": "The API could not complete the read operation.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "ApiVersion": {
        "description": "Current public API contract version.",
        "schema": {
          "type": "string",
          "const": "1"
        }
      },
      "RateLimitPolicy": {
        "description": "Fixed-window policy name, quota, and window in seconds.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit": {
        "description": "Current remaining quota and seconds until reset.",
        "schema": {
          "type": "string"
        }
      },
      "Deprecation": {
        "description": "RFC 9745 structured date indicating the operation is deprecated.",
        "schema": {
          "type": "string",
          "example": "@1787356800"
        }
      },
      "RateLimitLimit": {
        "description": "Compatibility form of the applied request quota.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "RateLimitRemaining": {
        "description": "Compatibility form of the remaining request quota.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "RateLimitReset": {
        "description": "Compatibility form of seconds until the quota resets.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "parameters": {
      "EventId": {
        "name": "id",
        "in": "query",
        "description": "Exact public event id.",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "EventTitle": {
        "name": "title",
        "in": "query",
        "description": "Case-insensitive title fragment.",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "EventSearch": {
        "name": "search",
        "in": "query",
        "description": "Keyword or natural-language fragment used to match event text.",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "EventLocation": {
        "name": "location",
        "in": "query",
        "description": "Location fragment such as a neighborhood, city, or venue.",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "EventCategory": {
        "name": "category",
        "in": "query",
        "description": "Event category filter.",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "AgeGroup": {
        "name": "age_group",
        "in": "query",
        "description": "Age-group text filter, for example 0-5 or All Ages.",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "DateFrom": {
        "name": "date_from",
        "in": "query",
        "description": "Include events on or after this calendar date (YYYY-MM-DD).",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date"
        }
      },
      "DateTo": {
        "name": "date_to",
        "in": "query",
        "description": "Include events on or before this calendar date (YYYY-MM-DD).",
        "required": false,
        "schema": {
          "type": "string",
          "format": "date"
        }
      },
      "FeedLimit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum number of feed events to return.",
        "required": false,
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 1,
          "maximum": 1000,
          "default": 250
        }
      },
      "FeedOffset": {
        "name": "offset",
        "in": "query",
        "description": "Number of matching feed events to skip.",
        "required": false,
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 0,
          "default": 0
        }
      },
      "SearchLimit": {
        "name": "limit",
        "in": "query",
        "description": "Number of event search results to return.",
        "required": false,
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 1,
          "maximum": 500,
          "default": 24
        }
      },
      "SearchOffset": {
        "name": "offset",
        "in": "query",
        "description": "Number of event search results to skip.",
        "required": false,
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 0,
          "default": 0
        }
      },
      "CollapseSeries": {
        "name": "collapse_series",
        "in": "query",
        "description": "Collapse recurring series into one representative result. Send the literal string true or false.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "true",
            "false"
          ],
          "default": "false"
        }
      },
      "SortBy": {
        "name": "sort_by",
        "in": "query",
        "description": "Result ordering.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "date",
            "popular",
            "price"
          ],
          "default": "date"
        }
      },
      "LocationType": {
        "name": "location_type",
        "in": "query",
        "description": "Location type filter as used by the public event search, such as indoor or outdoor.",
        "required": false,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "ExcludeLibrary": {
        "name": "exclude_library",
        "in": "query",
        "description": "Exclude library-like listings. Send the literal string true or false.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "true",
            "false"
          ],
          "default": "false"
        }
      },
      "IsFree": {
        "name": "is_free",
        "in": "query",
        "description": "Filter by free admission. Send the literal string true or false.",
        "required": false,
        "schema": {
          "type": "string",
          "enum": [
            "true",
            "false"
          ]
        }
      },
      "GuideSlug": {
        "name": "slug",
        "in": "path",
        "description": "Published guide slug returned by GET /api/v1/guides.",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      },
      "PlanId": {
        "name": "id",
        "in": "path",
        "description": "Published ready-to-go plan id returned by GET /api/v1/guides.",
        "required": true,
        "schema": {
          "type": "string",
          "minLength": 1
        }
      }
    },
    "schemas": {
      "ProblemDetails": {
        "type": "object",
        "description": "RFC 9457-style error details. Branch on code; show message and resolution to a human or agent.",
        "required": [
          "type",
          "title",
          "status",
          "code",
          "message",
          "detail",
          "resolution"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer",
            "format": "int32"
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code."
          },
          "message": {
            "type": "string"
          },
          "detail": {
            "type": "string"
          },
          "resolution": {
            "type": "string",
            "description": "Suggested recovery action."
          },
          "retryAfter": {
            "type": "integer",
            "format": "int32",
            "minimum": 1
          }
        },
        "additionalProperties": true
      },
      "LlmEvent": {
        "type": "object",
        "description": "An event normalized for agent and calendar use.",
        "required": [
          "id",
          "title",
          "description",
          "date",
          "time",
          "location",
          "full_address",
          "age_group",
          "category",
          "price",
          "is_free",
          "organizer",
          "organizer_contact",
          "tags",
          "url",
          "seo_keywords"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "date": {
            "type": "string"
          },
          "time": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "full_address": {
            "type": "string"
          },
          "age_group": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "price": {
            "type": "string"
          },
          "is_free": {
            "type": "boolean"
          },
          "organizer": {
            "type": "string"
          },
          "organizer_contact": {
            "$ref": "#/components/schemas/OrganizerContact"
          },
          "image_url": {
            "type": "string",
            "format": "uri"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "seo_keywords": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "OrganizerContact": {
        "type": "object",
        "properties": {
          "phone": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "website": {
            "type": "string",
            "format": "uri"
          }
        },
        "additionalProperties": false
      },
      "LlmEventFeedMetadata": {
        "type": "object",
        "required": [
          "total_events",
          "last_updated",
          "categories",
          "age_groups",
          "locations",
          "price_ranges",
          "total_matching_events",
          "returned_events",
          "limit",
          "offset",
          "has_more"
        ],
        "properties": {
          "total_events": {
            "type": "integer",
            "minimum": 0
          },
          "last_updated": {
            "type": "string",
            "format": "date-time"
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "age_groups": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "locations": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "price_ranges": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "total_matching_events": {
            "type": "integer",
            "minimum": 0
          },
          "returned_events": {
            "type": "integer",
            "minimum": 0
          },
          "limit": {
            "type": "integer",
            "minimum": 1
          },
          "offset": {
            "type": "integer",
            "minimum": 0
          },
          "has_more": {
            "type": "boolean"
          },
          "filters": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        },
        "additionalProperties": true
      },
      "LlmEventFeed": {
        "type": "object",
        "required": [
          "version",
          "events",
          "metadata",
          "search_suggestions"
        ],
        "properties": {
          "version": {
            "type": "string"
          },
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LlmEvent"
            }
          },
          "metadata": {
            "$ref": "#/components/schemas/LlmEventFeedMetadata"
          },
          "search_suggestions": {
            "$ref": "#/components/schemas/SearchSuggestions"
          }
        }
      },
      "SearchSuggestions": {
        "type": "object",
        "required": [
          "popular_searches",
          "trending_categories",
          "nearby_locations"
        ],
        "properties": {
          "popular_searches": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "trending_categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "nearby_locations": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PublicEvent": {
        "type": "object",
        "description": "A public event search row. Additional source-specific fields may be present.",
        "required": [
          "id",
          "title",
          "event_date",
          "start_time",
          "end_time",
          "location",
          "category",
          "organizer",
          "age_group",
          "price_text",
          "is_free",
          "attending_count"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "image_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "event_date": {
            "type": "string",
            "format": "date"
          },
          "event_end_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "start_time": {
            "type": "string"
          },
          "end_time": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "full_address": {
            "type": [
              "string",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "age_group": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "price_text": {
            "type": "string"
          },
          "is_free": {
            "type": "boolean"
          },
          "attending_count": {
            "type": "integer",
            "minimum": 0
          },
          "organizer": {
            "type": "string"
          },
          "organizer_website": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "tags": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          },
          "indoor_outdoor": {
            "type": [
              "string",
              "null"
            ]
          },
          "series_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "series_title": {
            "type": [
              "string",
              "null"
            ]
          },
          "series_locations_count": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "series_dates_count": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "series_instances_count": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0
          },
          "series_preview_locations": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "series_preview_dates": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "additionalProperties": true
      },
      "EventSearchResponse": {
        "type": "object",
        "required": [
          "events",
          "total",
          "hasMore"
        ],
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PublicEvent"
            }
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "hasMore": {
            "type": "boolean"
          }
        }
      },
      "GuideArticleSummary": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "excerpt",
          "author",
          "publish_date",
          "read_time",
          "category",
          "tags",
          "image",
          "featured",
          "hero_highlights"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "excerpt": {
            "type": "string"
          },
          "author": {
            "type": "string"
          },
          "publish_date": {
            "type": "string"
          },
          "read_time": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "image": {
            "type": "string"
          },
          "featured": {
            "type": "boolean"
          },
          "hero_highlights": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "GuideArticleDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/GuideArticleSummary"
          },
          {
            "type": "object",
            "required": [
              "content",
              "hero_image_caption"
            ],
            "properties": {
              "content": {
                "type": "string"
              },
              "hero_image_caption": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        ]
      },
      "GuidePlanSummary": {
        "type": "object",
        "required": [
          "id",
          "title",
          "location_name",
          "short_description",
          "image",
          "cost_tag",
          "timing_tag",
          "featured"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "location_name": {
            "type": "string"
          },
          "short_description": {
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "cost_tag": {
            "type": "string"
          },
          "timing_tag": {
            "type": "string"
          },
          "featured": {
            "type": "boolean"
          }
        }
      },
      "GuidePlanDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/GuidePlanSummary"
          },
          {
            "type": "object",
            "required": [
              "steps",
              "why_it_works",
              "parent_tip",
              "food_stops",
              "address",
              "route_stops",
              "gallery_images",
              "tags",
              "optional_add_on",
              "worth_it",
              "parent_conversations"
            ],
            "properties": {
              "steps": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "why_it_works": {
                "type": "string"
              },
              "parent_tip": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "food_stops": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "address": {
                "type": "string"
              },
              "route_stops": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "gallery_images": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "optional_add_on": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "worth_it": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/WorthIt"
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "parent_conversations": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ParentConversation"
                }
              }
            }
          }
        ]
      },
      "WorthIt": {
        "type": "object",
        "required": [
          "score",
          "metrics"
        ],
        "properties": {
          "score": {
            "type": "number"
          },
          "metrics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WorthItMetric"
            }
          }
        }
      },
      "WorthItMetric": {
        "type": "object",
        "required": [
          "label",
          "value",
          "note"
        ],
        "properties": {
          "label": {
            "type": "string"
          },
          "value": {
            "type": "number"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "ParentConversation": {
        "type": "object",
        "required": [
          "initial",
          "name",
          "ago",
          "text"
        ],
        "properties": {
          "initial": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "ago": {
            "type": "string"
          },
          "text": {
            "type": "string"
          }
        }
      },
      "GuideQuickPill": {
        "type": "object",
        "required": [
          "id",
          "title",
          "subtitle",
          "image",
          "symbol",
          "destination"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "subtitle": {
            "type": "string"
          },
          "image": {
            "type": [
              "string",
              "null"
            ]
          },
          "symbol": {
            "type": "string"
          },
          "destination": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/GuideArticleDestination"
              },
              {
                "$ref": "#/components/schemas/GuideEventsDestination"
              }
            ]
          }
        }
      },
      "GuideArticleDestination": {
        "type": "object",
        "required": [
          "type",
          "slug"
        ],
        "properties": {
          "type": {
            "const": "article"
          },
          "slug": {
            "type": "string"
          }
        }
      },
      "GuideEventsDestination": {
        "type": "object",
        "required": [
          "type",
          "tag"
        ],
        "properties": {
          "type": {
            "const": "events"
          },
          "tag": {
            "type": "string"
          }
        }
      },
      "GuideWeekendHighlights": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "event_ids"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "event_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "GuideHome": {
        "type": "object",
        "required": [
          "quick_pills",
          "articles",
          "plans",
          "weekend_highlights"
        ],
        "properties": {
          "quick_pills": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GuideQuickPill"
            }
          },
          "articles": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GuideArticleSummary"
            }
          },
          "plans": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GuidePlanSummary"
            }
          },
          "weekend_highlights": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/GuideWeekendHighlights"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "SemanticSearchRequest": {
        "type": "object",
        "required": [
          "kind",
          "query"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "events",
              "places",
              "classes"
            ]
          },
          "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "dateFromIso": {
            "type": "string",
            "format": "date"
          },
          "windowEndIso": {
            "type": "string",
            "format": "date"
          }
        },
        "additionalProperties": false
      },
      "SemanticMatch": {
        "type": "object",
        "description": "A match row returned by the configured events, places, or classes search function. Exactly one of event_id, place_id, or class_id is present, together with a similarity score.",
        "properties": {
          "event_id": {
            "type": "string",
            "format": "uuid"
          },
          "place_id": {
            "type": "string",
            "format": "uuid"
          },
          "class_id": {
            "type": "string",
            "format": "uuid"
          },
          "similarity": {
            "type": "number"
          }
        },
        "additionalProperties": true
      },
      "SemanticSearchResponse": {
        "type": "object",
        "required": [
          "matches"
        ],
        "properties": {
          "matches": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SemanticMatch"
            }
          },
          "degraded": {
            "type": "boolean",
            "description": "True when the route intentionally returned no semantic matches because vector infrastructure was unavailable."
          },
          "reason": {
            "type": "string"
          }
        }
      }
    }
  }
}
