openapi: 3.1.0

info:
  title: redflag-check API
  summary: Address-based Red Flag Warning lookups, school decisions, and buddy-check templates.
  description: |
    Free, public, no-auth REST API wrapping NWS Red Flag Warning data, the US Census geocoder,
    and Genasys evacuation zones into a single integration-friendly surface.

    **Why this exists:** during a Red Flag Warning, county-wide AC Alert texts often go to
    everyone in an entire county even though the active NWS warning polygon only covers a small
    zone. Half the residents are told to prepare a go-bag they don't need; others in the actual
    zone don't realize how close they are. This API closes that gap by giving any client an
    address-resolved verdict, plain-English action checklist, and one-tap share template.

    **Designed for:**

    - County PIO and OES dashboards (surface address-level RFW status)
    - School-district websites (campus-level decisions)
    - News widgets (live coverage embedding)
    - Mutual-aid apps (programmatic buddy-check generation)
    - Insurance and utility internal dashboards
    - Personal projects, automations, scripts

    **No key required.** Open CORS (`Access-Control-Allow-Origin: *`). Cached at the Vercel edge
    for 60 seconds. If you sustain over ~1 req/sec, contact the maintainer to plan capacity.

    **Disclaimer:** informational only, not an official emergency service. For official alerts,
    sign up at AC Alert. In case of fire, call 911. The full Terms of Use & Disclaimer are at
    https://redflag-check.info/terms
  version: 1.0.0
  contact:
    name: redflag-check on GitHub
    url: https://github.com/vedant-f-is-ma/redflag-check
  license:
    name: MIT
    url: https://github.com/vedant-f-is-ma/redflag-check/blob/main/LICENSE
  x-built-by: Vedant Thakker, Fremont CA, 2026

servers:
  - url: https://redflag-check.info/api/v1
    description: Production (custom domain)
  - url: https://redflag-check.vercel.app/api/v1
    description: Production (permanent Vercel alias)

tags:
  - name: zone-check
    description: Per-address Red Flag Warning verdict, forecast, and action checklist.
  - name: status
    description: List of active Red Flag Warnings across a US state.
  - name: schools
    description: Find schools near a point (statewide CA public-school directory) and per-campus decision views.
  - name: buddy
    description: Generated check-in templates (SMS, email, .ics calendar) for neighbor outreach.
  - name: health
    description: Service health and upstream API status.

paths:
  /zone-check:
    get:
      tags: [zone-check]
      operationId: getZoneCheck
      summary: Check whether a Red Flag Warning currently covers an address or coordinate.
      description: |
        Geocodes the address (US Census), pulls active NWS alerts at the point, fetches
        hourly forecast (wind, RH), builds a category-aware action checklist
        (`in_zone` / `adjacent` / `out_of_zone` / `data_unavailable`), and returns pre-built
        Genasys evacuation-zone and AC Alert sign-up links. Either `address` or both `lat`
        and `lng` must be provided.

        **Fail-safe behaviour:** if the NWS data needed to justify a "safe" answer cannot be
        retrieved (outage, timeout), the response is still HTTP 200 but the verdict state is
        `data_unavailable`, `in_red_flag_zone` is `null` (unknown — never a false `false`),
        `data_status.degraded` is `true`, and the checklist tells the user to assume warning
        conditions and verify at weather.gov. Integrators must treat `data_unavailable` as
        "act as if warned", not as "no warning".
      parameters:
        - name: address
          in: query
          required: false
          description: Full US address (will be geocoded via US Census).
          schema:
            type: string
          example: "12250 Skyline Blvd, Oakland, CA"
        - name: lat
          in: query
          required: false
          description: Latitude in decimal degrees (use with `lng` instead of `address`).
          schema:
            type: number
            format: float
          example: 37.7811
        - name: lng
          in: query
          required: false
          description: Longitude in decimal degrees.
          schema:
            type: number
            format: float
          example: -122.1556
      responses:
        "200":
          description: Verdict + alerts + forecast + checklist + links.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ZoneCheckResponse"
        "422":
          description: Missing or invalid parameters.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }
        "502":
          description: Upstream API (NWS / Census / Genasys) is degraded.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /status:
    get:
      tags: [status]
      operationId: getStatus
      summary: All active Red Flag Warnings in a US state.
      description: |
        Returns every currently active NWS Red Flag Warning for the requested state.
        Defaults to California. Useful for news widgets, county PIO dashboards, or any
        system that just wants the raw list of current RFWs.
      parameters:
        - name: area
          in: query
          required: false
          description: Two-letter US state code.
          schema:
            type: string
            default: CA
            pattern: "^[A-Z]{2}$"
          example: CA
      responses:
        "200":
          description: List of active warnings.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StatusResponse" }
        "502":
          description: NWS upstream error.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /schools:
    get:
      tags: [schools]
      operationId: listSchools
      summary: Find schools near a point, or search all California public schools by name.
      description: |
        Server-side school lookup over the full California Department of Education public-school
        directory (~10.5k active schools) plus an individually-verified curated set. The full
        list is never shipped to the client; pick a mode via query params:

        * **Radius** — `?lat=&lng=[&radius_mi=]` returns schools within the radius (default 10 mi),
          nearest first, each with `distance_mi`. In sparse/rural areas the radius auto-expands
          until a few results are found (`auto_expanded: true`, up to 60 mi); in dense areas the
          list is capped (`capped: true`) — check `total_within_radius` for the true count.
        * **Search** — `?q=` matches by name or city, tokenized so word order doesn't matter (for
          open-enrollment, magnet, or boarding-school cases where the nearest campus isn't relevant).
        * **No params** — returns only the curated, individually-verified set (backward compatible).

        Every returned record carries the same `id` used by `/school-status?id=...`.
      parameters:
        - name: lat
          in: query
          required: false
          description: Latitude for a radius query (pair with `lng`).
          schema: { type: number }
          example: 32.7157
        - name: lng
          in: query
          required: false
          description: Longitude for a radius query (pair with `lat`).
          schema: { type: number }
          example: -117.1611
        - name: radius_mi
          in: query
          required: false
          description: Search radius in miles for a radius query. Default 10, capped at 60.
          schema: { type: number, default: 10 }
        - name: q
          in: query
          required: false
          description: Name/city search text, tokenized (word order does not matter).
          schema: { type: string }
          example: Torrey Pines High
      responses:
        "200":
          description: Nearby schools, search matches, or the curated list, depending on the mode.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SchoolsResponse" }
        "400":
          description: Invalid lat/lng.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /school-status:
    get:
      tags: [schools]
      operationId: getSchoolStatus
      summary: Per-school decision view (warning status + forecast + recommended action).
      description: |
        Returns the zone status at the school's address, tonight's wind and humidity forecast,
        and a recommended-action level (`normal` / `modify_outdoor` / `indoors_only` /
        `consider_closure`) with rationale citing CIF AQI thresholds and standard fire-weather
        practice. Decisions remain with district leadership; this is informational only.
      parameters:
        - name: id
          in: query
          required: true
          description: School id from `/schools`.
          schema:
            type: string
          example: skyline-oakland
      responses:
        "200":
          description: Per-school decision view.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SchoolStatusResponse" }
        "404":
          description: Unknown school id.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /buddy-template:
    get:
      tags: [buddy]
      operationId: getBuddyTemplate
      summary: Generate a check-in template for a neighbor or vulnerable contact.
      description: |
        Returns a ready-to-send iMessage text, mailto link, downloadable .ics calendar
        reminder, and (if `friend_lat`/`friend_lng` are provided) the friend's current
        Red Flag zone status. Built for mutual-aid networks, community organizers, school
        PTAs, and anyone who wants to programmatically generate buddy-check messages.
      parameters:
        - name: name
          in: query
          required: false
          description: Friend's first name (used to personalize the template).
          schema:
            type: string
            default: "your neighbor"
          example: Jane
        - name: time
          in: query
          required: false
          description: ISO 8601 reminder time (defaults to 22:30 PT today).
          schema:
            type: string
            format: date-time
          example: "2026-06-10T22:30:00-07:00"
        - name: friend_lat
          in: query
          required: false
          description: Friend's latitude (for zone lookup).
          schema:
            type: number
            format: float
          example: 37.7811
        - name: friend_lng
          in: query
          required: false
          description: Friend's longitude.
          schema:
            type: number
            format: float
          example: -122.1556
      responses:
        "200":
          description: Generated templates and friend zone status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BuddyTemplateResponse" }
        "422":
          description: Invalid time format.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorResponse" }

  /health:
    get:
      tags: [health]
      operationId: getHealth
      summary: Service health and upstream API status.
      description: |
        Probes NWS, US Census, and Genasys with HEAD requests and reports each upstream's
        reachability and latency. Useful for integrators to monitor.
      responses:
        "200":
          description: Service + upstream status.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/HealthResponse" }

components:
  schemas:

    ZoneCheckResponse:
      type: object
      required: [input, location, in_red_flag_zone, alerts, action_checklist, links, sources, disclaimer, generated_at]
      properties:
        input:
          type: object
          properties:
            address:   { type: [string, "null"], nullable: true }
            lat_requested: { type: [number, "null"], nullable: true }
            lng_requested: { type: [number, "null"], nullable: true }
        location:    { $ref: "#/components/schemas/Location" }
        in_red_flag_zone:
          type: [boolean, "null"]
          nullable: true
          description: >
            True if at least one active NWS Red Flag Warning covers the resolved point.
            null when the NWS point query failed and containment could not be established —
            unknown, deliberately never reported as false during an outage.
        data_status:
          $ref: "#/components/schemas/DataStatus"
        alerts:
          type: array
          items: { $ref: "#/components/schemas/Alert" }
        forecast:
          oneOf:
            - { $ref: "#/components/schemas/Forecast" }
            - type: "null"
          description: May be null if the NWS gridpoint forecast endpoint errors for the point.
        action_checklist:
          $ref: "#/components/schemas/ActionChecklist"
        links:
          $ref: "#/components/schemas/Links"
        sources:
          type: array
          items: { type: string }
          example: ["NWS api.weather.gov", "US Census geocoder", "Genasys Protect"]
        disclaimer: { type: string }
        generated_at:
          type: string
          format: date-time

    Location:
      type: object
      required: [lat, lng]
      properties:
        lat: { type: number, format: float, example: 37.7811 }
        lng: { type: number, format: float, example: -122.1556 }
        matched_address:
          type: string
          description: Canonical address returned by US Census geocoder.
          example: "12250 SKYLINE BLVD, OAKLAND, CA, 94619"
        zip:
          type: [string, "null"]
          nullable: true
          example: "94619"

    Alert:
      type: object
      properties:
        id:           { type: string }
        event:        { type: string, example: "Red Flag Warning" }
        headline:     { type: string }
        description:  { type: string }
        instruction:  { type: [string, "null"], nullable: true }
        starts:       { type: string, format: date-time }
        ends:         { type: string, format: date-time }
        expires:      { type: string, format: date-time }
        severity:     { type: string, example: "Severe" }
        sender_name:  { type: string, example: "NWS San Francisco CA" }
        affected_areas:
          type: array
          items: { type: string }
        affected_zones:
          type: array
          items: { type: string }

    Forecast:
      type: object
      properties:
        tonight:
          type: object
          properties:
            max_wind_mph:    { type: number, example: 45 }
            min_humidity_pct:
              type: [number, "null"]
              nullable: true
              example: 14
            summary: { type: string, example: "Windy" }
        next_24h:
          type: array
          items: { $ref: "#/components/schemas/ForecastHour" }

    ForecastHour:
      type: object
      properties:
        time:         { type: string, format: date-time }
        wind_mph:     { type: number }
        humidity_pct: { type: [number, "null"], nullable: true }
        summary:      { type: string }

    ActionChecklist:
      type: object
      required: [category, do_now, do_not, if_evacuation_called]
      properties:
        category:
          type: string
          enum: [in_zone, adjacent, out_of_zone, data_unavailable]
          description: >
            data_unavailable is the fail-safe category: live NWS data could not be
            retrieved, so the checklist assumes warning conditions persist.
        do_now:
          type: array
          items: { type: string }
        do_not:
          type: array
          items: { type: string }
        if_evacuation_called:
          type: array
          items: { type: string }

    DataStatus:
      type: object
      description: >
        Which live upstream sources failed while building this response. When degraded
        is true the response was computed without some safety-relevant data; a would-be
        "safe_tonight" verdict is replaced by "data_unavailable" in that case, and the
        response is served with Cache-Control no-store.
      properties:
        degraded: { type: boolean }
        failed_sources:
          type: array
          items:
            type: string
            enum: [point_alerts, regional_polygons, forecast]
        note:
          type: [string, "null"]
          nullable: true
          description: Plain-English caveat suitable for direct display.

    Links:
      type: object
      properties:
        genasys_evacuation_zone_lookup:
          type: string
          format: uri
          description: Pre-built Genasys Protect deeplink for the resolved point.
        official_ac_alert_signup:
          type: string
          format: uri
        watch_duty:
          type: string
          format: uri
        airnow_fire_map:
          type: string
          format: uri

    StatusResponse:
      type: object
      properties:
        area:  { type: string, example: "CA" }
        count: { type: integer, example: 4 }
        active_red_flag_warnings:
          type: array
          items: { $ref: "#/components/schemas/ActiveRedFlagWarning" }
        source:       { type: string, example: "NWS api.weather.gov" }
        disclaimer:   { type: string }
        generated_at: { type: string, format: date-time }

    ActiveRedFlagWarning:
      type: object
      properties:
        id:            { type: string }
        event:         { type: string, example: "Red Flag Warning" }
        headline:      { type: string }
        starts:        { type: string, format: date-time }
        ends:          { type: string, format: date-time }
        expires:       { type: string, format: date-time }
        severity:      { type: string }
        sender_name:   { type: string }
        affected_areas:
          type: array
          items: { type: string }
        affected_zones:
          type: array
          items: { type: string }
        instruction:   { type: [string, "null"], nullable: true }

    SchoolsResponse:
      type: object
      properties:
        mode:                { type: string, enum: [radius, search, curated] }
        count:               { type: integer, description: Number of schools returned (after any cap). }
        schools:
          type: array
          items: { $ref: "#/components/schemas/School" }
        # radius mode only
        applied_radius_mi:   { type: number, description: "Radius actually used (may exceed the requested value if auto-expanded)." }
        auto_expanded:       { type: boolean, description: "True if the radius was grown to find enough results." }
        total_within_radius: { type: integer, description: "True count within the applied radius, before the result cap." }
        # search mode only
        total_matches:       { type: integer, description: "True match count, before the result cap." }
        capped:              { type: boolean, description: "True if more results existed than were returned." }
        generated_at:        { type: string, format: date-time }

    School:
      type: object
      properties:
        id:           { type: string, example: "skyline-oakland", description: "Use with /school-status?id=..." }
        name:         { type: string, example: "Skyline High School" }
        district:     { type: string, example: "Oakland Unified" }
        address:      { type: string, example: "12250 Skyline Blvd" }
        city:         { type: string, example: "Oakland" }
        lat:          { type: number, example: 37.79749 }
        lng:          { type: number, example: -122.1636 }
        source:       { type: string, enum: [curated, cde], description: "curated = individually human-verified; cde = bulk CDE directory import." }
        coord_source: { type: string, enum: [cde_provided, geocoded_census, geocoded_geoapify], description: "Provenance of the coordinate." }
        distance_mi:  { type: number, description: "Miles from the query point (radius mode only)." }
        zone_class:   { type: string, enum: [hills, ridge, flats], description: "Hand-classified fire context (curated schools only)." }
        cds:          { type: string, description: "CDE CDSCode (curated records link to their authoritative CDE row)." }
        zip:          { type: string }
        county:       { type: string }

    SchoolStatusResponse:
      type: object
      properties:
        school:           { $ref: "#/components/schemas/School" }
        in_red_flag_zone:
          type: [boolean, "null"]
          nullable: true
          description: null when the NWS point query failed — unknown, never a false "false".
        data_status:      { $ref: "#/components/schemas/DataStatus" }
        alerts:
          type: array
          items: { $ref: "#/components/schemas/Alert" }
        forecast:
          oneOf:
            - { $ref: "#/components/schemas/Forecast" }
            - type: "null"
        decision_recommendation:
          $ref: "#/components/schemas/DecisionRecommendation"
        sources:
          type: array
          items: { type: string }
        generated_at: { type: string, format: date-time }

    DecisionRecommendation:
      type: object
      properties:
        level:
          type: string
          enum: [normal, modify_outdoor, indoors_only, consider_closure]
          description: >
            Fail-safe: "normal" is never issued while live NWS data is unreachable —
            a degraded response caps the recommendation at modify_outdoor with a
            rationale explaining the data gap (see data_status).
        rationale:
          type: string
          description: Plain-English explanation citing CIF AQI thresholds and fire-weather conditions.

    BuddyTemplateResponse:
      type: object
      properties:
        name:                { type: string }
        reminder_start_iso:  { type: string, format: date-time }
        reminder_end_iso:    { type: string, format: date-time }
        sms_text:            { type: string }
        sms_link:
          type: string
          description: "sms: URL pre-filled with body. Opens in default messaging app."
        email_subject:       { type: string }
        email_body:          { type: string }
        mailto_link:         { type: string, description: "mailto: URL with subject and body pre-filled." }
        ics_content:         { type: string, description: "Raw .ics calendar content (RFC 5545)." }
        ics_filename:        { type: string }
        ics_data_url:        { type: string, description: "data: URL of the .ics content for one-click download." }
        friend_zone_status:
          oneOf:
            - { $ref: "#/components/schemas/FriendZoneStatus" }
            - type: "null"
          description: Present only if friend_lat and friend_lng were provided.
        generated_at:        { type: string, format: date-time }

    FriendZoneStatus:
      type: object
      properties:
        in_red_flag_zone:
          type: [boolean, "null"]
          nullable: true
          description: null when live NWS data was unreachable — unknown, not "not in a zone".
        data_unavailable:
          type: boolean
          description: True when the NWS lookup failed; assume warning conditions and send the check-in anyway.
        note:
          type: string
          description: Present when data_unavailable is true; plain-English caveat.
        active_red_flag_warnings:
          type: array
          items: { $ref: "#/components/schemas/Alert" }
        genasys_evacuation_zone_lookup:
          type: string
          format: uri

    HealthResponse:
      type: object
      properties:
        service: { type: string, example: "redflag-check" }
        version: { type: string, example: "v1" }
        status:  { type: string, enum: [ok, degraded, down] }
        upstreams:
          type: object
          properties:
            nws:             { $ref: "#/components/schemas/UpstreamStatus" }
            census_geocoder: { $ref: "#/components/schemas/UpstreamStatus" }
            genasys_protect: { $ref: "#/components/schemas/UpstreamStatus" }
        timestamp: { type: string, format: date-time }

    UpstreamStatus:
      type: object
      properties:
        ok:     { type: boolean }
        status: { type: integer, description: "HTTP status code, 0 on connection failure." }
        ms:     { type: integer, description: "Round-trip time in milliseconds." }

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:  { type: string, description: "Human-readable error description." }
        status: { type: integer, description: "Upstream status when relevant." }
