{
  "openapi": "3.1.0",
  "info": {
    "title": "SRAC Site Search API",
    "version": "1.1.0",
    "summary": "Full-text search over the Social Rights Advocacy Centre research collection.",
    "description": "Public, read-only search over the full-text index of socialrights.ca (and socialrightscura.ca): HTML pages and PDF documents on social and economic rights, Charter litigation, housing, poverty, equality and international human rights law. PDFs are indexed page by page, with OCR used for scanned PDFs where possible; the index is rebuilt nightly when the site changes.\n\nRate limits: 300 requests per minute and 3,000 per hour per client IP; 1,200 requests per minute across all clients. Requests over the limit receive HTTP 429 with a `Retry-After` header.\n\nThis API is provided to facilitate research, retrieval, citation, and analysis of materials in the Social Rights Advocacy Centre collection. Please use reasonable request rates and cite original documents using the URLs provided in search results.",
    "contact": {
      "name": "Social Rights Advocacy Centre",
      "url": "https://www.socialrights.ca/"
    }
  },
  "externalDocs": {
    "description": "Human-readable documentation",
    "url": "https://www.socialrights.ca/resources/search-api.html"
  },
  "servers": [
    {
      "url": "https://jacksaiapps.com/srac"
    }
  ],
  "paths": {
    "/public-api/site-search": {
      "get": {
        "operationId": "siteSearch",
        "summary": "Search the collection",
        "description": "Full-text search. Terms are stemmed and matched as prefixes and combined with AND. Results are one per document (all pages of a PDF collapse into its best-matching page), ranked by relevance with HTML pages ordered above PDFs at equal relevance.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Search query. An empty query returns no results.",
            "example": "housing"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 20
            },
            "description": "Maximum number of results (default 20; values above 200 are clamped)."
          },
          {
            "name": "source_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restrict results to one HTML page and the PDFs it links to. Accepts a site-relative path (`Brown-v-Alberta.html`, `/resources/UN/CRC2022.html`) or a full socialrights.ca URL."
          },
          {
            "name": "source_pages",
            "in": "query",
            "required": false,
            "style": "form",
            "explode": true,
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Repeatable form of `source_page`; also accepts comma- or newline-separated values. Results are limited to those pages and the PDFs they link to."
          }
        ],
        "responses": {
          "200": {
            "description": "Search results.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string",
                      "description": "The normalised query that was run."
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SearchResult"
                      }
                    },
                    "scope": {
                      "type": "object",
                      "properties": {
                        "source_pages": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Normalised source pages the search was restricted to (empty for a full-site search)."
                        }
                      }
                    },
                    "build": {
                      "$ref": "#/components/schemas/Build"
                    }
                  },
                  "required": [
                    "query",
                    "results",
                    "build"
                  ]
                },
                "example": {
                  "query": "housing",
                  "results": [
                    {
                      "kind": "pdf",
                      "title": "Volume 11 of Heegsma AR",
                      "url": "https://www.socialrights.ca/Heegsma/Volume 11 of Heegsma AR.pdf#page=234",
                      "rel_path": "Heegsma/Volume 11 of Heegsma AR.pdf",
                      "page": 234,
                      "snippet": "…the right to adequate <mark>housing</mark>…",
                      "snippet_text": "…the right to adequate housing…",
                      "score": -12.4
                    }
                  ],
                  "scope": {
                    "source_pages": []
                  },
                  "build": {
                    "built_at": "2026-08-11T07:09:26+00:00",
                    "html_count": 141,
                    "pdf_count": 1684,
                    "row_count": 92027,
                    "citation_count": 1434,
                    "orphan_count": 755,
                    "warning_count": 4
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/IndexUnavailable"
          }
        }
      }
    },
    "/public-api/documents": {
      "get": {
        "operationId": "listDocuments",
        "summary": "List indexed documents",
        "description": "Paginated manifest of every document in the index — metadata only, no document text — so a client can learn the scope of the collection without harvesting it. Ordered by kind (pages first) then path, stable between requests for a given index build.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 100
            },
            "description": "Page size (default 100, max 200)."
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            },
            "description": "Zero-based offset; use the `next_offset` from the previous response."
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "page",
                "pdf"
              ]
            },
            "description": "Only HTML pages or only PDFs."
          },
          {
            "name": "source_page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only that HTML page and the PDFs it links to (same forms as the search endpoint)."
          }
        ],
        "responses": {
          "200": {
            "description": "One page of the manifest.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "documents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Document"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total documents matching the filters."
                    },
                    "offset": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "next_offset": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Offset of the next page, or null on the last page."
                    },
                    "filters": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "source_page": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    },
                    "build": {
                      "$ref": "#/components/schemas/Build"
                    }
                  },
                  "required": [
                    "documents",
                    "total",
                    "offset",
                    "limit",
                    "next_offset",
                    "build"
                  ]
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/IndexUnavailable"
          }
        }
      }
    },
    "/public-api/openapi.json": {
      "get": {
        "operationId": "getOpenApi",
        "summary": "This document",
        "responses": {
          "200": {
            "description": "The OpenAPI description of this API."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Build": {
        "type": "object",
        "description": "Metadata about the index build that answered the request.",
        "properties": {
          "built_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "UTC timestamp of the last successful index build (the index is rebuilt nightly when the site changes)."
          },
          "html_count": {
            "type": "integer",
            "description": "Number of HTML pages indexed."
          },
          "pdf_count": {
            "type": "integer",
            "description": "Number of PDF documents indexed."
          },
          "row_count": {
            "type": "integer",
            "description": "Total searchable rows (one per HTML page, one per PDF page)."
          },
          "citation_count": {
            "type": "integer",
            "description": "Number of page→PDF citation links recorded."
          },
          "orphan_count": {
            "type": "integer",
            "description": "PDFs indexed that no HTML page links to."
          },
          "warning_count": {
            "type": "integer",
            "description": "Non-fatal warnings raised during the build."
          }
        }
      },
      "SearchResult": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "pdf"
            ],
            "description": "`page` for an HTML page, `pdf` for a PDF document."
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public URL of the document. For PDFs this includes a `#page=N` fragment pointing at the best-matching page."
          },
          "rel_path": {
            "type": "string",
            "description": "Path of the document relative to the site root."
          },
          "page": {
            "type": "integer",
            "description": "1-based PDF page number of the best-matching page; 0 for HTML pages."
          },
          "snippet": {
            "type": "string",
            "description": "Matching passage with the query terms wrapped in `<mark>` tags (HTML-escaped otherwise)."
          },
          "snippet_text": {
            "type": "string",
            "description": "The same passage as plain text, without highlight markup."
          },
          "score": {
            "type": "number",
            "description": "BM25 relevance score. Negative; more negative is a better match. Only comparable within one response."
          }
        },
        "required": [
          "kind",
          "title",
          "url",
          "rel_path",
          "page",
          "snippet",
          "snippet_text",
          "score"
        ]
      },
      "Document": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "pdf"
            ]
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public URL of the document (no page fragment)."
          },
          "rel_path": {
            "type": "string"
          },
          "page_count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Number of pages in the PDF where known; null for HTML pages."
          },
          "indexed_pages": {
            "type": "integer",
            "description": "Rows in the search index for this document (pages with extractable text)."
          },
          "source_pages": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "For PDFs: rel_paths of the HTML pages that link to it. Values are accepted by the search endpoint's `source_page` parameter."
          }
        },
        "required": [
          "kind",
          "title",
          "url",
          "rel_path",
          "page_count",
          "indexed_pages",
          "source_pages"
        ]
      }
    },
    "responses": {
      "RateLimited": {
        "description": "Rate limit exceeded. Wait for the number of seconds in `Retry-After` and try again.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              },
              "required": [
                "error"
              ]
            }
          }
        },
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds until the limit resets."
          }
        }
      },
      "IndexUnavailable": {
        "description": "The search index is temporarily unavailable (for example, mid-rebuild). Retry shortly.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              },
              "required": [
                "error"
              ]
            }
          }
        }
      }
    }
  }
}
