{
  "openapi": "3.1.0",
  "info": {
    "title": "VRChat Feedback Search Gateway",
    "version": "1.1.0",
    "summary": "Public search API. Agents use the short /api/agent routes; the website uses Algolia-compatible InstantSearch.",
    "description": "## Overview\n\nAgents should call **`GET /api/agent/search`** and **`GET /api/agent/posts/{board}/{urlName}`**. Those routes return short JSON: a snippet per hit, and one post with truncated **`details`** plus a few truncated comments. Cite each post **`url`** (`https://feedback.vrchat.com/{board}/p/{urlName}`). Responses include a **`Link`** header to this document.\n\nThe website uses the same JSON request and response shape as **Algolia** **InstantSearch**.\n\n## Query modes\n\n- **Default**: Free-text searches match searchable post attributes, including combined text, title, body (**`details`**), author name, and related fields. Whitespace-only or empty **`params.query`** does not restrict by text.\n- **Lucene syntax** (**`?mode=lucene`** on **POST** or **GET** **`/api/search`**): Interprets **`params.query`** with Lucene query syntax. Grammar and operators follow OpenSearch; see **[query_string query](https://opensearch.org/docs/latest/query-dsl/full-text/query-string/)**.\n\nOn **GET** **`/api/search`**, Lucene mode does not execute a search by itself—you must also supply at least one of **`q`**, **`query`**, **`hitsPerPage`**, or **`page`**.\n\n## Lucene field reference\n\n| **`Field`** | **`Kind`** | **`Example`** |\n| ----- | ----- | ------- |\n| **`aiCategories`** | **`text`** | **`aiCategories:groups`** |\n| **`aiCategories.keyword`** | **`keyword`** | **`aiCategories.keyword:\"groups.calendar\"`** |\n| **`aiTaggedAt`** | **`date`** | **`aiTaggedAt:[\"2025-01-01\" TO *]`** |\n| **`author.name`** | **`text`** | **`author.name:Alice`** |\n| **`author.name.keyword`** | **`keyword`** | **`author.name.keyword:\"Jane Doe\"`** |\n| **`board.name`** | **`text`** | **`board.name:bug`** |\n| **`board.name.keyword`** | **`keyword`** | **`board.name.keyword:\"Feature Requests\"`** |\n| **`board.urlName`** | **`keyword`** | **`board.urlName:bug-reports`** |\n| **`category.name`** | **`text`** | **`category.name:sdk`** |\n| **`category.name.keyword`** | **`keyword`** | **`category.name.keyword:SDK`** |\n| **`combined_text`** | **`text`** | **`combined_text:performance`** |\n| **`commentCount`** | **`numeric`** | **`commentCount:[1 TO 50]`** |\n| **`comments.author.name`** | **`text`** | **`comments.author.name:Alice`** |\n| **`comments.author.name.keyword`** | **`keyword`** | **`comments.author.name.keyword:\"Jane Doe\"`** |\n| **`comments.created`** | **`date`** | **`comments.created:[\"2025-01-01\" TO *]`** |\n| **`comments.likeCount`** | **`numeric`** | **`comments.likeCount:[1 TO *]`** |\n| **`comments.pinned`** | **`keyword`** | **`comments.pinned:true`** |\n| **`comments.value`** | **`text`** | **`comments.value:\"network lag\"`** |\n| **`created`** | **`date`** | **`created:[\"2025-01-01\" TO \"2026-01-01\"]`** |\n| **`details`** | **`text`** | **`details:\"network lag\"`** |\n| **`maxScore`** | **`numeric`** | **`maxScore:[100 TO *]`** |\n| **`mergeCount`** | **`numeric`** | **`mergeCount:[1 TO *]`** |\n| **`score`** | **`numeric`** | **`score:[10 TO *]`** |\n| **`status`** | **`keyword`** | **`status:open`** |\n| **`statusChanged`** | **`date`** | **`statusChanged:[\"2025-01-01\" TO *]`** |\n| **`title`** | **`text`** | **`title:avatar`** |\n| **`trendingScore`** | **`numeric`** | **`trendingScore:[500 TO *]`** |\n| **`updatedAt`** | **`date`** | **`updatedAt:[\"2025-01-01\" TO *]`** |\n| **`voteSettings.highEngagement`** | **`keyword / bool-like`** | **`voteSettings.highEngagement:true`** |\n| **`voteSettings.lowEngagement`** | **`keyword / bool-like`** | **`voteSettings.lowEngagement:true`** |\n| **`voteSettings.moderateEngagement`** | **`keyword / bool-like`** | **`voteSettings.moderateEngagement:true`** |\n\n**`Kind`** values (**`text`**, **`keyword`**, **`numeric`**, **`date`**, …) mirror index behavior; each **`Example`** uses Lucene syntax for the adjoining **`Field`**."
  },
  "externalDocs": {
    "description": "**InstantSearch** client integration (**Algolia**-compatible request and response formats).",
    "url": "https://www.searchkit.co/docs/proxy-elasticsearch/with-express-js"
  },
  "servers": [
    {
      "url": "/",
      "description": "Same-origin; server URL **`/`**."
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Agent",
      "description": "**`GET /api/agent/search`** and **`GET /api/agent/posts/{board}/{urlName}`**: short read-only JSON for agents."
    },
    {
      "name": "Search",
      "description": "**`POST`**/**`GET`** **`/api/search`** (**Algolia**/**InstantSearch** request and response shapes)."
    },
    {
      "name": "Operations",
      "description": "**`GET`** **`/healthz`**, **`GET`** **`/openapi.json`**, **`GET`** **`/api/index`**; **`GET`** **`/api/search`** (**discovery**)."
    }
  ],
  "paths": {
    "/api/search": {
      "post": {
        "operationId": "searchMultiQuery",
        "summary": "Run one or more InstantSearch queries.",
        "description": "Accepts a JSON **`array`** of **InstantSearch** multi-query payloads. Supply **`mode`** as an HTTP **`query`** parameter (never inside the request **`body`**); **`mode=lucene`** enables Lucene syntax for each **`params.query`**. Maximum **`body`** size **512 KiB**.",
        "tags": [
          "Search"
        ],
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "lucene"
              ]
            },
            "description": "Parsing mode for each item's **`params.query`**. Omit for default matching. **`lucene`** selects Lucene syntax; see overview."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MultiQueryRequest"
              },
              "examples": {
                "minimalQuery": {
                  "summary": "Single query, equivalent to **GET** **`/api/search?q=ui&hitsPerPage=15&page=0`**",
                  "value": [
                    {
                      "indexName": "feedback-posts",
                      "params": {
                        "query": "ui",
                        "hitsPerPage": 15,
                        "page": 0
                      }
                    }
                  ]
                },
                "facetedQuery": {
                  "summary": "Faceted query with **`facetFilters`** and **`indexName`** **`feedback-posts_score_desc`**",
                  "value": [
                    {
                      "indexName": "feedback-posts_score_desc",
                      "params": {
                        "query": "desktop crash",
                        "hitsPerPage": 20,
                        "page": 0,
                        "facets": [
                          "status",
                          "board_slug"
                        ],
                        "facetFilters": [
                          [
                            "status:open"
                          ]
                        ]
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "**`SearchResponse`** **`results`** entries with **`hits`**, paging, **`facets`**, and metadata (**`nbHits`**, **`nbPages`**, **`processingTimeMS`**, etc.).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "**`body`** is not a JSON **`array`**, or search expression is **`Invalid search query`** (bad Lucene/filter syntax).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "examples": {
                  "malformedBody": {
                    "summary": "Expected **`InstantSearch`** **`multi-query`** **`array`**",
                    "value": {
                      "message": "Expected a JSON array of InstantSearch multiple-queries payloads"
                    }
                  },
                  "invalidSearch": {
                    "summary": "Invalid Lucene/filter (**`Invalid search query`**)",
                    "value": {
                      "message": "Invalid search query",
                      "detail": "Search service rejected the request (HTTP 400): ..."
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "**`request`** **`body`** exceeds **512 KiB**. The response may omit **`application/json`** and use **`text/plain`**."
          },
          "500": {
            "description": "Search operation could not be completed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "search failed"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "searchSingleQueryOrDiscovery",
        "summary": "Run a single query via query string, or return a discovery document.",
        "description": "Runs a streamlined single-query search when any of **`q`**, **`query`**, **`hitsPerPage`**, or **`page`** appears; otherwise returns a discovery document (**`DiscoveryResponse`**).\n\n### Single-query search\n\nProviding any query parameter noted above invokes one search against **`feedback-posts`**, equivalent to a one-element **`POST`** payload. **`?mode=lucene`** applies Lucene parsing to the text query consistently with **`POST`**.\n\n| **`Parameter`** | Equivalent **`InstantSearch`** field | **`Default`** | **`Constraints`** |\n| ----- | ----- | ----- | ----- |\n| **`q`** or **`query`** | **`params.query`** | `\"\"` | **`q`** overrides **`query`** |\n| **`hitsPerPage`** | **`params.hitsPerPage`** | **`50`** | Clamped to **`[1, 500]`**; invalid inputs use **`50`** |\n| **`page`** | **`params.page`** | **`0`** | Clamped to **`[0, 9999]`**; invalid inputs use **`0`** |\n| **`mode`** | Parsing profile for **`params.query`** | _(omit ⇒ default)_ | **`lucene`** only; ineffective without **`q`**, **`query`**, **`hitsPerPage`**, or **`page`** above |\n\nAdvanced behaviours—multiple simultaneous queries, faceting definitions, granular filters beyond these shortcuts, choice of sort replica—require **`POST`**.\n\n### Discovery\n\nAbsent all of **`q`**, **`query`**, **`hitsPerPage`**, and **`page`** (even if **`mode=lucene`** is present), **`GET`** returns endpoint metadata rather than hits.",
        "tags": [
          "Search"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Search terms. Maps to **`params.query`**. Takes precedence over **`query`** when both are supplied.",
            "example": "desktop crash"
          },
          {
            "name": "query",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Alias of **`q`**. Ignored when **`q`** is also present."
          },
          {
            "name": "hitsPerPage",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 500
            },
            "description": "Page size. Defaults to **`50`**. Values outside **`[1, 500]`** or non-numeric input fall back to **`50`**."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0,
              "maximum": 9999
            },
            "description": "Zero-based page index. Defaults to **`0`**. Values outside **`[0, 9999]`** or non-numeric input fall back to **`0`**."
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "lucene"
              ]
            },
            "description": "Interpretation mode for the URL **`q`**/**`query`** search terms (**`params.query`** after mapping). Omit for default matching. **`lucene`** enables Lucene syntax; **`mode`** alone never runs a search—pair it with **`q`**, **`query`**, **`hitsPerPage`**, or **`page`**."
          }
        ],
        "responses": {
          "200": {
            "description": "Either search results (**`SearchResponse`**) when search parameters were included, or a discovery document (**`DiscoveryResponse`**).",
            "headers": {
              "Link": {
                "$ref": "#/components/headers/ServiceLink"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/SearchResponse"
                    },
                    {
                      "$ref": "#/components/schemas/DiscoveryResponse"
                    }
                  ]
                },
                "examples": {
                  "discovery": {
                    "summary": "Discovery (**`GET`** missing **`q`**, **`query`**, **`hitsPerPage`**, **`page`**)",
                    "value": {
                      "endpoints": {
                        "POST": "/api/search",
                        "GET": "/api/search?q=terms&hitsPerPage=50&page=0",
                        "agentSearch": "/api/agent/search?q=terms&limit=10&page=0",
                        "agentPost": "/api/agent/posts/{board}/{urlName}",
                        "index": "/api/index",
                        "openapi": "/openapi.json"
                      },
                      "description": "Agents: **`GET /api/agent/search`** and **`GET /api/agent/posts/{board}/{urlName}`**. UI: **`POST`**/**`GET /api/search`**. Contract: **`openapi.json`**."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid search expression (**`Invalid search query`**).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "Invalid search query",
                  "detail": "failed to parse ..."
                }
              }
            }
          },
          "500": {
            "description": "Search failed. Not used for **`DiscoveryResponse`**-only **`GET`** calls.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "search failed"
                }
              }
            }
          }
        }
      },
      "options": {
        "operationId": "searchCorsPreflight",
        "summary": "CORS preflight.",
        "description": "Cross-origin resource sharing (CORS) preflight. Returns **`204`** with permissive **`Access-Control-*`** headers for browser clients.",
        "tags": [
          "Search"
        ],
        "responses": {
          "204": {
            "description": "**`No`** **`Content`**."
          }
        }
      }
    },
    "/api/index": {
      "get": {
        "operationId": "indexGeneration",
        "summary": "Current OpenSearch backing index name.",
        "description": "Returns the backing index currently published behind the **`feedback-posts`** alias. The name changes after each ingest alias swap, so clients can poll this endpoint and re-run the current search when **`index`** changes. Response is not cached.",
        "tags": [
          "Operations"
        ],
        "responses": {
          "200": {
            "description": "Current backing index name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IndexResponse"
                },
                "example": {
                  "index": "feedback-posts-202608130112"
                }
              }
            }
          },
          "500": {
            "description": "Backing index lookup failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "index lookup failed"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/search": {
      "get": {
        "operationId": "agentSearch",
        "summary": "Search posts for an agent.",
        "description": "Short read-only search. Each hit is a title, canonical post **`url`**, board, status, score, comment count, created time, and a **`details`** snippet of at most **200** characters. **`_source`** omits voters, comments, and the full body.\n\n**`q`** uses the same strict text match as the website default (not Lucene). A non-empty **`q`** orders hits by relevance, then newest **`created`**. An empty or omitted **`q`** lists newest posts. **`board`** and **`status`** are exact filters on **`board.urlName`** and **`status`**.\n\n**`limit`** defaults to **10** and is clamped to **`[1, 20]`**; invalid input uses **10**. **`page`** is zero-based and defaults to **0**; invalid input uses **0**. **`page * limit`** must be below **50000**.",
        "tags": [
          "Agent"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Search text. Omit or send an empty string to list newest posts."
          },
          {
            "name": "board",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Exact **`board.urlName`**, for example **`feature-requests`**."
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Exact post **`status`**, for example **`open`**."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 10
            },
            "description": "Page size. Defaults to **10**. Values above **20** are clamped to **20**. Invalid or non-positive input uses **10**."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Zero-based page index. Defaults to **0**."
          }
        ],
        "responses": {
          "200": {
            "description": "Short hit list.",
            "headers": {
              "Link": {
                "$ref": "#/components/headers/ServiceLink"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentSearchResponse"
                },
                "example": {
                  "q": "avatar performance",
                  "board": "feature-requests",
                  "status": "open",
                  "page": 0,
                  "limit": 10,
                  "nbHits": 1,
                  "hits": [
                    {
                      "post_id": "5aff34492d6f472658e02dfa",
                      "title": "Support for VRM",
                      "url": "https://feedback.vrchat.com/feature-requests/p/support-for-vrm",
                      "board": "feature-requests",
                      "status": "open",
                      "score": 40,
                      "commentCount": 3,
                      "created": "2018-05-19T00:00:00.000Z",
                      "snippet": "Quest avatar limits…"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameter, or **`page`** is past the result window.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "invalid board"
                }
              }
            }
          },
          "500": {
            "description": "Search failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "search failed"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/posts/{board}/{urlName}": {
      "get": {
        "operationId": "agentPost",
        "summary": "Fetch one post for an agent.",
        "description": "One post identified by exact **`board.urlName`** and **`urlName`**. **`details`** is truncated to **4000** characters (**`detailsTruncated`**). **`comments`** keeps at most **8** public comments, pinned first and then newest, each **`value`** truncated to **500** characters. Deleted, internal, private, spam, and blank comments are omitted. Voters are omitted.",
        "tags": [
          "Agent"
        ],
        "parameters": [
          {
            "name": "board",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Exact **`board.urlName`**."
          },
          {
            "name": "urlName",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Exact post **`urlName`**."
          }
        ],
        "responses": {
          "200": {
            "description": "The post, truncated for an agent context window.",
            "headers": {
              "Link": {
                "$ref": "#/components/headers/ServiceLink"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AgentPost"
                },
                "example": {
                  "post_id": "5aff34492d6f472658e02dfa",
                  "title": "Support for VRM",
                  "url": "https://feedback.vrchat.com/feature-requests/p/support-for-vrm",
                  "board": "feature-requests",
                  "status": "open",
                  "score": 40,
                  "commentCount": 12,
                  "created": "2018-05-19T00:00:00.000Z",
                  "details": "Please add VRM import.",
                  "detailsTruncated": false,
                  "comments": [
                    {
                      "author": "Ada",
                      "created": "2020-01-01T00:00:00.000Z",
                      "pinned": true,
                      "value": "Staff note.",
                      "valueTruncated": false
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "description": "No indexed post matches that board and url name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "post not found"
                }
              }
            }
          },
          "500": {
            "description": "Lookup failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "message": "search failed"
                }
              }
            }
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "health",
        "summary": "Liveness probe.",
        "description": "Reports whether this search service responds to HTTP requests. Response body is the plain-text string **`ok`**.",
        "tags": [
          "Operations"
        ],
        "responses": {
          "200": {
            "description": "Probe succeeded; body is **`ok`**.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "const": "ok"
                }
              }
            }
          },
          "502": {
            "description": "The service is unreachable from the deployment edge."
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "openapi",
        "summary": "This OpenAPI document.",
        "description": "This **`resource`** (**`openapi.json`**): **`OpenAPI`** **`3.1`**, **`application/json`** **`Content-Type`** on success.",
        "tags": [
          "Operations"
        ],
        "responses": {
          "200": {
            "description": "**`OpenAPI`** **`3.1`** document (**`application/json`**).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "ServiceLink": {
        "description": "Machine contract (**`rel=service-desc`**) and human API reference (**`rel=service-doc`**).",
        "schema": {
          "type": "string",
          "example": "</openapi.json>; rel=\"service-desc\"; type=\"application/openapi+json\", </openapi.html>; rel=\"service-doc\""
        }
      }
    },
    "schemas": {
      "MultiQueryRequest": {
        "type": "array",
        "description": "InstantSearch **`array`**; each **`object`** has **`indexName`** + **`params`**. Allowed **`params`** keys follow Algolia conventions; **`params.query`** parsing depends on URL **`mode`** (**`lucene`** or omitted — see overview).",
        "minItems": 1,
        "items": {
          "$ref": "#/components/schemas/MultiQueryRequestItem"
        }
      },
      "MultiQueryRequestItem": {
        "type": "object",
        "required": [
          "indexName",
          "params"
        ],
        "properties": {
          "indexName": {
            "type": "string",
            "description": "Index replica to query. Use **`feedback-posts`** for the default sort (newest post first by **`created`**). Activity sorts use **`lastActivityAt`** (max of **`created`**, **`statusChanged`**, and visible comment **`created`**). Additional replicas include **`feedback-posts_created_asc`**, **`feedback-posts_activity_desc`**, **`feedback-posts_activity_asc`**, **`feedback-posts_score_desc`**, **`feedback-posts_score_asc`**, and **`feedback-posts_relevance_desc`**. **`feedback-posts_created_desc`** does not exist; rely on **`feedback-posts`** for newest-created browsing.",
            "examples": [
              "feedback-posts",
              "feedback-posts_created_asc",
              "feedback-posts_activity_desc",
              "feedback-posts_activity_asc",
              "feedback-posts_score_desc",
              "feedback-posts_relevance_desc"
            ]
          },
          "params": {
            "type": "object",
            "description": "Algolia/**InstantSearch** **`params`** object—keys below plus any additional keys your client sends. Listed keys are illustrative; semantics follow **`InstantSearch`** when applicable.",
            "additionalProperties": true,
            "properties": {
              "query": {
                "type": "string",
                "description": "Free-text search string (**`InstantSearch`** **`params.query`**). Empty/whitespace does not constrain text."
              },
              "hitsPerPage": {
                "type": "integer",
                "minimum": 1,
                "description": "**`hitsPerPage`** page size."
              },
              "page": {
                "type": "integer",
                "minimum": 0,
                "description": "**`page`** (**`0`**-based)."
              },
              "facets": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Facet attributes whose counts accompany the hits. Common post facets: **`board_slug`**, **`board_name`**, **`status`**, **`category_name`**, **`author_name`**, **`aiCategories`**, **`vote_highEngagement`**, **`vote_moderateEngagement`**, **`vote_lowEngagement`**; quantitative facets **`score`**, **`maxScore`**, **`commentCount`**, **`mergeCount`**, **`trendingScore`**, **`post_created`**, **`post_updated`**, **`post_statusChanged`**; nested comment facets **`comment_author_name`**, **`comment_pinned`**, **`comment_likeCount`**, **`comment_created`**; nested voter facet **`voter_name`**."
              },
              "facetFilters": {
                "description": "Algolia **`facetFilters`**: a **`string`**, **`string[]`**, or nested **`array`** (**OR-of-AND** groups).",
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  {
                    "type": "array",
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                ]
              },
              "numericFilters": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Algolia **`numericFilters`** entries such as **`score>=10`**."
              },
              "filters": {
                "type": "string",
                "description": "Algolia **`filters`** (**`string`** expression)."
              },
              "excludeFacet": {
                "type": "object",
                "description": "Exclude posts that already match any listed value of this facet before aggregating. Sidebar additional-hit queries (`+N`) send this with the facet's own **`facetFilters`** omitted and **`hitsPerPage`** `0`. Nested voter and comment attributes exclude a matching nested document.",
                "required": ["attribute", "values"],
                "properties": {
                  "attribute": {
                    "type": "string",
                    "description": "Facet attribute name, such as **`status`** or **`voter_name`**."
                  },
                  "values": {
                    "type": "array",
                    "items": { "type": "string" },
                    "minItems": 1,
                    "description": "Checked values. A post matching any of them is left out of this query."
                  }
                }
              },
              "maxValuesPerFacet": {
                "type": "integer",
                "minimum": 1,
                "description": "Algolia **`maxValuesPerFacet`** cap."
              }
            }
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "description": "Algolia-style search-response envelope listing one **`results`** entry per submitted query.",
        "required": [
          "results"
        ],
        "properties": {
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchResult"
            }
          }
        },
        "additionalProperties": true
      },
      "SearchResult": {
        "type": "object",
        "description": "Single-query result. Mirrors Algolia **`SearchResponse`**; only commonly used fields enumerated.",
        "properties": {
          "hits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Hit"
            },
            "description": "**`hits`** (**`Hit`** **`array`**) returned for **`page`**."
          },
          "nbHits": {
            "type": "integer",
            "minimum": 0,
            "description": "**`nbHits`** total hits."
          },
          "page": {
            "type": "integer",
            "minimum": 0,
            "description": "**`page`** (**`0`**-based)."
          },
          "nbPages": {
            "type": "integer",
            "minimum": 0,
            "description": "**`nbPages`** total pages."
          },
          "hitsPerPage": {
            "type": "integer",
            "minimum": 1,
            "description": "**`hitsPerPage`** for **`page`**."
          },
          "processingTimeMS": {
            "type": "integer",
            "minimum": 0,
            "description": "**`processingTimeMS`** latency."
          },
          "query": {
            "type": "string",
            "description": "Echo of **`params.query`** (or mapped **`GET`** **`q`**/**`query`**)."
          },
          "facets": {
            "type": "object",
            "description": "**`facets`**: each **`facetName`** maps (**`facetValue`** → **`count`**).",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "integer",
                "minimum": 0
              }
            }
          }
        },
        "additionalProperties": true
      },
      "Hit": {
        "type": "object",
        "description": "Indexed **`Hit`** document: **`objectID`**, post fields and nested **`author`**, **`board`**, **`category`**, **`comments`**, **`_highlightResult`**, **`_snippetResult`**, etc.",
        "properties": {
          "objectID": {
            "type": "string"
          },
          "post_id": {
            "type": "string"
          },
          "__v": {
            "type": "integer"
          },
          "title": {
            "type": "string"
          },
          "details": {
            "type": "string"
          },
          "urlName": {
            "type": "string"
          },
          "ideaID": {
            "type": "string"
          },
          "boardID": {
            "type": "string"
          },
          "categoryID": {
            "type": "string"
          },
          "subCategoryID": {
            "type": "string"
          },
          "companyID": {
            "type": "string"
          },
          "authorID": {
            "type": "string"
          },
          "deleteID": {
            "type": "string"
          },
          "deletedBy": {
            "type": "string"
          },
          "updatedBy": {
            "type": "string"
          },
          "by": {
            "type": "string"
          },
          "byID": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "score": {
            "type": "integer"
          },
          "maxScore": {
            "type": "integer"
          },
          "mergeCount": {
            "type": "integer"
          },
          "trendingScore": {
            "type": "number"
          },
          "commentCount": {
            "type": "integer"
          },
          "viewerVote": {
            "type": "integer",
            "nullable": true
          },
          "viewerIsAuthor": {
            "type": "boolean",
            "nullable": true
          },
          "loading": {
            "type": "boolean",
            "nullable": true
          },
          "notFound": {
            "type": "boolean",
            "nullable": true
          },
          "etaPublic": {
            "type": "boolean",
            "nullable": true
          },
          "eta": {
            "type": "string",
            "nullable": true
          },
          "ogImageURL": {
            "type": "string",
            "nullable": true
          },
          "imageURLs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "fileURLs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "opportunities": {
            "type": "string",
            "nullable": true
          },
          "created": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "statusChanged": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "deletedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "lastUpdated": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "error": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true
          },
          "linkedEntry": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true
          },
          "sourceFeatureExtractionItem": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true
          },
          "files": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true
          },
          "author": {
            "type": "object",
            "additionalProperties": true
          },
          "board": {
            "type": "object",
            "additionalProperties": true
          },
          "category": {
            "type": "object",
            "additionalProperties": true
          },
          "voteSettings": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true
          },
          "voters": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "nullable": true
          },
          "comments": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "nullable": true
          },
          "pinnedComment": {
            "type": "object",
            "additionalProperties": true,
            "nullable": true
          },
          "aiCategories": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          },
          "aiTaggedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "_highlightResult": {
            "type": "object",
            "additionalProperties": true
          },
          "_snippetResult": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "additionalProperties": true
      },
      "IndexResponse": {
        "type": "object",
        "description": "Backing index currently published behind the **`feedback-posts`** alias.",
        "required": [
          "index"
        ],
        "properties": {
          "index": {
            "type": "string",
            "description": "OpenSearch backing index name (timestamped, e.g. **`feedback-posts-202608130112`**)."
          }
        },
        "additionalProperties": false
      },
      "DiscoveryResponse": {
        "type": "object",
        "description": "Discovery payload from **`GET /api/search`** when none of **`q`**, **`query`**, **`hitsPerPage`**, or **`page`** are present; **`mode`** alone never performs a search.",
        "required": [
          "endpoints",
          "description"
        ],
        "properties": {
          "endpoints": {
            "type": "object",
            "required": [
              "POST",
              "GET",
              "agentSearch",
              "agentPost",
              "index",
              "openapi"
            ],
            "properties": {
              "POST": {
                "type": "string",
                "description": "**`POST`** **`/api/search`** URL."
              },
              "GET": {
                "type": "string",
                "description": "**`GET`** example (**`/api/search`** URL **`query`** string)."
              },
              "agentSearch": {
                "type": "string",
                "description": "**`GET /api/agent/search`** URL."
              },
              "agentPost": {
                "type": "string",
                "description": "**`GET /api/agent/posts/{board}/{urlName}`** URL."
              },
              "index": {
                "type": "string",
                "description": "**`GET`** **`/api/index`** URL (current backing index name)."
              },
              "openapi": {
                "type": "string",
                "description": "**`openapi`** key: **`/openapi.json`** path **`string`**."
              }
            },
            "additionalProperties": false
          },
          "description": {
            "type": "string",
            "description": "Synthesized **`discovery`** narration (**Markdown** tolerated)."
          }
        },
        "additionalProperties": false
      },
      "AgentHit": {
        "type": "object",
        "description": "One search hit. Fields that are missing on the indexed post are omitted.",
        "required": [
          "snippet"
        ],
        "properties": {
          "post_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "Canonical post URL: **`https://feedback.vrchat.com/{board}/p/{urlName}`**.",
            "examples": [
              "https://feedback.vrchat.com/feature-requests/p/support-for-vrm"
            ]
          },
          "board": {
            "type": "string",
            "description": "**`board.urlName`**."
          },
          "status": {
            "type": "string"
          },
          "score": {
            "type": "integer"
          },
          "commentCount": {
            "type": "integer"
          },
          "created": {
            "type": "string",
            "description": "Post **`created`** timestamp as stored in the index."
          },
          "snippet": {
            "type": "string",
            "description": "Plain-text **`details`** fragment, at most **200** characters. Empty when the post has no **`details`** text."
          }
        },
        "additionalProperties": false
      },
      "AgentSearchResponse": {
        "type": "object",
        "required": [
          "q",
          "page",
          "limit",
          "nbHits",
          "hits"
        ],
        "properties": {
          "q": {
            "type": "string"
          },
          "board": {
            "type": "string",
            "description": "Present when the request filtered by **`board`**."
          },
          "status": {
            "type": "string",
            "description": "Present when the request filtered by **`status`**."
          },
          "page": {
            "type": "integer",
            "minimum": 0
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20
          },
          "nbHits": {
            "type": "integer",
            "minimum": 0
          },
          "hits": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AgentHit"
            }
          }
        },
        "additionalProperties": false
      },
      "AgentComment": {
        "type": "object",
        "required": [
          "pinned",
          "value",
          "valueTruncated"
        ],
        "properties": {
          "author": {
            "type": "string"
          },
          "created": {
            "type": "string"
          },
          "pinned": {
            "type": "boolean"
          },
          "value": {
            "type": "string",
            "description": "Comment text, at most **500** characters."
          },
          "valueTruncated": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "AgentPost": {
        "type": "object",
        "description": "One post. **`comments`** contains at most **8** comments.",
        "required": [
          "comments"
        ],
        "properties": {
          "post_id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "Canonical post URL: **`https://feedback.vrchat.com/{board}/p/{urlName}`**."
          },
          "board": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "score": {
            "type": "integer"
          },
          "commentCount": {
            "type": "integer",
            "description": "Full comment count on the post, which can be larger than **`comments.length`**."
          },
          "created": {
            "type": "string"
          },
          "details": {
            "type": "string",
            "description": "Post body, at most **4000** characters."
          },
          "detailsTruncated": {
            "type": "boolean"
          },
          "comments": {
            "type": "array",
            "maxItems": 8,
            "items": {
              "$ref": "#/components/schemas/AgentComment"
            }
          }
        },
        "additionalProperties": false
      },
      "Error": {
        "type": "object",
        "required": [
          "message"
        ],
        "properties": {
          "message": {
            "type": "string",
            "description": "**`message`**: **`string`**; **`detail`**: supplemental **`diagnostics`** (optional)."
          },
          "detail": {
            "type": "string",
            "description": "Supplemental diagnostics when **`message`** is **`Invalid search query`**; omit when **`body`** is not an **`InstantSearch`** **`array`**, etc."
          }
        },
        "additionalProperties": false
      }
    }
  }
}
