{
  "openapi": "3.1.0",
  "info": {
    "title": "VietnamSafe Public API",
    "version": "1.0.0",
    "summary": "Real-time Vietnam travel safety data for humans and AI agents.",
    "description": "Public, read-only REST API for Vietnam tourist safety data: per-region safety scores, active alerts, hospitals, beach conditions, road risk, verified scams, fair transport pricing, and AI-classified safety news.\n\nNo authentication is required for any endpoint in this spec. All responses are JSON in the envelope `{ \"success\": boolean, \"data\": ... }`; errors return `{ \"success\": false, \"error\": { \"code\", \"message\" } }`.\n\nRate limits (per IP): 100 requests/minute globally; `/regions/*` and `/news/*` are limited to 60 requests/minute. Exceeding the limit returns HTTP 429.\n\nAI agents can also connect via the Model Context Protocol at https://api.vietnamsafe.app/mcp. For commercial/high-volume access see https://vietnamsafe.app/data.",
    "contact": {
      "name": "VietnamSafe API",
      "email": "api@thaisafe.app",
      "url": "https://vietnamsafe.app/agents"
    },
    "termsOfService": "https://vietnamsafe.app/terms"
  },
  "servers": [
    {
      "url": "https://api.vietnamsafe.app",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Regions",
      "description": "Aggregated per-region safety summaries with live scores."
    },
    {
      "name": "Alerts",
      "description": "Active safety alerts (weather, road, health, security, police checkpoints)."
    },
    {
      "name": "Safety",
      "description": "Hospitals, beaches, roads, scams, and transport pricing."
    },
    {
      "name": "News",
      "description": "Safety-relevant news from Thai outlets, AI-classified by region and category."
    }
  ],
  "paths": {
    "/regions/{slug}/summary": {
      "get": {
        "tags": [
          "Regions"
        ],
        "operationId": "getRegionSummary",
        "summary": "Live safety summary for a region",
        "description": "Returns the current safety picture for one region: composite safety score (overall, alert, report, road sub-scores with a green/yellow/red color band, recomputed every 15 minutes), counts of active alerts and recent community reports, hospital and beach counts, and up to 5 curated verified community quotes. Cached for 5 minutes. Scores are computed from community incident reports, weather alerts and road data; news coverage is deliberately excluded because it measures newsworthiness, not danger. When a region has no scoreable signal, `score.status` is `insufficient_data` and every score field is null — that means unknown, NOT safe, and must not be rendered as a score or a color.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Region slug, e.g. `bangkok`, `phuket`, `chiang-mai`, `pattaya`, `krabi`, `koh-samui`. Max 50 characters.",
            "schema": {
              "type": "string",
              "maxLength": 50,
              "examples": [
                "phuket"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Region summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/RegionSummary"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/alerts": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "operationId": "listAlerts",
        "summary": "List active safety alerts",
        "description": "Paginated list of currently active alerts, newest first. Alerts include weather warnings, road hazards, health advisories, security incidents, and community-reported police checkpoints. Filter by region with `?region=`.",
        "parameters": [
          {
            "name": "region",
            "in": "query",
            "required": false,
            "description": "Region slug filter, e.g. `phuket`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Active alerts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Alert"
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "pageSize": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/alerts/{id}": {
      "get": {
        "tags": [
          "Alerts"
        ],
        "operationId": "getAlert",
        "summary": "Get a single alert",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "Alert",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AlertEnvelope"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/hospitals": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "listHospitals",
        "summary": "Hospital directory",
        "description": "Hospitals with coordinates, phone and emergency phone numbers, 24-hour status, and ratings. Filter by `?region=`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Region"
          }
        ],
        "responses": {
          "200": {
            "description": "Hospitals",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HospitalListEnvelope"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/hospitals/{id}": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "getHospital",
        "summary": "Get a single hospital",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "Hospital",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Hospital"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/beaches": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "listBeaches",
        "summary": "Beach conditions",
        "description": "Beach directory with hazard lists. Safety conditions (flag status, current strength, water quality) are returned only for beaches with `monitored: true`; unmonitored beaches omit those fields entirely rather than reporting an unverified value. Filter by `?province=`. Pass `?locale=` (e.g. `th`, `fr`, `ja`) to receive machine-translated name/location/province fields.",
        "parameters": [
          {
            "name": "province",
            "in": "query",
            "required": false,
            "description": "Province slug filter, e.g. `phuket`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "Beaches",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Beach"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/beaches/{id}": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "getBeach",
        "summary": "Get a single beach",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "Beach",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Beach"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/roads": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "listRoads",
        "summary": "Road safety data",
        "description": "Roads with traffic level, scooter safety rating, hazards, accident rate (decayed 1%/day since last update), fatality counts, and tourist risk score. Returns at most 200 rows. Filter by `?region=`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/Region"
          },
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "Roads",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Road"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/roads/{id}": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "getRoad",
        "summary": "Get a single road",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "Road",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Road"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/scams": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "listScams",
        "summary": "Verified tourist scam database",
        "description": "Known tourist scams ordered by report count (most reported first), with location, severity, and verification status.",
        "responses": {
          "200": {
            "description": "Scams",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Scam"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/scams/{id}": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "getScam",
        "summary": "Get a single scam report",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          }
        ],
        "responses": {
          "200": {
            "description": "Scam",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/Scam"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/safety/transport": {
      "get": {
        "tags": [
          "Safety"
        ],
        "operationId": "listTransportRoutes",
        "summary": "Fair transport pricing guide",
        "description": "Fair vs. typical tourist prices (THB) for common taxi, tuk-tuk, boat, and train routes — useful for detecting overcharging.",
        "responses": {
          "200": {
            "description": "Transport routes",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TransportRoute"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/news": {
      "get": {
        "tags": [
          "News"
        ],
        "operationId": "listNews",
        "summary": "List safety news articles",
        "description": "Paginated safety-relevant news, newest first. `category`, `region`, and `source` accept comma-separated lists. `minRelevance` (0-1) filters by AI relevance score. Responses are cached for up to 5 minutes.",
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated categories, e.g. `crime,weather`."
          },
          {
            "name": "region",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated region slugs."
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated source slugs."
          },
          {
            "name": "minRelevance",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "News articles",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsArticle"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "integer"
                        },
                        "pageSize": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/news/feed": {
      "get": {
        "tags": [
          "News"
        ],
        "operationId": "getNewsFeed",
        "summary": "Curated safety news feed",
        "description": "High-relevance (score >= 0.5) safety news, newest first. Pass `?locale=` for machine-translated title/description.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "Curated feed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsArticle"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/news/categories": {
      "get": {
        "tags": [
          "News"
        ],
        "operationId": "listNewsCategories",
        "summary": "News categories with counts",
        "responses": {
          "200": {
            "description": "Categories",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "category": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/news/{id}": {
      "get": {
        "tags": [
          "News"
        ],
        "operationId": "getNewsArticle",
        "summary": "Get a single news article",
        "parameters": [
          {
            "$ref": "#/components/parameters/Id"
          },
          {
            "$ref": "#/components/parameters/Locale"
          }
        ],
        "responses": {
          "200": {
            "description": "Article",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data"
                  ],
                  "properties": {
                    "success": {
                      "const": true
                    },
                    "data": {
                      "$ref": "#/components/schemas/NewsArticle"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Id": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        }
      },
      "Region": {
        "name": "region",
        "in": "query",
        "required": false,
        "description": "Region slug filter, e.g. `phuket`, `bangkok`.",
        "schema": {
          "type": "string"
        }
      },
      "Locale": {
        "name": "locale",
        "in": "query",
        "required": false,
        "description": "BCP-47-style locale code (e.g. `th`, `zh`, `ja`, `fr`). When set and not `en`, translatable text fields are machine-translated.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid parameters",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (60-100 requests/minute per IP depending on route)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorEnvelope"
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorEnvelope": {
        "type": "object",
        "required": [
          "success",
          "error"
        ],
        "properties": {
          "success": {
            "const": false
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "examples": [
                  "NOT_FOUND",
                  "INVALID_PARAM",
                  "RATE_LIMITED"
                ]
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "RegionSummary": {
        "type": "object",
        "required": [
          "region",
          "score",
          "counts",
          "quotes"
        ],
        "properties": {
          "region": {
            "type": "string",
            "description": "Region slug."
          },
          "score": {
            "type": "object",
            "required": [
              "status",
              "overall",
              "alert",
              "report",
              "road",
              "colorBand",
              "computedAt"
            ],
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "ok",
                  "insufficient_data"
                ],
                "description": "`insufficient_data` means no component had data and every field below is null. Treat it as unknown, never as safe."
              },
              "overall": {
                "type": [
                  "number",
                  "null"
                ],
                "minimum": 0,
                "maximum": 10,
                "description": "Composite safety score, 0 (unsafe) to 10 (safest). Null when status is insufficient_data."
              },
              "alert": {
                "type": [
                  "number",
                  "null"
                ],
                "minimum": 0,
                "maximum": 10,
                "description": "Null when no alert data was available for this region."
              },
              "report": {
                "type": [
                  "number",
                  "null"
                ],
                "minimum": 0,
                "maximum": 10,
                "description": "Null when no community reports were available for this region."
              },
              "road": {
                "type": [
                  "number",
                  "null"
                ],
                "minimum": 0,
                "maximum": 10,
                "description": "Null when no road data was available for this region."
              },
              "colorBand": {
                "type": [
                  "string",
                  "null"
                ],
                "enum": [
                  "green",
                  "yellow",
                  "red",
                  null
                ]
              },
              "computedAt": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time",
                "description": "Null when status is insufficient_data — nothing was computed, so there is no timestamp."
              }
            }
          },
          "counts": {
            "type": "object",
            "required": [
              "activeAlerts",
              "recentReports",
              "verifiedReports",
              "hospitals",
              "beaches"
            ],
            "properties": {
              "activeAlerts": {
                "type": "integer"
              },
              "recentReports": {
                "type": "integer",
                "description": "Approved community reports in the last 30 days."
              },
              "verifiedReports": {
                "type": "integer"
              },
              "hospitals": {
                "type": "integer"
              },
              "beaches": {
                "type": "integer"
              }
            }
          },
          "quotes": {
            "type": "array",
            "maxItems": 5,
            "description": "Curated verified community reports (PII scrubbed), highest-upvoted first.",
            "items": {
              "type": "object",
              "required": [
                "id",
                "description",
                "type",
                "upvotes",
                "createdAt"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "type": {
                  "type": "string"
                },
                "upvotes": {
                  "type": "integer"
                },
                "createdAt": {
                  "type": "string",
                  "format": "date-time"
                }
              }
            }
          }
        }
      },
      "Alert": {
        "type": "object",
        "required": [
          "id",
          "type",
          "severity",
          "title",
          "description",
          "region",
          "latitude",
          "longitude",
          "isActive",
          "startTime",
          "source",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "Alert type, e.g. `weather`, `road`, `health`, `security`, `police`."
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "danger"
            ]
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "isActive": {
            "type": "boolean"
          },
          "startTime": {
            "type": "string",
            "format": "date-time"
          },
          "endTime": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "source": {
            "type": "string",
            "description": "Origin of the alert, e.g. `community`, `news`."
          },
          "metadata": {
            "type": [
              "object",
              "null"
            ],
            "description": "Public incident metadata (aggregate confirm/deny counts; voter identities removed).",
            "properties": {
              "confirms": {
                "type": "integer"
              },
              "denies": {
                "type": "integer"
              },
              "direction": {
                "type": "string"
              },
              "note": {
                "type": "string"
              }
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AlertEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "data": {
            "$ref": "#/components/schemas/Alert"
          }
        }
      },
      "Hospital": {
        "type": "object",
        "required": [
          "id",
          "name",
          "nameLocal",
          "latitude",
          "longitude",
          "phone",
          "emergencyPhone",
          "rating",
          "isOpen24h",
          "address",
          "region"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "nameLocal": {
            "type": "string",
            "description": "Thai-script name."
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "phone": {
            "type": "string"
          },
          "emergencyPhone": {
            "type": "string"
          },
          "rating": {
            "type": "number",
            "minimum": 0,
            "maximum": 5
          },
          "isOpen24h": {
            "type": "boolean"
          },
          "address": {
            "type": "string"
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "region": {
            "type": "string"
          }
        }
      },
      "HospitalListEnvelope": {
        "type": "object",
        "required": [
          "success",
          "data"
        ],
        "properties": {
          "success": {
            "const": true
          },
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Hospital"
            }
          }
        }
      },
      "Beach": {
        "type": "object",
        "required": [
          "id",
          "name",
          "location",
          "province",
          "description",
          "latitude",
          "longitude",
          "monitored",
          "hazards",
          "rating",
          "isPopular",
          "lastUpdated"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "province": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "monitored": {
            "type": "boolean",
            "description": "Whether this beach has a verified safety assessment. When false, `flagStatus`, `currentStrength` and `waterQuality` are omitted entirely — no swim-safety data exists for this beach. Do not treat an absent flag as safe."
          },
          "flagStatus": {
            "type": "string",
            "enum": [
              "green",
              "yellow",
              "red"
            ],
            "description": "Swimming flag: green = safe, yellow = caution, red = do not swim. Only present when `monitored` is true."
          },
          "currentStrength": {
            "type": "string",
            "description": "e.g. `calm`, `moderate`, `strong`. Only present when `monitored` is true."
          },
          "waterQuality": {
            "type": "string",
            "description": "e.g. `good`, `fair`, `poor`. Only present when `monitored` is true."
          },
          "hazards": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "rating": {
            "type": "number",
            "minimum": 0,
            "maximum": 5
          },
          "isPopular": {
            "type": "boolean"
          },
          "lastUpdated": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Road": {
        "type": "object",
        "required": [
          "id",
          "name",
          "location",
          "province",
          "region",
          "description",
          "latitude",
          "longitude",
          "roadType",
          "trafficLevel",
          "scooterSafety",
          "hazards",
          "connectedBeaches",
          "topCauses",
          "lastUpdated"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "province": {
            "type": "string"
          },
          "region": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "roadType": {
            "type": "string",
            "description": "e.g. `highway`, `arterial`, `mountain`."
          },
          "trafficLevel": {
            "type": "string",
            "description": "e.g. `light`, `moderate`, `heavy`."
          },
          "scooterSafety": {
            "type": "string",
            "description": "e.g. `safe`, `caution`, `dangerous`."
          },
          "hazards": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "speedLimit": {
            "type": [
              "integer",
              "null"
            ],
            "description": "km/h."
          },
          "connectedBeaches": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "accidentRate": {
            "type": [
              "number",
              "null"
            ],
            "description": "Accidents per period, decayed 1%/day since lastUpdated."
          },
          "fatalityCount": {
            "type": [
              "integer",
              "null"
            ]
          },
          "topCauses": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "dataSource": {
            "type": [
              "string",
              "null"
            ]
          },
          "touristRiskScore": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 10
          },
          "lastUpdated": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Scam": {
        "type": "object",
        "required": [
          "id",
          "type",
          "title",
          "description",
          "location",
          "latitude",
          "longitude",
          "reportCount",
          "severity",
          "verified",
          "dateReported"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "Scam category, e.g. `taxi`, `gem`, `rental`."
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "location": {
            "type": "string"
          },
          "latitude": {
            "type": "number"
          },
          "longitude": {
            "type": "number"
          },
          "reportCount": {
            "type": "integer"
          },
          "severity": {
            "type": "string",
            "enum": [
              "low",
              "medium",
              "high"
            ]
          },
          "verified": {
            "type": "boolean"
          },
          "dateReported": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TransportRoute": {
        "type": "object",
        "required": [
          "id",
          "type",
          "from",
          "to",
          "fairPrice",
          "touristPrice",
          "currency",
          "duration",
          "verified"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "description": "Transport mode, e.g. `taxi`, `tuktuk`, `boat`, `train`."
          },
          "from": {
            "type": "string"
          },
          "to": {
            "type": "string"
          },
          "fairPrice": {
            "type": "integer",
            "description": "Fair local price in THB."
          },
          "touristPrice": {
            "type": "integer",
            "description": "Typical inflated tourist price in THB."
          },
          "currency": {
            "type": "string",
            "default": "THB"
          },
          "duration": {
            "type": "string"
          },
          "notes": {
            "type": [
              "string",
              "null"
            ]
          },
          "verified": {
            "type": "boolean"
          }
        }
      },
      "NewsArticle": {
        "type": "object",
        "required": [
          "id",
          "source",
          "feed",
          "title",
          "description",
          "sourceUrl",
          "publishedAt",
          "relevanceScore",
          "isAlertWorthy",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "description": "Source slug, e.g. `bangkokpost`."
          },
          "feed": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "aiSummary": {
            "type": [
              "string",
              "null"
            ],
            "description": "AI-generated safety-focused summary."
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri"
          },
          "urlSection": {
            "type": [
              "string",
              "null"
            ]
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "category": {
            "type": [
              "string",
              "null"
            ],
            "description": "AI-classified category, e.g. `crime`, `weather`, `traffic`."
          },
          "region": {
            "type": [
              "string",
              "null"
            ],
            "description": "AI-classified region slug."
          },
          "relevanceScore": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "isAlertWorthy": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}