{
  "openapi": "3.1.0",
  "info": {
    "title": "BaanScope Public Data API",
    "version": "1.0.0",
    "summary": "Read-only access to BaanScope's Bangkok new-build property datasets and page content.",
    "description": "BaanScope tracks every new-build (off-plan and under-construction) residential project from Thailand's 9 largest SET-listed developers in the Bangkok Metropolitan Region — 365 projects at the current scan, re-checked against developer websites roughly every 2 days.\n\nThis API is free, needs no authentication and no API key, and is rate-limited only by ordinary CDN fair use. Aggregated datasets are licensed CC BY 4.0; attribution is the only condition.\n\n**Read this before quoting a figure.** Thailand has no public transaction registry. Every price exposed here is a developer's own published asking price, never a sale price. Coverage is new-build stock from the tracked developers only — not resale, and not the whole market.",
    "contact": {
      "name": "Cesar Rodriguez",
      "email": "info@baanscope.com",
      "url": "https://baanscope.com/contact"
    },
    "license": {
      "name": "CC BY 4.0",
      "url": "https://creativecommons.org/licenses/by/4.0/"
    },
    "termsOfService": "https://baanscope.com/about"
  },
  "servers": [
    {
      "url": "https://baanscope.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Human-readable API and open-data documentation",
    "url": "https://baanscope.com/api-docs"
  },
  "tags": [
    {
      "name": "datasets",
      "description": "Aggregated CSV/JSON downloads of the tracked pipeline."
    },
    {
      "name": "content",
      "description": "Machine-readable representations of site pages."
    },
    {
      "name": "reference",
      "description": "Supporting reference data used by the site."
    },
    {
      "name": "discovery",
      "description": "Documents that describe the site to agents and crawlers."
    }
  ],
  "paths": {
    "/data/{file}": {
      "get": {
        "operationId": "getDataset",
        "tags": [
          "datasets"
        ],
        "summary": "Download one aggregated dataset as CSV or JSON",
        "description": "Returns one of the 4 published aggregates of the tracked project pipeline, cut by district, by developer, by completion year or by rail line. Each row carries project counts, published unit counts, property mix, median entry price, median price per m² and median appraised land value. Per-group price statistics are omitted where the group has fewer than 3 projects, so a null median means \"too few projects to publish\", not \"zero\". The JSON variant wraps the rows in an envelope carrying the licence, the coverage caveat and a column dictionary; the CSV variant is the bare table with a header row.",
        "parameters": [
          {
            "name": "file",
            "in": "path",
            "required": true,
            "description": "Dataset id plus format extension. Both formats contain identical rows.",
            "schema": {
              "type": "string",
              "enum": [
                "districts.csv",
                "districts.json",
                "developers.csv",
                "developers.json",
                "completion-wave.csv",
                "completion-wave.json",
                "rail-lines.csv",
                "rail-lines.json"
              ]
            },
            "example": "districts.json"
          }
        ],
        "responses": {
          "200": {
            "description": "The dataset. Content type follows the requested extension.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "description": "Always `*` — the files are CC BY 4.0 and meant to be fetched cross-origin.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatasetEnvelope"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "RFC 4180 CSV with a header row."
                }
              }
            }
          },
          "404": {
            "description": "Unknown dataset id or unsupported extension.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/fx": {
      "get": {
        "operationId": "getFxRates",
        "tags": [
          "reference"
        ],
        "summary": "Current THB reference exchange rates",
        "description": "Reference rates used by the site's currency switcher, expressed as Thai baht per one unit of the foreign currency. Sourced from ECB reference rates with a second public source as backup, revalidated every 12 hours, and falling back to a committed snapshot if both are unreachable. Indicative only — not a dealing rate.",
        "responses": {
          "200": {
            "description": "Rates, the date the source published them, and which source was used.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FxRates"
                }
              }
            }
          }
        }
      }
    },
    "/md/{path}": {
      "get": {
        "operationId": "getPageMarkdown",
        "tags": [
          "content"
        ],
        "summary": "Markdown representation of any page",
        "description": "Returns the content of a site page as markdown rather than HTML — the same content a reader sees, without navigation chrome, scripts or styling. The identical representation is served from the page's own URL through content negotiation: send `Accept: text/markdown` to any page and you get this document back. Responses carry `Vary: Accept`. Unknown paths return 404 with a short markdown body listing where to look instead.",
        "parameters": [
          {
            "name": "path",
            "in": "path",
            "required": true,
            "description": "Site path without the leading slash, optionally locale-prefixed. Use \"index\" or an empty segment for the homepage. Examples: \"projects\", \"data\", \"blog/bangkok-rental-yields\", \"th/guide\".",
            "schema": {
              "type": "string"
            },
            "example": "projects"
          }
        ],
        "responses": {
          "200": {
            "description": "The page as markdown.",
            "headers": {
              "Vary": {
                "description": "Always includes `Accept`, so caches key the markdown and HTML variants separately.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "No such page. Body is markdown pointing at the sitemap and llms.txt.",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "406": {
            "description": "The Accept header names media types and none of them can be served for this URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiDocument",
        "tags": [
          "discovery"
        ],
        "summary": "This document",
        "description": "The OpenAPI 3.1 description of this API. Stable URL, no authentication, safe to fetch on every run.",
        "responses": {
          "200": {
            "description": "The OpenAPI document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsTxt",
        "tags": [
          "discovery"
        ],
        "summary": "Site overview for language models",
        "description": "An llmstxt.org context file: what the site covers, when to use it, the caveats that apply to its figures, and links to the main sections and machine-readable resources. The best first fetch for an agent that has not seen this site before.",
        "responses": {
          "200": {
            "description": "Plain-text llms.txt document.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Uniform error body returned by every endpoint. `code` is stable and safe to branch on; `hint` says what the caller should do next.",
        "required": [
          "error",
          "code",
          "message",
          "hint",
          "status",
          "docs"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Legacy alias of `code`, kept for existing clients. Always identical to `code`.",
            "enum": [
              "invalid-json",
              "not-an-object",
              "bad-type",
              "bad-email",
              "bad-calc-rows",
              "rate-limited",
              "not-configured",
              "send-failed",
              "subscribe-failed",
              "not-found",
              "method-not-allowed",
              "unsupported-media-type",
              "not-acceptable"
            ]
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code.",
            "enum": [
              "invalid-json",
              "not-an-object",
              "bad-type",
              "bad-email",
              "bad-calc-rows",
              "rate-limited",
              "not-configured",
              "send-failed",
              "subscribe-failed",
              "not-found",
              "method-not-allowed",
              "unsupported-media-type",
              "not-acceptable"
            ]
          },
          "message": {
            "type": "string",
            "description": "One-sentence description of what went wrong."
          },
          "hint": {
            "type": "string",
            "description": "The concrete next action that resolves it."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status, repeated in the body so logged payloads are self-contained.",
            "enum": [
              400,
              404,
              405,
              406,
              415,
              429,
              502,
              503
            ]
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "URL of this OpenAPI document."
          }
        }
      },
      "FxRates": {
        "type": "object",
        "description": "Thai baht per one unit of each supported foreign currency.",
        "required": [
          "rates",
          "asOf",
          "source"
        ],
        "properties": {
          "rates": {
            "type": "object",
            "description": "THB per 1 unit. THB itself is implicit and always 1.",
            "required": [
              "USD",
              "EUR",
              "CNY"
            ],
            "properties": {
              "USD": {
                "type": "number",
                "description": "Thai baht per 1 USD."
              },
              "EUR": {
                "type": "number",
                "description": "Thai baht per 1 EUR."
              },
              "CNY": {
                "type": "number",
                "description": "Thai baht per 1 CNY."
              }
            }
          },
          "asOf": {
            "type": "string",
            "format": "date",
            "description": "Date the upstream source published these rates (YYYY-MM-DD)."
          },
          "source": {
            "type": "string",
            "description": "Which source answered, or `fallback` when both were unreachable.",
            "enum": [
              "frankfurter",
              "er-api",
              "fallback"
            ]
          }
        }
      },
      "DatasetColumn": {
        "type": "object",
        "description": "One column of a dataset, with the meaning needed to read it correctly.",
        "required": [
          "key",
          "label",
          "note"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Field name used in the JSON rows."
          },
          "label": {
            "type": "string",
            "description": "Human header, also the CSV header cell."
          },
          "note": {
            "type": "string",
            "description": "What the column means and how it was computed."
          }
        }
      },
      "DatasetEnvelope": {
        "type": "object",
        "description": "JSON download wrapper: provenance and column dictionary alongside the rows.",
        "required": [
          "dataset",
          "title",
          "license",
          "generated",
          "columns",
          "rowCount",
          "rows"
        ],
        "properties": {
          "dataset": {
            "type": "string",
            "description": "Dataset id.",
            "enum": [
              "districts",
              "developers",
              "completion-wave",
              "rail-lines"
            ]
          },
          "title": {
            "type": "string",
            "description": "Human title of the dataset."
          },
          "description": {
            "type": "string",
            "description": "What one row represents."
          },
          "source": {
            "type": "string",
            "description": "Attribution string for the publisher."
          },
          "license": {
            "type": "string",
            "description": "SPDX-style licence label.",
            "enum": [
              "CC BY 4.0"
            ]
          },
          "licenseUrl": {
            "type": "string",
            "format": "uri",
            "description": "Full licence text."
          },
          "attribution": {
            "type": "string",
            "description": "Ready-made credit line to reproduce."
          },
          "coverage": {
            "type": "string",
            "description": "What the rows do and do not include."
          },
          "generated": {
            "type": "string",
            "format": "date",
            "description": "Build date of this file — i.e. how fresh the underlying scan is."
          },
          "suppression": {
            "type": "string",
            "description": "Explains why some medians are null: groups below 3 projects are suppressed."
          },
          "columns": {
            "type": "array",
            "description": "Column dictionary, in column order.",
            "items": {
              "$ref": "#/components/schemas/DatasetColumn"
            }
          },
          "rowCount": {
            "type": "integer",
            "description": "Number of rows in `rows`."
          },
          "rows": {
            "type": "array",
            "description": "The data. Keys match `columns[].key`; null means not published.",
            "items": {
              "type": "object",
              "additionalProperties": {
                "type": [
                  "string",
                  "number",
                  "null"
                ]
              }
            }
          }
        }
      }
    }
  },
  "x-locales": {
    "description": "Every page is published in these locales. English is served unprefixed; others use a path prefix.",
    "values": [
      "en",
      "th",
      "zh"
    ]
  }
}
