{
  "openapi": "3.1.0",
  "info": {
    "title": "gridcarbon",
    "version": "1.0.0-pre",
    "summary": "Hourly grid carbon intensity for 45 zones, no key, no signup.",
    "description": "Hourly electricity grid carbon intensity in gCO2eq/kWh for 33 European bidding zones (ENTSO-E), 11 US balancing authorities (EIA-930) and Great Britain (NESO).\n\nNo authentication. CORS is open (`Access-Control-Allow-Origin: *`). Values are derived from published generation by fuel type using IPCC AR5 lifecycle factors; GB republishes NESO's operational (combustion-only) figure and is not comparable in level with the other 44 zones. Values are production-based (what was generated inside the zone), generator-terminal, and are recomputed in place over a 72-hour trailing window as operators revise. The full method, factor table and known biases: https://gridcarbon.dev/methodology\n\nRate limit: 60 requests per minute per IP, loosely enforced (Cloudflare's limiter is permissive and eventually consistent). Honour the `Cache-Control` max-age on each endpoint and you will never see a 429.\n\nHistory is uneven by region: GB from 2017-09, the 11 US zones from 2019-01, the 33 European zones from 2024-08.",
    "termsOfService": "https://gridcarbon.dev/#privacy",
    "contact": { "name": "gridcarbon", "url": "https://gridcarbon.dev" },
    "license": { "name": "CC BY 4.0 (data)", "url": "https://creativecommons.org/licenses/by/4.0/" }
  },
  "externalDocs": { "description": "Methodology, factor table and known biases", "url": "https://gridcarbon.dev/methodology" },
  "servers": [ { "url": "https://api.gridcarbon.dev" } ],
  "tags": [
    { "name": "intensity", "description": "Carbon intensity readings" },
    { "name": "meta", "description": "Zones, liveness and ingestion freshness" }
  ],
  "paths": {
    "/v1": {
      "get": {
        "tags": ["meta"], "summary": "API index", "operationId": "index",
        "responses": { "200": { "description": "Endpoint list and attribution", "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } } }
      }
    },
    "/v1/health": {
      "get": {
        "tags": ["meta"], "summary": "Liveness", "operationId": "health",
        "description": "Always 200 if the worker is up. Says nothing about whether data is fresh; use /v1/status for that.",
        "responses": { "200": { "description": "Alive", "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "ts": { "type": "string", "format": "date-time" } }, "required": ["ok", "ts"] } } } } }
      }
    },
    "/v1/status": {
      "get": {
        "tags": ["meta"], "summary": "Ingestion freshness per source", "operationId": "status",
        "description": "Returns HTTP 200 when every source is within its expected publication lag and HTTP 503 when at least one is behind. Values are still served either way; treat the affected source's zones as stale. Thresholds are per source (ENTSO-E 8h, NESO 4h, EIA 36h) and reflect the upstreams' own publishing cadence. `Cache-Control: max-age=120`.",
        "responses": {
          "200": { "description": "All sources current", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Status" } } } },
          "503": { "description": "At least one source behind its window", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Status" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/zones": {
      "get": {
        "tags": ["meta"], "summary": "List zones", "operationId": "zones",
        "description": "The 45 zone codes accepted by the intensity endpoints. `Cache-Control: max-age=3600`.",
        "responses": {
          "200": { "description": "Zones", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Zone" } } }, "required": ["data"] },
            "example": { "data": [ { "zone": "DE", "name": "Germany / Luxembourg", "source": "entsoe", "resolution_min": 60 }, { "zone": "GB", "name": "Great Britain", "source": "uk-neso", "resolution_min": 30 }, { "zone": "US-NYISO", "name": "New York ISO", "source": "eia", "resolution_min": 60 } ] } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/intensity/latest": {
      "get": {
        "tags": ["intensity"], "summary": "Newest published reading, one zone or all 45", "operationId": "latest",
        "description": "\"Latest\" means the newest interval the upstream has published, not now: European zones typically run 2-4 hours behind, US zones 11-30 hours, GB about an hour. Every reading carries the start of the interval it describes in `ts`; use it. For GB the newest half-hour is NESO's forecast (`method: upstream:uk-neso:forecast`), which can run well above the settled value. Without `zone` the response contains all 45 zones in one call. `Cache-Control: max-age=300`.",
        "parameters": [ { "name": "zone", "in": "query", "required": false, "description": "Zone code from /v1/zones, case-insensitive (FR, DE, US-NYISO, GB, ...). Omit for all zones.", "schema": { "type": "string" }, "example": "FR" } ],
        "responses": {
          "200": { "description": "Readings", "content": { "application/json": { "schema": { "type": "object", "properties": { "unit": { "type": "string", "const": "gCO2eq/kWh" }, "data": { "type": "array", "items": { "$ref": "#/components/schemas/Reading" } } }, "required": ["unit", "data"] },
            "example": { "unit": "gCO2eq/kWh", "data": [ { "zone": "FR", "ts": "2026-09-03T05:00:00Z", "gco2eq_kwh": 32.2, "method": "computed:v1" } ] } } } },
          "404": { "description": "Unknown zone", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "unknown or empty zone: XX" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/intensity": {
      "get": {
        "tags": ["intensity"], "summary": "Hourly history for one zone over a UTC window", "operationId": "history",
        "description": "Returns readings with `from <= ts < to`, ascending, capped at 5000 points per response (`truncated: true` plus a `note` when the cap is hit; narrow the window to get the rest). Defaults to the last 24 hours. GB is half-hourly, so one month of GB is about 1,488 points. `Cache-Control: max-age=600`.",
        "parameters": [
          { "name": "zone", "in": "query", "required": true, "description": "Zone code from /v1/zones, case-insensitive.", "schema": { "type": "string" }, "example": "US-NYISO" },
          { "name": "from", "in": "query", "required": false, "description": "Inclusive UTC start, ISO 8601. Default: 24 hours ago.", "schema": { "type": "string", "format": "date-time" }, "example": "2019-01-01T00:00:00Z" },
          { "name": "to", "in": "query", "required": false, "description": "Exclusive UTC end, ISO 8601. Default: now.", "schema": { "type": "string", "format": "date-time" }, "example": "2019-02-01T00:00:00Z" }
        ],
        "responses": {
          "200": { "description": "Window of readings", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/History" },
            "example": { "zone": "US-NYISO", "from": "2019-01-01T00:00:00Z", "to": "2019-01-01T02:00:00Z", "unit": "gCO2eq/kWh", "count": 2, "truncated": false, "data": [ { "ts": "2019-01-01T00:00:00Z", "gco2eq_kwh": 231.4, "method": "computed:v1" }, { "ts": "2019-01-01T01:00:00Z", "gco2eq_kwh": 229.9, "method": "computed:v1" } ] } } } },
          "400": { "description": "Missing zone", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "missing required param: zone" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Reading": {
        "type": "object",
        "properties": {
          "zone": { "type": "string", "description": "Zone code" },
          "ts": { "type": "string", "format": "date-time", "description": "UTC start of the interval the value describes" },
          "gco2eq_kwh": { "type": "number", "description": "Average carbon intensity of generation over the interval, gCO2eq/kWh" },
          "method": { "type": "string", "description": "`computed:v1` = derived from published generation by fuel with IPCC AR5 lifecycle factors; `upstream:uk-neso:actual` / `upstream:uk-neso:forecast` = NESO's own operational (combustion-only) figure, republished. GB is not comparable in level with the other zones.", "examples": ["computed:v1", "upstream:uk-neso:actual", "upstream:uk-neso:forecast"] }
        },
        "required": ["ts", "gco2eq_kwh", "method"]
      },
      "History": {
        "type": "object",
        "properties": {
          "zone": { "type": "string" }, "from": { "type": "string", "format": "date-time" }, "to": { "type": "string", "format": "date-time" },
          "unit": { "type": "string", "const": "gCO2eq/kWh" }, "count": { "type": "integer" },
          "truncated": { "type": "boolean", "description": "true when the 5000-point cap was hit" },
          "note": { "type": "string", "description": "Present only when truncated" },
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/Reading" } }
        },
        "required": ["zone", "from", "to", "unit", "count", "truncated", "data"]
      },
      "Zone": {
        "type": "object",
        "properties": { "zone": { "type": "string" }, "name": { "type": "string" }, "source": { "type": "string", "enum": ["entsoe", "eia", "uk-neso"] }, "resolution_min": { "type": "integer", "description": "60, or 30 for GB" } },
        "required": ["zone", "name", "source", "resolution_min"]
      },
      "Status": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" }, "ts": { "type": "string", "format": "date-time" }, "note": { "type": "string" },
          "sources": { "type": "array", "items": { "type": "object", "properties": {
            "source": { "type": "string", "enum": ["entsoe", "eia", "uk-neso"] }, "zones": { "type": "integer" },
            "freshest_lag_hours": { "type": "number" }, "stalest_lag_hours": { "type": "number" }, "stale_after_hours": { "type": "number" }, "ok": { "type": "boolean" } },
            "required": ["source", "zones", "freshest_lag_hours", "stalest_lag_hours", "stale_after_hours", "ok"] } }
        },
        "required": ["ok", "ts", "note", "sources"]
      },
      "Error": { "type": "object", "properties": { "error": { "type": "string" }, "hint": { "type": "string" } }, "required": ["error"] }
    },
    "responses": {
      "RateLimited": {
        "description": "Rate limited. Documented at 60/min per IP; enforced loosely.",
        "headers": { "retry-after": { "schema": { "type": "string" }, "description": "Seconds to wait" } },
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  }
}
