{
  "openapi": "3.1.0",
  "info": {
    "title": "Bluewatch API",
    "version": "1.0.0",
    "summary": "Live UK firefighter recruitment as JSON.",
    "description": "Free, read-only JSON API of live UK firefighter recruitment opportunities,\nemploying services, and station-level metadata. Cached at the CDN edge so it\nis fast and stable without any auth or rate limiting.\n\nUse this API to build dashboards, custom widgets, mobile apps, Slack bots\nor anything else that benefits from a real-time view of UK firefighter\nrecruitment. Attribution back to bluewatch.app is appreciated but not\nrequired.",
    "contact": {
      "name": "Bluewatch",
      "email": "hello@bluewatch.app",
      "url": "https://bluewatch.app/developers"
    },
    "license": {
      "name": "Free to use with attribution",
      "url": "https://bluewatch.app/developers"
    }
  },
  "servers": [
    {
      "url": "https://bluewatch.app/api/v1",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Human-friendly developer guide",
    "url": "https://bluewatch.app/developers"
  },
  "tags": [
    {
      "name": "Opportunities",
      "description": "Live recruitment opportunities. Each opportunity is a single role at a service (and optionally a specific station)."
    },
    {
      "name": "Services",
      "description": "Employing fire and rescue services, airport ARFF units, defence fire teams, industrial fire teams and overseas employers."
    },
    {
      "name": "Meta",
      "description": "Counts and freshness signals. Cheap to poll for change detection."
    }
  ],
  "paths": {
    "/opportunities": {
      "get": {
        "tags": [
          "Opportunities"
        ],
        "summary": "List live opportunities",
        "description": "Returns live recruitment opportunities, most-recently-first-seen at the top. Paginated; use the `links.next` URL to walk the full set.",
        "operationId": "listOpportunities",
        "parameters": [
          {
            "$ref": "#/components/parameters/ServiceType"
          },
          {
            "name": "service",
            "in": "query",
            "description": "Filter by service slug (e.g. `london-fire-brigade`). See `/services` for the full set.",
            "required": false,
            "schema": {
              "type": "string",
              "example": "london-fire-brigade"
            }
          },
          {
            "name": "role",
            "in": "query",
            "description": "Filter by role type.",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/RoleType"
            }
          },
          {
            "name": "region",
            "in": "query",
            "description": "Filter by UK region slug. Wales rolls up the three Welsh services.",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/RegionSlug"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by service category.",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ServiceCategory"
            }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Only return opportunities first seen at or after this ISO 8601 timestamp.",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "example": "2026-05-01T00:00:00Z"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of opportunities.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpportunityListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/opportunities/{slug}": {
      "get": {
        "tags": [
          "Opportunities"
        ],
        "summary": "Get a single opportunity",
        "description": "Look up an opportunity by slug. Slugs are stable once issued.",
        "operationId": "getOpportunity",
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "responses": {
          "200": {
            "description": "The opportunity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OpportunityDetailResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/services": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "List services",
        "description": "Returns employing services (fire and rescue services, airport ARFF units, defence fire teams, industrial fire teams, overseas employers). Sorted alphabetically by name.",
        "operationId": "listServices",
        "parameters": [
          {
            "$ref": "#/components/parameters/ServiceType"
          },
          {
            "name": "region",
            "in": "query",
            "description": "Filter by UK region slug.",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/RegionSlug"
            }
          },
          {
            "name": "category",
            "in": "query",
            "description": "Filter by service category.",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/ServiceCategory"
            }
          },
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Offset"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of services.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceListResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          }
        }
      }
    },
    "/services/{slug}": {
      "get": {
        "tags": [
          "Services"
        ],
        "summary": "Get a single service",
        "description": "Look up a service by slug. Use the slug from `/jobs/fire/{slug}` URLs.",
        "operationId": "getService",
        "parameters": [
          {
            "$ref": "#/components/parameters/Slug"
          }
        ],
        "responses": {
          "200": {
            "description": "The service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ServiceDetailResponse"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/meta": {
      "get": {
        "tags": [
          "Meta"
        ],
        "summary": "API metadata",
        "description": "Returns counts by service_type plus the latest opportunity timestamp. Cheap to poll for change detection — compare `latest_opportunity_at` before re-fetching big lists.",
        "operationId": "getMeta",
        "responses": {
          "200": {
            "description": "Metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetaResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "ServiceType": {
        "name": "service_type",
        "in": "query",
        "description": "Vertical to query. Defaults to `fire`. Only `fire` is currently active.",
        "required": false,
        "schema": {
          "$ref": "#/components/schemas/ServiceType"
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Page size. Default 50, max 200.",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "Offset": {
        "name": "offset",
        "in": "query",
        "description": "Page offset (zero-based).",
        "required": false,
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "Slug": {
        "name": "slug",
        "in": "path",
        "description": "Stable URL-safe identifier.",
        "required": true,
        "schema": {
          "type": "string"
        },
        "example": "london-fire-brigade"
      }
    },
    "schemas": {
      "ServiceType": {
        "type": "string",
        "enum": [
          "fire",
          "police",
          "medic"
        ],
        "description": "Emergency service vertical."
      },
      "ServiceCategory": {
        "type": "string",
        "enum": [
          "local_authority_frs",
          "airport_arff",
          "defence_fire",
          "industrial",
          "private_contractor",
          "overseas_transfer",
          "other"
        ],
        "description": "Category of employer."
      },
      "RoleType": {
        "type": "string",
        "enum": [
          "wholetime",
          "on_call",
          "transferee",
          "control",
          "specialist",
          "officer",
          "cadet",
          "volunteer",
          "operational_hq"
        ],
        "description": "Type of role."
      },
      "OpportunityStatus": {
        "type": "string",
        "enum": [
          "live",
          "closed",
          "draft"
        ],
        "description": "Lifecycle of an opportunity."
      },
      "RegionSlug": {
        "type": "string",
        "enum": [
          "north-east",
          "north-west",
          "yorkshire-and-humber",
          "east-midlands",
          "west-midlands",
          "east-of-england",
          "london",
          "south-east",
          "south-west",
          "scotland",
          "wales",
          "northern-ireland"
        ],
        "description": "UK region slug. Wales rolls up the three Welsh services into one."
      },
      "Service": {
        "type": "object",
        "required": [
          "service_type",
          "service_category",
          "slug",
          "name",
          "short_name",
          "country",
          "region",
          "website_url",
          "recruitment_url",
          "description",
          "url"
        ],
        "properties": {
          "service_type": {
            "$ref": "#/components/schemas/ServiceType"
          },
          "service_category": {
            "$ref": "#/components/schemas/ServiceCategory"
          },
          "slug": {
            "type": "string",
            "example": "london-fire-brigade"
          },
          "name": {
            "type": "string",
            "example": "London Fire Brigade"
          },
          "short_name": {
            "type": [
              "string",
              "null"
            ],
            "example": "LFB"
          },
          "country": {
            "type": "string",
            "example": "UK"
          },
          "region": {
            "type": [
              "string",
              "null"
            ],
            "example": "London"
          },
          "website_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "example": "https://www.london-fire.gov.uk"
          },
          "recruitment_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "example": "https://www.london-fire.gov.uk/jobs/"
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical bluewatch.app page for this service."
          }
        }
      },
      "Opportunity": {
        "type": "object",
        "required": [
          "service_type",
          "service_slug",
          "service_name",
          "service_short_name",
          "service_region",
          "service_category",
          "station_slug",
          "station_name",
          "slug",
          "title",
          "summary",
          "role_type",
          "specialism",
          "is_cadet",
          "status",
          "posted_at",
          "closes_at",
          "first_seen_at",
          "last_seen_at",
          "apply_url",
          "source_url",
          "url"
        ],
        "properties": {
          "service_type": {
            "$ref": "#/components/schemas/ServiceType"
          },
          "service_slug": {
            "type": "string"
          },
          "service_name": {
            "type": "string"
          },
          "service_short_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "service_region": {
            "type": [
              "string",
              "null"
            ]
          },
          "service_category": {
            "$ref": "#/components/schemas/ServiceCategory"
          },
          "station_slug": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set when the opportunity is tied to a specific station (typical for on-call roles)."
          },
          "station_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "slug": {
            "type": "string",
            "description": "Stable identifier for this opportunity."
          },
          "title": {
            "type": "string"
          },
          "summary": {
            "type": [
              "string",
              "null"
            ]
          },
          "role_type": {
            "$ref": "#/components/schemas/RoleType"
          },
          "specialism": {
            "type": [
              "string",
              "null"
            ],
            "description": "Free-text specialism (e.g. \"USAR\", \"fire investigation\"). Set for some specialist roles."
          },
          "is_cadet": {
            "type": "boolean"
          },
          "status": {
            "$ref": "#/components/schemas/OpportunityStatus"
          },
          "posted_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "closes_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "first_seen_at": {
            "type": "string",
            "format": "date-time"
          },
          "last_seen_at": {
            "type": "string",
            "format": "date-time"
          },
          "apply_url": {
            "type": "string",
            "format": "uri",
            "description": "Authoritative submission destination on the issuing service's own site."
          },
          "source_url": {
            "type": "string",
            "format": "uri",
            "description": "The page Bluewatch scraped this opportunity from."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical bluewatch.app page for this opportunity."
          }
        }
      },
      "ListMeta": {
        "type": "object",
        "required": [
          "total",
          "limit",
          "offset",
          "service_type"
        ],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Total matching items across all pages."
          },
          "limit": {
            "type": "integer"
          },
          "offset": {
            "type": "integer"
          },
          "service_type": {
            "$ref": "#/components/schemas/ServiceType"
          }
        }
      },
      "ListLinks": {
        "type": "object",
        "required": [
          "self",
          "next",
          "prev"
        ],
        "properties": {
          "self": {
            "type": "string",
            "format": "uri"
          },
          "next": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "prev": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "OpportunityListResponse": {
        "type": "object",
        "required": [
          "data",
          "meta",
          "links"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Opportunity"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/ListMeta"
          },
          "links": {
            "$ref": "#/components/schemas/ListLinks"
          }
        }
      },
      "OpportunityDetailResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Opportunity"
          }
        }
      },
      "ServiceListResponse": {
        "type": "object",
        "required": [
          "data",
          "meta",
          "links"
        ],
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Service"
            }
          },
          "meta": {
            "$ref": "#/components/schemas/ListMeta"
          },
          "links": {
            "$ref": "#/components/schemas/ListLinks"
          }
        }
      },
      "ServiceDetailResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Service"
          }
        }
      },
      "MetaCounts": {
        "type": "object",
        "description": "Counts keyed by service_type.",
        "required": [
          "services",
          "stations",
          "live_opportunities"
        ],
        "properties": {
          "services": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "stations": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "live_opportunities": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      },
      "MetaResponse": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "version",
              "site_url",
              "docs_url",
              "counts",
              "latest_opportunity_at"
            ],
            "properties": {
              "version": {
                "type": "string",
                "example": "v1"
              },
              "site_url": {
                "type": "string",
                "format": "uri"
              },
              "docs_url": {
                "type": "string",
                "format": "uri"
              },
              "counts": {
                "$ref": "#/components/schemas/MetaCounts"
              },
              "latest_opportunity_at": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "date-time"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "example": "invalid_role"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid query parameter.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
