{
  "openapi": "3.0.3",
  "info": {
    "title": "Waltsoft SitePulse Sentinel-2 Satellite Change Detection API",
    "description": "Satellite-based land change detection using ESA Sentinel-2 L2A imagery. Returns NDVI/NDBI-derived change scores and per-scene evidence for any coordinate, enabling AI agents to detect construction activity, deforestation, urban expansion and agricultural shifts without GIS expertise. Billed at $1.50 per /v1/site call and $6.00 per /v1/timeline call.\n",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://sitepulse.waltsoft.net/v1",
      "description": "SitePulse production endpoint"
    }
  ],
  "paths": {
    "/site": {
      "get": {
        "summary": "12-month site change detection",
        "description": "Returns a change verdict, NDVI/NDBI deltas, confidence level and the per-scene evidence for a coordinate over the past 12 months. change_direction is \"built-up\" when NDVI falls AND NDBI rises, \"revegetated\" when NDVI rises AND NDBI falls, \"none\" when neither threshold is crossed, or \"inconclusive\" when too few cloud-free scenes were found.\n",
        "operationId": "detectChange",
        "tags": [
          "Change Detection"
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float",
              "minimum": -90,
              "maximum": 90,
              "example": 30.2216
            },
            "description": "Latitude of the target location in decimal degrees"
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float",
              "minimum": -180,
              "maximum": 180,
              "example": -97.6206
            },
            "description": "Longitude of the target location in decimal degrees"
          }
        ],
        "responses": {
          "200": {
            "description": "Change detection result for the requested coordinate",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteResponse"
                },
                "example": {
                  "site": {
                    "lon": -97.6206,
                    "lat": 30.2216
                  },
                  "window_months": 12,
                  "change_detected": true,
                  "change_direction": "built-up",
                  "confidence": "high",
                  "ndvi_delta": -0.19,
                  "ndbi_delta": 0.14,
                  "baseline": {
                    "ndvi": 0.31,
                    "ndbi": -0.08,
                    "from": "2025-08-14",
                    "to": "2025-10-02"
                  },
                  "recent": {
                    "ndvi": 0.12,
                    "ndbi": 0.06,
                    "from": "2026-06-11",
                    "to": "2026-08-08"
                  },
                  "scenes_used": 9,
                  "scenes_rejected_for_cloud": 3,
                  "scenes_matched": 14,
                  "evidence": [],
                  "limits": [],
                  "source": "Sentinel-2 L2A (ESA/Copernicus) via AWS Open Data \u2014 s3://sentinel-cogs"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/UnprocessableEntity"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    },
    "/timeline": {
      "get": {
        "summary": "36-month NDVI/NDBI time series",
        "description": "Returns a monthly change score, NDVI and NDBI series for a coordinate spanning the past 36 months. Useful for trend analysis, anomaly detection and seasonal pattern recognition. Each entry uses the lowest-cloud-cover Sentinel-2 scene available for that calendar month.\n",
        "operationId": "getTimeline",
        "tags": [
          "Change Detection"
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float",
              "minimum": -90,
              "maximum": 90,
              "example": 30.2216
            },
            "description": "Latitude of the target location in decimal degrees"
          },
          {
            "name": "lon",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "format": "float",
              "minimum": -180,
              "maximum": 180,
              "example": -97.6206
            },
            "description": "Longitude of the target location in decimal degrees"
          }
        ],
        "responses": {
          "200": {
            "description": "36-month change and index time series",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TimelineResponse"
                },
                "example": {
                  "site": {
                    "lon": -97.6206,
                    "lat": 30.2216
                  },
                  "window_months": 36,
                  "change_detected": false,
                  "change_direction": "none",
                  "confidence": "medium",
                  "ndvi_delta": -0.04,
                  "ndbi_delta": 0.02,
                  "baseline": {
                    "ndvi": 0.34,
                    "ndbi": -0.09,
                    "from": "2023-09-01",
                    "to": "2024-01-01"
                  },
                  "recent": {
                    "ndvi": 0.3,
                    "ndbi": -0.07,
                    "from": "2026-04-01",
                    "to": "2026-08-08"
                  },
                  "scenes_used": 24,
                  "scenes_rejected_for_cloud": 8,
                  "scenes_matched": 36,
                  "evidence": [],
                  "limits": [],
                  "source": "Sentinel-2 L2A (ESA/Copernicus) via AWS Open Data \u2014 s3://sentinel-cogs"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimitExceeded"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "IndexWindow": {
        "type": "object",
        "required": [
          "ndvi",
          "ndbi",
          "from",
          "to"
        ],
        "properties": {
          "ndvi": {
            "type": "number",
            "format": "float",
            "description": "Mean NDVI over this window",
            "example": 0.31
          },
          "ndbi": {
            "type": "number",
            "format": "float",
            "description": "Mean NDBI over this window",
            "example": -0.08
          },
          "from": {
            "type": "string",
            "description": "Start date of this window (ISO 8601)",
            "example": "2025-08-14"
          },
          "to": {
            "type": "string",
            "description": "End date of this window (ISO 8601)",
            "example": "2025-10-02"
          }
        }
      },
      "SiteResponse": {
        "type": "object",
        "required": [
          "site",
          "window_months",
          "change_detected",
          "change_direction",
          "confidence",
          "ndvi_delta",
          "ndbi_delta",
          "baseline",
          "recent",
          "scenes_used",
          "scenes_rejected_for_cloud",
          "scenes_matched",
          "evidence",
          "limits",
          "source"
        ],
        "properties": {
          "site": {
            "type": "object",
            "description": "Echo of the requested coordinate",
            "required": [
              "lon",
              "lat"
            ],
            "properties": {
              "lon": {
                "type": "number",
                "format": "float",
                "example": -97.6206
              },
              "lat": {
                "type": "number",
                "format": "float",
                "example": 30.2216
              }
            }
          },
          "window_months": {
            "type": "integer",
            "description": "Number of months analysed",
            "example": 12
          },
          "change_detected": {
            "type": "boolean",
            "description": "True when a statistically significant change was found",
            "example": true
          },
          "change_direction": {
            "type": "string",
            "enum": [
              "built-up",
              "revegetated",
              "none",
              "inconclusive"
            ],
            "description": "Direction of the detected change",
            "example": "built-up"
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ],
            "description": "Confidence in the verdict based on scene count and index spread",
            "example": "high"
          },
          "ndvi_delta": {
            "type": "number",
            "format": "float",
            "description": "NDVI change (recent minus baseline); negative = vegetation loss",
            "example": -0.19
          },
          "ndbi_delta": {
            "type": "number",
            "format": "float",
            "description": "NDBI change (recent minus baseline); positive = built-up increase",
            "example": 0.14
          },
          "baseline": {
            "$ref": "#/components/schemas/IndexWindow"
          },
          "recent": {
            "$ref": "#/components/schemas/IndexWindow"
          },
          "scenes_used": {
            "type": "integer",
            "description": "Number of Sentinel-2 scenes included in the analysis",
            "example": 9
          },
          "scenes_rejected_for_cloud": {
            "type": "integer",
            "description": "Scenes discarded for exceeding the cloud-cover threshold",
            "example": 3
          },
          "scenes_matched": {
            "type": "integer",
            "description": "Total scenes found in STAC before any filter",
            "example": 14
          },
          "evidence": {
            "type": "array",
            "description": "Per-scene index readings used in the analysis",
            "items": {
              "type": "object"
            }
          },
          "limits": {
            "type": "array",
            "description": "Informational notes about coverage gaps or edge cases",
            "items": {
              "type": "string"
            }
          },
          "source": {
            "type": "string",
            "description": "Data provenance statement",
            "example": "Sentinel-2 L2A (ESA/Copernicus) via AWS Open Data \u2014 s3://sentinel-cogs"
          }
        }
      },
      "TimelineResponse": {
        "type": "object",
        "required": [
          "site",
          "window_months",
          "change_detected",
          "change_direction",
          "confidence",
          "ndvi_delta",
          "ndbi_delta",
          "baseline",
          "recent",
          "scenes_used",
          "scenes_rejected_for_cloud",
          "scenes_matched",
          "evidence",
          "limits",
          "source"
        ],
        "properties": {
          "site": {
            "type": "object",
            "description": "Echo of the requested coordinate",
            "required": [
              "lon",
              "lat"
            ],
            "properties": {
              "lon": {
                "type": "number",
                "format": "float",
                "example": -97.6206
              },
              "lat": {
                "type": "number",
                "format": "float",
                "example": 30.2216
              }
            }
          },
          "window_months": {
            "type": "integer",
            "example": 36
          },
          "change_detected": {
            "type": "boolean",
            "example": false
          },
          "change_direction": {
            "type": "string",
            "enum": [
              "built-up",
              "revegetated",
              "none",
              "inconclusive"
            ],
            "example": "none"
          },
          "confidence": {
            "type": "string",
            "enum": [
              "high",
              "medium",
              "low"
            ],
            "example": "medium"
          },
          "ndvi_delta": {
            "type": "number",
            "format": "float",
            "example": -0.04
          },
          "ndbi_delta": {
            "type": "number",
            "format": "float",
            "example": 0.02
          },
          "baseline": {
            "$ref": "#/components/schemas/IndexWindow"
          },
          "recent": {
            "$ref": "#/components/schemas/IndexWindow"
          },
          "scenes_used": {
            "type": "integer",
            "example": 24
          },
          "scenes_rejected_for_cloud": {
            "type": "integer",
            "example": 8
          },
          "scenes_matched": {
            "type": "integer",
            "example": 36
          },
          "evidence": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "limits": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "source": {
            "type": "string",
            "example": "Sentinel-2 L2A (ESA/Copernicus) via AWS Open Data \u2014 s3://sentinel-cogs"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "integer"
                  },
                  "message": {
                    "type": "string"
                  }
                }
              }
            ]
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Missing or invalid query parameters",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "invalid_coordinates"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Invalid or inactive API key",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": {
                "code": 401,
                "message": "Invalid or inactive API key"
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "No API key sent (x402 pay-per-call challenge in response body)",
        "content": {
          "application/json": {
            "schema": {
              "type": "object"
            }
          }
        }
      },
      "NotFound": {
        "description": "No cloud-free Sentinel-2 scenes found for this location in the window",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "no_scenes_available"
            }
          }
        }
      },
      "UnprocessableEntity": {
        "description": "Coordinates fall outside Sentinel-2 coverage (above 84 degrees latitude)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "outside_coverage"
            }
          }
        }
      },
      "RateLimitExceeded": {
        "description": "Rate limit exceeded",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "rate_limit_exceeded"
            }
          }
        }
      },
      "InternalServerError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "internal_error"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "Upstream (STAC API or S3 COG reads) temporarily unavailable; call not billed",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            },
            "example": {
              "error": "upstream_analysis_unavailable"
            }
          }
        }
      }
    },
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "AWS Marketplace API key (mp_...) delivered via AWS Secrets Manager on subscription"
      }
    }
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ]
}