{
  "openapi": "3.1.0",
  "info": {
    "title": "IDG Earth API",
    "version": "1.0.0",
    "summary": "Read-only JSON for the hubs, networks and centers listed on idg.earth.",
    "description": "Read-only JSON for the three organisation directories shown on idg.earth: hubs, networks and centers. Each endpoint returns the same public shape the site itself reads, plus `updated_at` for incremental syncs.\n\n**Access.** Every request needs an API key, sent as `Authorization: Bearer <key>` or `x-api-key`. Ask for one at we@remagine.one, with your name, organisation and what you are building. Keys look like `idg_live_` followed by 48 hex characters.\n\n**Conditional requests.** Every response carries `ETag` and `Last-Modified`. Send `If-None-Match` to receive a bodyless `304 Not Modified` when nothing changed. Use `modified_since` to fetch only rows updated since a given moment. Responses are per key, so `Cache-Control` is `private, max-age=0, must-revalidate`.\n\n**Versioning.** `/v1` is the stable path. Additive changes ship without a bump; breaking changes go under `/v2`.\n\n**Terms.** The data originates from the Inner Development Goals Foundation's public directory of hubs and networks and the IDG Centers programme. IDG Earth (We Remagine One AB) is an independent broker of that data and is not the Foundation. Do not present this data as the Foundation's endorsement of your service. The IDG name, framework and logo belong to the Foundation. Contact details are for reaching hubs and networks, not for bulk mailing; a key that is misused is revoked.",
    "contact": {
      "name": "IDG Earth (We Remagine One AB)",
      "email": "we@remagine.one",
      "url": "https://idg.earth/developers"
    },
    "termsOfService": "https://idg.earth/developers"
  },
  "servers": [
    {
      "url": "https://idg.earth/api/v1"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    },
    {
      "apiKeyHeader": []
    }
  ],
  "tags": [
    {
      "name": "Hubs",
      "description": "IDG hubs: local groups tied to a place."
    },
    {
      "name": "Networks",
      "description": "IDG networks: groups working across borders or around a theme."
    },
    {
      "name": "Centers",
      "description": "IDG Centers: host organisations in the IDG Centers programme."
    }
  ],
  "paths": {
    "/hubs": {
      "get": {
        "tags": [
          "Hubs"
        ],
        "summary": "List hubs",
        "operationId": "listHubs",
        "description": "All active hubs, sorted by name, as the site shows them. Send `If-None-Match` to get a `304` when nothing changed.",
        "parameters": [
          {
            "$ref": "#/components/parameters/modifiedSince"
          },
          {
            "$ref": "#/components/parameters/ifNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The hubs.",
            "headers": {
              "ETag": {
                "description": "Hash of the body. Send it back as `If-None-Match`.",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "Newest `updated_at` among the rows, as an HTTP date.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "private, max-age=0, must-revalidate"
                }
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HubResponse"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "head": {
        "tags": [
          "Hubs"
        ],
        "summary": "Headers for hubs",
        "operationId": "headHubs",
        "description": "The same headers as GET, without a body. A cheap way to poll `ETag` or `Last-Modified`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/modifiedSince"
          },
          {
            "$ref": "#/components/parameters/ifNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Headers only.",
            "headers": {
              "ETag": {
                "description": "Hash of the body. Send it back as `If-None-Match`.",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "Newest `updated_at` among the rows, as an HTTP date.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "private, max-age=0, must-revalidate"
                }
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/networks": {
      "get": {
        "tags": [
          "Networks"
        ],
        "summary": "List networks",
        "operationId": "listNetworks",
        "description": "All active networks, sorted by name, as the site shows them. Send `If-None-Match` to get a `304` when nothing changed.",
        "parameters": [
          {
            "$ref": "#/components/parameters/modifiedSince"
          },
          {
            "$ref": "#/components/parameters/ifNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The networks.",
            "headers": {
              "ETag": {
                "description": "Hash of the body. Send it back as `If-None-Match`.",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "Newest `updated_at` among the rows, as an HTTP date.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "private, max-age=0, must-revalidate"
                }
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NetworkResponse"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "head": {
        "tags": [
          "Networks"
        ],
        "summary": "Headers for networks",
        "operationId": "headNetworks",
        "description": "The same headers as GET, without a body. A cheap way to poll `ETag` or `Last-Modified`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/modifiedSince"
          },
          {
            "$ref": "#/components/parameters/ifNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Headers only.",
            "headers": {
              "ETag": {
                "description": "Hash of the body. Send it back as `If-None-Match`.",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "Newest `updated_at` among the rows, as an HTTP date.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "private, max-age=0, must-revalidate"
                }
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/centers": {
      "get": {
        "tags": [
          "Centers"
        ],
        "summary": "List centers",
        "operationId": "listCenters",
        "description": "All active centers, sorted by name, as the site shows them. Send `If-None-Match` to get a `304` when nothing changed.",
        "parameters": [
          {
            "$ref": "#/components/parameters/modifiedSince"
          },
          {
            "$ref": "#/components/parameters/ifNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "The centers.",
            "headers": {
              "ETag": {
                "description": "Hash of the body. Send it back as `If-None-Match`.",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "Newest `updated_at` among the rows, as an HTTP date.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "private, max-age=0, must-revalidate"
                }
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CenterResponse"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "head": {
        "tags": [
          "Centers"
        ],
        "summary": "Headers for centers",
        "operationId": "headCenters",
        "description": "The same headers as GET, without a body. A cheap way to poll `ETag` or `Last-Modified`.",
        "parameters": [
          {
            "$ref": "#/components/parameters/modifiedSince"
          },
          {
            "$ref": "#/components/parameters/ifNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Headers only.",
            "headers": {
              "ETag": {
                "description": "Hash of the body. Send it back as `If-None-Match`.",
                "schema": {
                  "type": "string"
                }
              },
              "Last-Modified": {
                "description": "Newest `updated_at` among the rows, as an HTTP date.",
                "schema": {
                  "type": "string"
                }
              },
              "Cache-Control": {
                "schema": {
                  "type": "string",
                  "const": "private, max-age=0, must-revalidate"
                }
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string",
                  "const": "*"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "`Authorization: Bearer idg_live_…`. Preferred."
      },
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "The same key in an `x-api-key` header, for clients that cannot set `Authorization`."
      }
    },
    "parameters": {
      "modifiedSince": {
        "name": "modified_since",
        "in": "query",
        "required": false,
        "description": "Only rows with `updated_at` at or after this ISO-8601 date or date-time. An unparseable value is ignored.",
        "schema": {
          "type": "string",
          "format": "date-time"
        },
        "example": "2026-09-01"
      },
      "ifNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "The `ETag` of a previous response. Weak comparison; `W/` prefixes and lists are fine.",
        "schema": {
          "type": "string"
        }
      }
    },
    "responses": {
      "NotModified": {
        "description": "Nothing changed since the `ETag` you sent. No body.",
        "headers": {
          "ETag": {
            "description": "Hash of the body. Send it back as `If-None-Match`.",
            "schema": {
              "type": "string"
            }
          },
          "Last-Modified": {
            "description": "Newest `updated_at` among the rows, as an HTTP date.",
            "schema": {
              "type": "string"
            }
          },
          "Cache-Control": {
            "schema": {
              "type": "string",
              "const": "private, max-age=0, must-revalidate"
            }
          },
          "Access-Control-Allow-Origin": {
            "schema": {
              "type": "string",
              "const": "*"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, unknown or revoked API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "The data could not be loaded.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "Source": {
        "type": "object",
        "description": "Where the data comes from. IDG Earth transcribes the Foundation's directory.",
        "required": [
          "name",
          "url",
          "retrieved"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "retrieved": {
            "type": "string",
            "format": "date",
            "description": "When IDG Earth last refreshed the directory from the source."
          }
        }
      },
      "Provider": {
        "type": "object",
        "description": "Who serves the data. `role` is `broker` today; it becomes `source` if IDG Earth ever becomes the directory of record.",
        "required": [
          "name",
          "url",
          "operator",
          "role",
          "affiliation"
        ],
        "properties": {
          "name": {
            "type": "string",
            "const": "IDG Earth"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "operator": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "broker",
              "source"
            ]
          },
          "affiliation": {
            "type": "string"
          }
        }
      },
      "Licence": {
        "type": "object",
        "description": "The Foundation's licence, restated on every response.",
        "required": [
          "holder",
          "document",
          "url",
          "terms"
        ],
        "properties": {
          "holder": {
            "type": "string"
          },
          "document": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "terms": {
            "type": "string"
          }
        }
      },
      "Contact": {
        "type": "object",
        "description": "A contact person, as listed on the Foundation's directory.",
        "required": [
          "person_id",
          "first_name",
          "last_name",
          "email",
          "role"
        ],
        "properties": {
          "person_id": {
            "type": "string",
            "format": "uuid"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "The published contact email. For reaching the hub or network, not for bulk mailing."
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "description": "e.g. Lead coordinator, Coordinator."
          }
        }
      },
      "CenterContact": {
        "type": "object",
        "description": "A contact person. Centers carry no email in the public data.",
        "required": [
          "person_id",
          "first_name",
          "last_name",
          "role"
        ],
        "properties": {
          "person_id": {
            "type": "string",
            "format": "uuid"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Hub": {
        "type": "object",
        "required": [
          "id",
          "name",
          "location",
          "latitude",
          "longitude",
          "description",
          "website",
          "founded_at",
          "theme",
          "geography",
          "meeting_frequency",
          "contacts",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the record last changed on IDG Earth. Drives `Last-Modified` and `modified_since`."
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "founded_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "theme": {
            "type": [
              "string",
              "null"
            ],
            "description": "The hub's or network's theme, if it has one."
          },
          "geography": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "local",
              "regional",
              "national",
              "international",
              null
            ]
          },
          "meeting_frequency": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free text, e.g. Monthly."
          },
          "contacts": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/Contact"
            }
          }
        }
      },
      "Network": {
        "type": "object",
        "required": [
          "id",
          "name",
          "scope",
          "description",
          "website",
          "founded_at",
          "theme",
          "geography",
          "meeting_frequency",
          "contacts",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the record last changed on IDG Earth. Drives `Last-Modified` and `modified_since`."
          },
          "scope": {
            "type": [
              "string",
              "null"
            ],
            "description": "Geographic or thematic scope, e.g. Nordic, Global, Education."
          },
          "founded_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "theme": {
            "type": [
              "string",
              "null"
            ],
            "description": "The hub's or network's theme, if it has one."
          },
          "geography": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "local",
              "regional",
              "national",
              "international",
              null
            ]
          },
          "meeting_frequency": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free text, e.g. Monthly."
          },
          "contacts": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/Contact"
            }
          }
        }
      },
      "Center": {
        "type": "object",
        "required": [
          "id",
          "name",
          "organisation_type",
          "location",
          "latitude",
          "longitude",
          "description",
          "website",
          "launched_at",
          "contacts",
          "updated_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "website": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "description": "When the record last changed on IDG Earth. Drives `Last-Modified` and `modified_since`."
          },
          "organisation_type": {
            "type": [
              "string",
              "null"
            ],
            "description": "Kind of host organisation, free text."
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "latitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "longitude": {
            "type": [
              "number",
              "null"
            ]
          },
          "launched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "contacts": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/CenterContact"
            }
          }
        }
      },
      "HubResponse": {
        "type": "object",
        "required": [
          "count",
          "last_modified",
          "source",
          "provider",
          "licence",
          "hubs"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Number of rows in this response."
          },
          "last_modified": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Newest `updated_at` among the rows, or null when there are none."
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "provider": {
            "$ref": "#/components/schemas/Provider"
          },
          "licence": {
            "$ref": "#/components/schemas/Licence"
          },
          "hubs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Hub"
            }
          }
        }
      },
      "NetworkResponse": {
        "type": "object",
        "required": [
          "count",
          "last_modified",
          "source",
          "provider",
          "licence",
          "networks"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Number of rows in this response."
          },
          "last_modified": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Newest `updated_at` among the rows, or null when there are none."
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "provider": {
            "$ref": "#/components/schemas/Provider"
          },
          "licence": {
            "$ref": "#/components/schemas/Licence"
          },
          "networks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Network"
            }
          }
        }
      },
      "CenterResponse": {
        "type": "object",
        "required": [
          "count",
          "last_modified",
          "source",
          "provider",
          "licence",
          "centers"
        ],
        "properties": {
          "count": {
            "type": "integer",
            "description": "Number of rows in this response."
          },
          "last_modified": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time",
            "description": "Newest `updated_at` among the rows, or null when there are none."
          },
          "source": {
            "$ref": "#/components/schemas/Source"
          },
          "provider": {
            "$ref": "#/components/schemas/Provider"
          },
          "licence": {
            "$ref": "#/components/schemas/Licence"
          },
          "centers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Center"
            }
          }
        }
      }
    }
  }
}
