openapi: 3.1.0
info:
  title: NEER Flood API
  version: "1.0.0"
  description: >
    Flood-depth data, exposure analytics, the NEER Flood Score, and property flood
    reports for India, served from the India Flood Atlas (modeled ~30 m depth,
    multi-return-period).

    Auth: send `X-API-Key`. Tiers gate what you can call:
      - free       : single point + 100-year only, rate-limited
      - org        : portfolios, AOI exposure, raster download, tiles
      - enterprise : all return periods, batch reports, higher limits, SLAs

    Return periods other than 100-year (`2yr,5yr,10yr,25yr,50yr,500yr,1000yr`) are
    entitlement-gated and return 403 unless the key is provisioned for them.

    Sync vs async: point, admin summary, small portfolios and small raster clips
    answer inline. Large portfolios, large rasters and reports return a Job
    (202) whose result is delivered as a presigned download URL.

    Climate scenarios: every product takes an optional `scenario` axis, orthogonal
    to the return period. `current` (today's climate) is the default and is live now.
    Forthcoming 2100 projections — `2100-ssp245` (SSP2-4.5) and `2100-ssp585`
    (SSP5-8.5) — deepen the same return periods under future climate. Until a
    scenario is published (or if a key isn't entitled to it), requesting it returns
    403 with reason `scenario_not_available`.
  contact: { name: NEER, url: https://www.neer.ai, email: support@neer.ai }
  license: { name: Proprietary }

servers:
  - url: https://flood-api.neer.io
    description: Production

security:
  - ApiKeyAuth: []

tags:
  - { name: Point,     description: Depth + NEER Flood Score at a location }
  - { name: Portfolio, description: Score many assets at once }
  - { name: Exposure,  description: Buildings, roads & rail exposed in an area }
  - name: Raster
    description: >
      Flood-depth GeoTIFF (cloud-optimized) for a bbox or boundary — the raw depth
      values, for analysis in GIS/modeling pipelines. Clips are capped by area per
      tier (`max_area_km2`); a request exceeding the cap returns 413. Whole-state and
      national rasters are NOT served here — they are a licensed, fingerprinted bulk
      deliverable under an enterprise data agreement, not an open endpoint. Return
      periods other than 100-year require a per-return-period entitlement (else 403).
  - name: Tiles
    description: >
      Embeddable flood-depth map layer for web maps (MapLibre/Mapbox/Leaflet/Google
      Maps). Intended for proptech & real-estate portals to overlay flood depth on
      their listings. Because tiles are requested from the end-user's browser, use a
      domain-restricted tile token (query `?token=`) instead of the `X-API-Key`
      header — provision it per allowed origin. The TileJSON descriptor carries the
      required attribution ("India Flood Atlas · NEER"), which must remain visible.
  - { name: Report,    description: NEER Flood Report (per-property PDF) }
  - { name: Jobs,      description: Async job status & results }

paths:

  /point:
    get:
      tags: [Point]
      summary: Depth + NEER Flood Score at a single location
      description: Available on the free tier for `event=100yr` only. Rate-limited.
      parameters:
        - { name: lat, in: query, required: true, schema: { type: number, minimum: 6, maximum: 38 } }
        - { name: lon, in: query, required: true, schema: { type: number, minimum: 67, maximum: 98 } }
        - name: event
          in: query
          required: false
          description: A single return period, or `all` for the full curve (entitlement-gated).
          schema: { $ref: '#/components/schemas/EventOrAll', default: all }
        - { $ref: '#/components/parameters/ScenarioParam' }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/PointRisk' } } } }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /portfolio:
    post:
      tags: [Portfolio]
      summary: Score a list of assets
      description: >
        Sync for small books (<= 10,000 assets); larger books return a Job (202)
        and deliver a downloadable result (JSON/CSV) via presigned URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [assets]
              properties:
                assets:
                  type: array
                  minItems: 1
                  maxItems: 2000000
                  items:
                    type: object
                    required: [lat, lon]
                    properties:
                      id:  { type: string, description: Your asset identifier, echoed back }
                      lat: { type: number }
                      lon: { type: number }
                events:   { $ref: '#/components/schemas/EventList' }
                scenario: { $ref: '#/components/schemas/Scenario' }
                format:   { type: string, enum: [json, csv], default: json }
      responses:
        '200':
          description: Scored inline (small book)
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary: { $ref: '#/components/schemas/PortfolioSummary' }
                  assets:  { type: array, items: { $ref: '#/components/schemas/ScoredAsset' } }
        '202': { $ref: '#/components/responses/JobAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /exposure:
    post:
      tags: [Exposure]
      summary: Buildings, roads & rail exposed in an area of interest
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [aoi]
              properties:
                aoi:    { $ref: '#/components/schemas/AOI' }
                layers:
                  type: array
                  items: { type: string, enum: [buildings, roads, rail] }
                  default: [buildings, roads, rail]
                event:    { $ref: '#/components/schemas/Event', default: 100yr }
                scenario: { $ref: '#/components/schemas/Scenario' }
                format:   { type: string, enum: [json, csv], default: json }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ExposureSummary' } } } }
        '202': { $ref: '#/components/responses/JobAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /exposure/admin/{level}/{code}:
    get:
      tags: [Exposure]
      summary: Pre-computed exposure for a named administrative unit
      description: Instant — served from precomputed aggregates. Free tier allows `event=100yr`.
      parameters:
        - { name: level, in: path, required: true, schema: { type: string, enum: [state, district] } }
        - { name: code,  in: path, required: true, schema: { type: string }, description: "State or district code/slug, e.g. 'bihar' or 'bihar:supaul'" }
        - { name: event, in: query, required: false, schema: { $ref: '#/components/schemas/Event', default: 100yr } }
        - { $ref: '#/components/parameters/ScenarioParam' }
        - { name: format, in: query, required: false, schema: { type: string, enum: [json, csv], default: json } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ExposureSummary' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /raster:
    get:
      tags: [Raster]
      summary: Flood-depth GeoTIFF for a bounding box
      description: >
        Sync when the clip is small (<= configured pixel/size cap); larger clips
        return a Job (202) with a presigned GeoTIFF download URL. The requested
        area must be within the key's `max_area_km2` cap (see Raster tag) — larger
        AOIs return 413 and must be licensed as a bulk deliverable.
      parameters:
        - { name: bbox,  in: query, required: true, description: "west,south,east,north (EPSG:4326)", schema: { type: string, example: "85.0,25.3,85.6,25.9" } }
        - { name: event, in: query, required: false, schema: { $ref: '#/components/schemas/Event', default: 100yr } }
        - { $ref: '#/components/parameters/ScenarioParam' }
        - { name: format, in: query, required: false, schema: { type: string, enum: [geotiff, png], default: geotiff } }
      responses:
        '200': { description: Clip returned inline, content: { image/tiff: { schema: { type: string, format: binary } } } }
        '202': { $ref: '#/components/responses/JobAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '413': { $ref: '#/components/responses/AreaTooLarge' }
    post:
      tags: [Raster]
      summary: Flood-depth GeoTIFF clipped to a GeoJSON boundary
      description: >
        Same area cap as the GET form: the boundary's area must be within the key's
        `max_area_km2`. Whole-state / national extents are a licensed bulk deliverable,
        not served here.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [boundary]
              properties:
                boundary: { $ref: '#/components/schemas/GeoJSONGeometry' }
                event:    { $ref: '#/components/schemas/Event', default: 100yr }
                scenario: { $ref: '#/components/schemas/Scenario' }
                format:   { type: string, enum: [geotiff, png], default: geotiff }
      responses:
        '200': { description: Clip returned inline, content: { image/tiff: { schema: { type: string, format: binary } } } }
        '202': { $ref: '#/components/responses/JobAccepted' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '413': { $ref: '#/components/responses/AreaTooLarge' }

  /tiles/{event}/tilejson.json:
    get:
      tags: [Tiles]
      summary: TileJSON descriptor (drop-in for MapLibre/Mapbox/Leaflet)
      description: >
        Returns a TileJSON 3.0 document a real-estate/proptech map can add in one
        line. Includes the `{z}/{x}/{y}` tile URL template (with the token bound),
        min/max zoom, bounds (India), and the required attribution string.
      security:
        - TileToken: []
      parameters:
        - { name: event, in: path, required: true, schema: { $ref: '#/components/schemas/Event' } }
        - { $ref: '#/components/parameters/ScenarioParam' }
        - { name: colormap, in: query, required: false, schema: { type: string, default: depth_blues } }
      responses:
        '200': { description: TileJSON, content: { application/json: { schema: { $ref: '#/components/schemas/TileJSON' } } } }
        '403': { $ref: '#/components/responses/Forbidden' }

  /tiles/{event}/{z}/{x}/{y}.png:
    get:
      tags: [Tiles]
      summary: XYZ flood-depth map tile
      description: >
        Raster tile for map embedding. Authenticated with a domain-restricted tile
        token (query `?token=`), not the `X-API-Key` header, so it is safe to expose
        in browser map requests.
      security:
        - TileToken: []
      parameters:
        - { name: event, in: path, required: true, schema: { $ref: '#/components/schemas/Event' } }
        - { name: z, in: path, required: true, schema: { type: integer, minimum: 0, maximum: 16 } }
        - { name: x, in: path, required: true, schema: { type: integer } }
        - { name: y, in: path, required: true, schema: { type: integer } }
        - { $ref: '#/components/parameters/ScenarioParam' }
        - { name: colormap, in: query, required: false, schema: { type: string, default: depth_blues } }
      responses:
        '200': { description: PNG tile, content: { image/png: { schema: { type: string, format: binary } } } }
        '204': { description: Empty tile (no data) }
        '403': { description: Token invalid or origin not allow-listed }

  /report/property:
    post:
      tags: [Report]
      summary: Generate a NEER Flood Report for one property (or a batch)
      description: >
        Composes the NEER Flood Score, the multi-return-period depth curve, the
        30-year probability, a modeled flood-extent map, guidance and disclaimers
        into a branded PDF (+ JSON + hosted web link). Single property returns a
        Job that usually completes in seconds; a batch (lender use) is always a Job.
        Location must be a precise coordinate; `address` is geocoded and must be
        confirmed on the returned map.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/ReportRequestSingle'
                - $ref: '#/components/schemas/ReportRequestBatch'
      responses:
        '202': { $ref: '#/components/responses/JobAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /report/{report_id}:
    get:
      tags: [Report]
      summary: Retrieve a previously generated report (verifiable by ID)
      description: Reports are immutable and stored by ID so a lender can re-verify.
      parameters:
        - { name: report_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Report' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /jobs/{job_id}:
    get:
      tags: [Jobs]
      summary: Poll an async job
      parameters:
        - { name: job_id, in: path, required: true, schema: { type: string } }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Job' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

components:

  securitySchemes:
    ApiKeyAuth: { type: apiKey, in: header, name: X-API-Key }
    TileToken:
      type: apiKey
      in: query
      name: token
      description: Domain-restricted tile token, safe to expose in browser map requests. Provisioned per allowed origin.

  parameters:
    ScenarioParam:
      name: scenario
      in: query
      required: false
      description: Climate scenario (default `current`). 2100 pathways are forthcoming — see the `Scenario` schema.
      schema: { $ref: '#/components/schemas/Scenario' }

  schemas:

    Event:
      type: string
      enum: [2yr, 5yr, 10yr, 25yr, 50yr, 100yr, 500yr, 1000yr]
    EventOrAll:
      type: string
      enum: [2yr, 5yr, 10yr, 25yr, 50yr, 100yr, 500yr, 1000yr, all]
    EventList:
      type: array
      items: { $ref: '#/components/schemas/Event' }
      default: [100yr]

    Scenario:
      type: string
      description: >
        Climate scenario, orthogonal to the return period. `current` is today's
        climate (live). `2100-ssp245` (SSP2-4.5) and `2100-ssp585` (SSP5-8.5) are
        2100 projections — forthcoming; requesting one before it is published or
        without entitlement returns 403 `scenario_not_available`.
      enum: [current, 2100-ssp245, 2100-ssp585]
      default: current

    ScenarioList:
      type: array
      description: >
        Scenarios to include in a report, side by side (e.g. current vs 2100 for a
        30-year loan horizon). 2100 pathways are honoured once published.
      items: { $ref: '#/components/schemas/Scenario' }
      default: [current]

    DepthByRP:
      type: object
      description: Modeled depth (m) at each return period; null where not yet published.
      additionalProperties: { type: number, nullable: true }
      example: { "2yr": 0.0, "10yr": 0.6, "100yr": 2.1, "1000yr": 3.6 }

    FloodScore:
      type: object
      properties:
        neer_flood_score: { type: integer, minimum: 0, maximum: 100 }
        category:         { type: string, enum: [None, Minimal, Minor, Moderate, Major, Severe] }
        floods_from_rp:   { type: string, nullable: true, description: Most frequent return period that reaches this location }
        depth_100yr_m:    { type: number, nullable: true }
        prob_30yr_100yr:  { type: number, description: "P(>=1 100-yr flood over 30 years) = 1-(0.99)^30" }
        scenario:         { $ref: '#/components/schemas/Scenario' }
        score_version:    { type: string, example: v1 }
        model_version:    { type: string, example: india-atlas-2026.08 }

    PointRisk:
      allOf:
        - $ref: '#/components/schemas/FloodScore'
        - type: object
          properties:
            lat: { type: number }
            lon: { type: number }
            depth_by_rp_m: { $ref: '#/components/schemas/DepthByRP' }

    ScoredAsset:
      allOf:
        - type: object
          properties:
            id:  { type: string }
            lat: { type: number }
            lon: { type: number }
        - $ref: '#/components/schemas/FloodScore'

    PortfolioSummary:
      type: object
      properties:
        assets_total:     { type: integer }
        assets_exposed:   { type: integer, description: In the modeled 1000-yr floodplain }
        by_category:      { type: object, additionalProperties: { type: integer } }
        mean_score:       { type: number }
        max_score:        { type: integer }

    AOI:
      description: One of a bbox, a GeoJSON geometry, or a named admin unit.
      oneOf:
        - type: object
          required: [bbox]
          properties: { bbox: { type: array, items: { type: number }, minItems: 4, maxItems: 4, description: "[west,south,east,north]" } }
        - type: object
          required: [geojson]
          properties: { geojson: { $ref: '#/components/schemas/GeoJSONGeometry' } }
        - type: object
          required: [admin]
          properties:
            admin:
              type: object
              required: [level, code]
              properties:
                level: { type: string, enum: [state, district] }
                code:  { type: string }

    GeoJSONGeometry:
      type: object
      properties:
        type: { type: string, enum: [Polygon, MultiPolygon] }
        coordinates: { type: array, items: {} }
      required: [type, coordinates]

    ExposureSummary:
      type: object
      properties:
        aoi_name: { type: string, nullable: true }
        event:    { $ref: '#/components/schemas/Event' }
        scenario: { $ref: '#/components/schemas/Scenario' }
        buildings:
          type: object
          properties:
            total:        { type: integer }
            exposed:      { type: integer }
            pct:          { type: number }
            mean_depth_m: { type: number }
            by_depth_band: { $ref: '#/components/schemas/DepthBands' }
        roads:
          type: object
          properties:
            total_km:   { type: number }
            at_risk_km: { type: number }
            pct:        { type: number }
            by_depth_band_km: { $ref: '#/components/schemas/DepthBands' }
        rail:
          type: object
          properties:
            total_km:   { type: number }
            at_risk_km: { type: number }
            pct:        { type: number }
        note:
          type: string
          description: e.g. depth-band methodology; DEM-uncertainty caveats for steep terrain.

    DepthBands:
      type: object
      description: Counts (buildings) or km (transport) by depth band, meters.
      properties:
        "0.15-0.3": { type: number }
        "0.3-0.6":  { type: number }
        "0.6-1.0":  { type: number }
        "1.0-2.0":  { type: number }
        "2.0-3.0":  { type: number }
        ">3.0":     { type: number }

    ReportRequestSingle:
      type: object
      required: [location]
      properties:
        location: { $ref: '#/components/schemas/ReportLocation' }
        scenarios: { $ref: '#/components/schemas/ScenarioList' }
        buyer_reference: { type: string, description: Free-text label shown on the report (e.g. loan/application ref) }
        webhook_url:     { type: string, format: uri, description: Optional callback when the report is ready }
    ReportRequestBatch:
      type: object
      required: [properties]
      properties:
        properties:
          type: array
          minItems: 1
          maxItems: 100000
          items: { $ref: '#/components/schemas/ReportLocation' }
        scenarios: { $ref: '#/components/schemas/ScenarioList' }
        webhook_url: { type: string, format: uri }

    ReportLocation:
      description: Precise coordinate preferred; an address is geocoded and must be confirmed on the returned map.
      oneOf:
        - type: object
          required: [lat, lon]
          properties:
            id:  { type: string }
            lat: { type: number }
            lon: { type: number }
        - type: object
          required: [address]
          properties:
            id:      { type: string }
            address: { type: string }

    Report:
      type: object
      properties:
        report_id:   { type: string }
        status:      { type: string, enum: [ready] }
        generated_at: { type: string, format: date-time }
        location:    { type: object, properties: { lat: { type: number }, lon: { type: number }, address: { type: string, nullable: true }, geocode_confidence: { type: string, nullable: true } } }
        risk:        { $ref: '#/components/schemas/PointRisk' }
        scenarios:   { type: array, items: { $ref: '#/components/schemas/Scenario' }, description: Scenarios included in this report }
        risk_by_scenario:
          type: object
          nullable: true
          description: Per-scenario risk (e.g. current vs 2100) when more than one scenario is requested; keyed by scenario id.
          additionalProperties: { $ref: '#/components/schemas/PointRisk' }
        pdf_url:     { type: string, format: uri, description: Presigned, time-limited download }
        web_url:     { type: string, format: uri, description: Hosted, shareable report page }
        model_version: { type: string }
        score_version: { type: string }
        disclaimer:  { type: string, description: Modeled hazard assessment; not an official government flood certificate. }

    Job:
      type: object
      properties:
        job_id:  { type: string }
        status:  { type: string, enum: [queued, running, done, failed] }
        created_at: { type: string, format: date-time }
        progress: { type: number, minimum: 0, maximum: 1, nullable: true }
        result:
          type: object
          nullable: true
          properties:
            result_url: { type: string, format: uri, description: Presigned download (GeoTIFF/CSV/JSON/PDF) }
            report_id:  { type: string, nullable: true }
            expires_at: { type: string, format: date-time }
        error: { type: string, nullable: true }

    TileJSON:
      type: object
      description: TileJSON 3.0 descriptor for map SDKs.
      properties:
        tilejson:    { type: string, example: "3.0.0" }
        name:        { type: string, example: "India Flood Atlas — 100yr depth" }
        tiles:
          type: array
          items: { type: string }
          example: ["https://flood-api.neer.io/tiles/100yr/{z}/{x}/{y}.png?token=..."]
        minzoom:     { type: integer, example: 4 }
        maxzoom:     { type: integer, example: 16 }
        bounds:      { type: array, items: { type: number }, example: [67.0, 6.0, 98.0, 38.0] }
        attribution: { type: string, example: "India Flood Atlas · NEER" }

    Error:
      type: object
      properties:
        error:   { type: string }
        message: { type: string }
        request_id: { type: string }

    AreaError:
      description: Returned when a raster request exceeds the key's area cap.
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            requested_area_km2: { type: number }
            max_area_km2:       { type: number, description: The cap for this key's tier }
            remedy:             { type: string, example: "Whole-state and national rasters are a licensed bulk deliverable — contact sales." }
      example:
        error: area_too_large
        message: Requested raster area exceeds this key's limit.
        requested_area_km2: 41200
        max_area_km2: 2500
        remedy: "Whole-state and national rasters are a licensed bulk deliverable — contact sales."

  responses:
    JobAccepted:
      description: Accepted for async processing.
      content: { application/json: { schema: { $ref: '#/components/schemas/Job' } } }
      headers:
        Location: { schema: { type: string }, description: URL to poll the job }
    BadRequest:   { description: Invalid request, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    Unauthorized: { description: Missing/invalid API key, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    Forbidden:    { description: "Key not entitled — e.g. tier lacks the product, a non-100yr return period (`return_period_not_entitled`), or an unpublished/ungranted climate scenario (`scenario_not_available`).", content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    NotFound:     { description: Not found, content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
    AreaTooLarge:
      description: Requested raster area exceeds the key's `max_area_km2` cap. State/national extents are a licensed bulk deliverable.
      content: { application/json: { schema: { $ref: '#/components/schemas/AreaError' } } }
    RateLimited:
      description: Rate limit exceeded.
      headers:
        Retry-After:           { schema: { type: integer } }
        X-RateLimit-Limit:     { schema: { type: integer } }
        X-RateLimit-Remaining: { schema: { type: integer } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
