openapi: 3.1.0
info:
  title: DataQuoll
  description: |
    DataQuoll turns fragmented Australian public data into answers about locations, events and assets.

    Today's datasets: real-time emergency incidents and warnings (bushfires, floods, storms, earthquakes, rescues, and more) in one GeoJSON schema with CAP-aligned severity, urgency, and certainty fields; disaster declarations reported by LGA as declared-area status; live river gauges with BoM flood classes; and a historical archive. DataQuoll was formerly EmergencyAPI. Standards-validated CAP output is available as an Atom feed via ?format=cap-atom and as per-incident documents via /incidents/{id}?format=cap-au, both validated against the OASIS CAP 1.2 schema and the CAP-AU Profile v3.0. Includes real-time incidents, proximity search, and historical audit trail snapshots.

    **Disclaimer:** DataQuoll provides aggregated emergency incident data for informational
    purposes only. This data is sourced from official government feeds and may be delayed,
    incomplete, or inaccurate. Do not use this API as a substitute for official emergency warnings.
  version: 1.72.1
  contact:
    name: DataQuoll Support
    url: https://dataquoll.io
  license:
    name: Proprietary

servers:
  - url: https://dataquoll.io/api/v1
    description: Primary host
  - url: https://emergencyapi.com/api/v1
    description: Legacy host, identical responses until the announced sunset

paths:
  /incidents:
    get:
      operationId: listIncidents
      summary: List all current incidents
      description: Returns a GeoJSON FeatureCollection of all current emergency incidents, with support for filtering, pagination, and bounding box queries.
      tags: [Incidents]
      parameters:
        - name: state
          in: query
          description: "Comma-separated state codes (e.g. nsw,vic,qld)"
          schema:
            type: string
          example: nsw,vic
        - name: eventType
          in: query
          description: "Comma-separated event types (bushfire, burn_off, fire_ban, structure_fire, vehicle_fire, grass_fire, other_fire, hazmat, rescue, flood, storm, tree_down, cyclone, earthquake, extreme_heat, vehicle_accident, medical, alarm, other). For every kind of fire, use category=fire."
          schema:
            type: string
          example: bushfire,flood
        - name: category
          in: query
          description: "Comma-separated event categories. Groups related event types (e.g. fire includes bushfire, structure_fire, grass_fire, vehicle_fire, other_fire (a fire of another or unstated kind) and fire_ban). burn_off is its own category (planned). See /api/v1/schema for the full mapping."
          schema:
            type: string
          example: fire,weather
        - name: featureType
          in: query
          description: "Comma-separated feature types. Defaults to 'incident' (point incidents only), so boundary polygons never appear unless requested. Request them explicitly: incident_area, warning_area, fire_ban_area, road_disruption. See /api/v1/schema."
          schema:
            type: string
          example: warning_area
        - name: severity
          in: query
          description: "CAP-AU severity levels (Extreme, Severe, Moderate, Minor, Unknown)"
          schema:
            type: string
          example: Extreme,Severe
        - name: urgency
          in: query
          description: "CAP-AU urgency levels (Immediate, Expected, Future, Past, Unknown)"
          schema:
            type: string
          example: Immediate,Expected
        - name: certainty
          in: query
          description: "CAP-AU certainty levels (Observed, Likely, Possible, Unlikely, Unknown)"
          schema:
            type: string
          example: Observed,Likely
        - name: warningLevel
          in: query
          description: "Australian Warning System levels"
          schema:
            type: string
          example: watch_and_act,emergency_warning
        - name: status
          in: query
          description: "Incident status filter"
          schema:
            type: string
          example: active,contained
        - name: agency
          in: query
          description: "Source agency filter"
          schema:
            type: string
          example: rfs,cfs
        - name: official
          in: query
          description: >-
            Filter by source provenance. `true` excludes sources the feed registry marks
            unofficial (today the four South Australian community-decoded pager feeds);
            `false` returns only those sources. Omit it to return everything, which is the
            default and unchanged behaviour.


            Read the semantics before relying on it: `official` defaults to true in our feed
            configuration and is declared explicitly on only the unofficial sources, so
            `official=true` means "not marked unofficial", not "verified government source".
            An official feed can still carry individual ambulance dispatches. Use
            `/attribution` to see the flag per source.
          schema:
            type: boolean
        - name: bbox
          in: query
          description: "Bounding box: minLon,minLat,maxLon,maxLat. Records at precision 'state' (not placed) are never inside a box, so a bbox query leaves them out."
          schema:
            type: string
          example: "150.0,-34.0,152.0,-33.0"
        - name: limit
          in: query
          description: "Results per page (default 100, max 500)"
          schema:
            type: integer
            default: 100
            maximum: 500
        - name: cursor
          in: query
          description: "Opaque pagination cursor, taken from meta.next_cursor of the previous response. Absent next_cursor means there are no further pages."
          schema:
            type: string
        - name: after
          in: query
          description: "Alias for cursor. This is the parameter name emitted in links.next, and is accepted for backward compatibility. If both are supplied, after wins."
          schema:
            type: string
        - name: format
          in: query
          description: "Response format. CSV includes columns: id, state, agency, feed_id, title, event_type, status, warning_level, severity, urgency, certainty, lng, lat, address, suburb, lga, location_state, reported_at, updated_at, fetched_at, is_retracted, retracted_at, retraction_reason. cap-atom returns a standards-validated CAP Atom feed (each entry inlines a CAP-AU alert in <content>; incidents retracted in the last 7 days appear as Cancel messages). cap-au is the deprecated legacy <alerts> wrapper, kept for backward compatibility; new consumers should use cap-atom. CSV, cap-atom and cap-au require the Developer plan or above; requesting one on a lower plan returns 403."
          schema:
            type: string
            enum: [geojson, json, csv, cap-au, cap-atom]
            default: geojson
        - name: include_retracted
          in: query
          description: "Include retracted incidents (default false). Retracted incidents are marked when upstream feeds stop publishing them."
          schema:
            type: boolean
            default: false
        - name: include_boundary
          in: query
          description: "Set true to add properties.boundary: the area (Polygon or MultiPolygon) the agency published for each incident, or null when it published none. Off by default because polygons make list responses much larger. /incidents/{id} always includes it."
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: A GeoJSON FeatureCollection of incidents
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
          content:
            application/geo+json:
              schema:
                $ref: '#/components/schemas/IncidentCollection'
              example:
                type: FeatureCollection
                features:
                  - type: Feature
                    id: nsw-rfs-1234567
                    geometry:
                      type: Point
                      coordinates: [150.604, -33.883]
                    properties:
                      source:
                        state: nsw
                        agency: RFS
                        feedId: "1234567"
                      title: "Bush Fire - Warragamba"
                      eventType: bushfire
                      status: active
                      warningLevel: watch_and_act
                      severity: Severe
                      urgency: Expected
                      certainty: Observed
                      location:
                        address: "Warragamba Dam Rd, Warragamba NSW"
                        suburb: Warragamba
                        state: NSW
                        latitude: -33.883
                        longitude: 150.604
                      details:
                        description: "Bush fire burning in a south-easterly direction"
                      timestamps:
                        reported: "2026-04-07T14:30:00+10:00"
                        updated: "2026-04-07T16:45:00+10:00"
                        fetched: "2026-04-07T16:46:12+10:00"
                      retraction:
                        retracted: true
                        retractedAt: "2026-04-07T18:00:00+10:00"
                        reason: upstream_removed
                meta:
                  total_count: 245
                  has_more: true
                  next_cursor: cursor_xyz789
                  generatedAt: "2026-04-07T16:46:12+10:00"
                links:
                  self: "/v1/incidents?limit=100"
                  next: "/v1/incidents?limit=100&after=cursor_xyz789"
                attribution: "https://dataquoll.io/api/v1/attribution"
        '429':
          description: Rate limit exceeded
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
            Retry-After:
              description: Seconds until the monthly quota resets.
              schema:
                type: integer
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /incidents/{id}:
    get:
      operationId: getIncident
      summary: Get a single incident
      description: Returns a single incident as a GeoJSON Feature.
      tags: [Incidents]
      parameters:
        - name: id
          in: path
          required: true
          description: "Incident ID (e.g. nsw-rfs-1234567)"
          schema:
            type: string
        - name: format
          in: query
          description: "Response format. Default geojson returns a GeoJSON Feature. cap-au returns the incident as a single standards-valid CAP-AU <alert> document (Content-Type application/cap+xml), validated against the OASIS CAP 1.2 schema and the CAP-AU Profile v3.0. CSV, cap-atom and cap-au require the Developer plan or above; requesting one on a lower plan returns 403."
          schema:
            type: string
            enum: [geojson, cap-au]
            default: geojson
      responses:
        '200':
          description: A GeoJSON Feature, or a CAP-AU alert document when format=cap-au
          content:
            application/geo+json:
              schema:
                $ref: '#/components/schemas/IncidentFeature'
            application/cap+xml:
              schema:
                type: string
                description: A single CAP-AU <alert> document (XML).
        '404':
          description: Incident not found
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /incidents/nearby:
    get:
      operationId: nearbyIncidents
      summary: Find incidents near a location
      description: "Returns incidents within a radius (km) of a given lat/lng coordinate, sorted by distance. Records at precision 'state' are left out: their point only stands for somewhere in the state."
      tags: [Incidents]
      parameters:
        - name: lat
          in: query
          required: true
          description: "Latitude"
          schema:
            type: number
          example: -33.87
        - name: lng
          in: query
          required: true
          description: "Longitude"
          schema:
            type: number
          example: 151.21
        - name: radius
          in: query
          required: true
          description: "Radius in kilometres (max 500)"
          schema:
            type: number
          example: 50
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
        - name: state
          in: query
          schema:
            type: string
        - name: eventType
          in: query
          schema:
            type: string
        - name: severity
          in: query
          schema:
            type: string
        - name: eventCategory
          in: query
          schema:
            type: string
        - name: urgency
          in: query
          schema:
            type: string
        - name: certainty
          in: query
          schema:
            type: string
        - name: warningLevel
          in: query
          schema:
            type: string
        - name: agency
          in: query
          schema:
            type: string
        - name: official
          in: query
          description: >-
            Filter by source provenance. `true` excludes sources the feed registry marks
            unofficial (today the four South Australian community-decoded pager feeds);
            `false` returns only those sources. Omit it to return everything, which is the
            default and unchanged behaviour.


            Read the semantics before relying on it: `official` defaults to true in our feed
            configuration and is declared explicitly on only the unofficial sources, so
            `official=true` means "not marked unofficial", not "verified government source".
            An official feed can still carry individual ambulance dispatches. Use
            `/attribution` to see the flag per source.
          schema:
            type: boolean
        - name: status
          in: query
          description: "Comma-separated incident statuses (active, contained, controlled, safe, completed). Defaults to all."
          schema:
            type: string
        - name: include_retracted
          in: query
          description: "Set true to include retracted incidents. Default false; retracted incidents are hidden, matching /incidents."
          schema:
            type: boolean
            default: false
        - name: include_boundary
          in: query
          description: "Set true to add properties.boundary: the area (Polygon or MultiPolygon) the agency published for each incident, or null when it published none. Off by default because polygons make list responses much larger. /incidents/{id} always includes it."
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Nearby incidents as GeoJSON FeatureCollection
          content:
            application/geo+json:
              schema:
                $ref: '#/components/schemas/IncidentCollection'
        '400':
          description: Missing or invalid parameters
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /incidents/snapshot:
    get:
      operationId: incidentSnapshot
      summary: Audit trail snapshot
      description: >-
        Returns the incidents that were active at a specific point in time. Recent
        datetimes are reconstructed from live and lifecycle data. Datetimes before
        2026-04-08 are reconstructed from the historical archive (4.9M records) and
        are gated by the same tier lookback as /incidents/history (free has no
        archive access; Starter 1 year, Developer and Business 5 years, Pro and
        Enterprise unlimited).
        Archive (pre-2026-04-08) snapshots require a state filter, and meta.source
        indicates whether the result came from "live" or "archive". The answer
        ends at about 4 MB (archive flood extents come back whole): meta.size_limited
        and meta.truncated are then true, so narrow by state or event_type.
      tags: [Incidents]
      parameters:
        - name: datetime
          in: query
          required: true
          description: "ISO 8601 datetime to query (cannot be in the future)"
          schema:
            type: string
            format: date-time
          example: "2026-04-20T14:30:00+09:30"
        - name: state
          in: query
          description: "Filter by state code. Required for archive (pre-2026-04-08) snapshots."
          schema:
            type: string
          example: sa
        - name: event_type
          in: query
          description: "Filter by event type"
          schema:
            type: string
          example: bushfire
        - name: limit
          in: query
          description: "Max results (default 500)"
          schema:
            type: integer
            default: 500
            maximum: 500
        - name: include_boundary
          in: query
          description: "Set true to add properties.boundary: the area (Polygon or MultiPolygon) the agency published for each incident, or null when it published none. Off by default because polygons make list responses much larger. /incidents/{id} always includes it."
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Incidents active at the requested datetime
          content:
            application/geo+json:
              schema:
                $ref: '#/components/schemas/IncidentCollection'
        '400':
          description: Missing or invalid datetime, or an archive (pre-2026-04-08) snapshot requested without the required state filter
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '403':
          description: Archive access requires a paid plan, or the datetime predates your tier's lookback window
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /incidents/history:
    get:
      operationId: listHistoricalIncidents
      summary: Historical incident data (paid tier)
      description: "Returns historical emergency incidents from two sources, merged and ordered newest first: the imported archive (1840 to 1 May 2026, 4.9M records) and the records our own pollers have written since April 2026, which carry `boundary` where an agency published an area. `meta.sources` names which answered. Free tier is blocked. Starter is limited to 1 year of lookback, Developer and Business to 5 years, Pro, Organisation and Enterprise unlimited. A window too large to answer within the database budget returns 400 asking you to narrow it. Engine-era records also carry `featureType` (incidents by default; pass `featureType` to ask for warning_area or incident_area markers) and a `retraction` object only when an agency withdrew the record rather than letting it end: leaving the agency's live feed is the normal end of an incident and is not reported as a retraction here. Records are ordered by `reported_at` newest first, undated records last. PAGE WITH `cursor`: pass `meta.next_cursor` from one response as `cursor` on the next, or follow `links.next`. `before` and `after` bound the WINDOW and are not a paging mechanism: thousands of blocks of records share an identical `reported_at` (the largest holding 634,813), and advancing `before` past a page would skip the rest of that block. Records sharing an instant have no natural order; the order given is stable across pages, which is what the cursor relies on. A PAGE ALSO ENDS AT ABOUT 4 MB: archive polygons are returned whole (a single mapped flood extent can run to megabytes), so a page stops adding records once its size passes the cap, always serving at least one record. `meta.size_limited` is then true and `meta.has_more` true; keep following the cursor."
      tags: [Incidents]
      parameters:
        - name: state
          in: query
          description: "Comma-separated state codes"
          schema:
            type: string
          example: wa,sa
        - name: eventType
          in: query
          description: "Comma-separated event types"
          schema:
            type: string
          example: bushfire,flood
        - name: official
          in: query
          description: >-
            Filter by source provenance. `true` excludes sources the feed registry marks
            unofficial (today the four South Australian community-decoded pager feeds);
            `false` returns only those sources. Omit it to return everything, which is the
            default and unchanged behaviour.


            Read the semantics before relying on it: `official` defaults to true in our feed
            configuration and is declared explicitly on only the unofficial sources, so
            `official=true` means "not marked unofficial", not "verified government source".
            An official feed can still carry individual ambulance dispatches. Use
            `/attribution` to see the flag per source.
          schema:
            type: boolean
        - name: after
          in: query
          description: "Start of date range (ISO 8601, inclusive). Tier-limited."
          schema:
            type: string
            format: date-time
          example: "2022-01-01T00:00:00Z"
        - name: before
          in: query
          description: "End of date range (ISO 8601, exclusive). Defaults to now."
          schema:
            type: string
            format: date-time
        - name: cursor
          in: query
          description: "Opaque pagination cursor. Pass `meta.next_cursor` from the previous response, or follow `links.next`. Use this rather than advancing `before`, which cannot page through records sharing one `reported_at`."
          schema:
            type: string
        - name: limit
          in: query
          description: "Max results per page (1-500, default 100)"
          schema:
            type: integer
            default: 100
            maximum: 500
      responses:
        '200':
          description: Historical incidents matching filters
          content:
            application/geo+json:
              schema:
                $ref: '#/components/schemas/IncidentCollection'
        '403':
          description: Free tier blocked or depth limit exceeded
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /states:
    get:
      operationId: listStates
      summary: Feed health by state
      description: Returns the status of each state feed, including last poll time, health, and incident count.
      tags: [Meta]
      responses:
        '200':
          description: Feed status for all states
          content:
            application/json:
              schema:
                type: object
                properties:
                  states:
                    type: array
                    items:
                      type: object
                      properties:
                        state:
                          type: string
                        agency:
                          type: string
                        status:
                          type: string
                          enum: [healthy, degraded, down, unknown]
                        lastPolled:
                          type: string
                          format: date-time
                        lastSuccess:
                          type: string
                          format: date-time
                        incidentCount:
                          type: integer
                        errorCount:
                          type: integer

  /declarations/postcode/{postcode}:
    get:
      operationId: declarationsByPostcode
      summary: Disaster declaration status for a postcode
      description: "Returns the declaration STATUS for a postcode: declared, partial, activation_only, not_declared, uncertain, or unknown_postcode, with per-LGA detail and official source links. Reports declared-area status only; the billing/eligibility decision rests with the practitioner. Built for Medicare disaster telehealth checks."
      tags: [Declarations]
      parameters:
        - name: postcode
          in: path
          required: true
          schema: { type: string, pattern: '^\d{4}$' }
          example: '4380'
        - name: as_at
          in: query
          description: Point-in-time check (YYYY-MM-DD). Defaults to today.
          schema: { type: string, format: date }
        - name: include_expired
          in: query
          description: Include expired records in the per-LGA lists. Never changes matchStatus.
          schema: { type: boolean, default: false }
        - name: instrument_type
          in: query
          description: Trim the returned record lists to one instrument type. Never changes matchStatus.
          schema: { type: string, enum: [state_natural_disaster_declaration, drfa_activation_agrn, state_of_disaster_emergency_powers] }
        - name: suburb
          in: query
          description: "Optional suburb/locality name (ABS SAL) to narrow the result to the local government areas that suburb falls in, within this postcode only. Matching is exact after normalisation; there is no fuzzy matching. An unmatched suburb never fails the lookup: the full-postcode result is returned with narrowing.applied=false, a reason, and the postcode's known suburbs. Narrowing can legitimately change matchStatus (for example partial to declared); a suburb that itself spans two LGAs stays partial, and a bare name that denotes suburbs on both sides of a border (Mingoola in 4380) matches all of them."
          schema: { type: string }
          example: 'Stanthorpe'
      responses:
        '200':
          description: Declaration status (including unknown_postcode for an absent postcode)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PostcodeDeclarationResult' }
        '400':
          description: Malformed postcode or as_at
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '401':
          description: Missing or invalid API key
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '403':
          description: The declarations API is not on this account (it comes with the Organisation licence)
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '429':
          description: Rate limit exceeded
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
            Retry-After:
              description: Seconds until the monthly quota resets.
              schema:
                type: integer
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }

  /declarations/point:
    post:
      operationId: declarationsByPoint
      summary: Disaster declaration status for an exact point
      description: "Returns the declaration STATUS for the local government area containing a lat/lng point, resolved against ABS LGA 2022 (ASGS Edition 3) boundary polygons at full resolution. POST with a JSON body is used deliberately so coordinates never appear in URLs or request logs; the request is read-only and safe to retry. Coordinates are never stored, logged, or echoed back, and the response is Cache-Control: no-store. Reports declared-area status only; the billing decision rests with the practitioner. A point outside every Australian LGA returns unknown_location. A point exactly on a shared LGA boundary returns every covering area with onBoundary=true. LGA matching is at ABS code level. ABS gives each state one catch-all code for land outside councils, which joins separate places (Unincorporated NSW is the Far West, Lord Howe Island and parts of Sydney Harbour). Where sources name those places (NSW and VIC), the matched LGA carries the part the point is in as area, and records placed in another part are left out; a record published for a whole catch-all without naming a part carries scope part_not_stated."
      tags: [Declarations]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PointLookupRequest' }
      responses:
        '200':
          description: Declaration status for the covering LGA (including unknown_location when no LGA covers the point)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PointDeclarationResult' }
        '400':
          description: Invalid body, lat, lng, or as_at. Error detail never repeats submitted values.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '401':
          description: Missing or invalid API key
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '403':
          description: The declarations API is not on this account (it comes with the Organisation licence)
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '429':
          description: Rate limit exceeded
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
            Retry-After:
              description: Seconds until the monthly quota resets.
              schema:
                type: integer
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }

  /hazard-history/point:
    post:
      operationId: hazardHistoryByPoint
      summary: Observed record and declared status for an exact point
      description: "What official sources recorded near a location, unioned with the disaster declarations that covered its local government area. Two lanes are returned because both are required: the observed lane (historical archive plus live-era incident history within a radius) and the declared lane (declarations for the covering ABS LGA, counted once per AGRN even when several authorities recorded the same event). A feed that republishes another feed's incidents is left out of the observed lane so they are not counted twice (today nsw-rfs-cap, which repeats nsw-rfs). Medical calls are left out for privacy, alarm calls because an alarm is not a recorded hazard (the agency either found no fire or did not record the outcome), and road and traffic feeds because they record roadworks, closures and traffic rather than hazards. Mapped areas (fire footprints, official warning areas) stay in, because they record what reached a place, so one fire can count once per source that recorded it. Measured example: a Lismore address returns the February 2022 flood only through the declared lane, because the spatial lane holds no record for it at address precision. POST with a JSON body is used deliberately so coordinates never appear in URLs or request logs; the request is read-only and safe to retry. Coordinates are never stored, logged, or echoed back, and the response is Cache-Control: no-store. This reports a RECORD and a declared STATUS. It is not an assessment of risk, insurability, or any obligation, and absence of a record is not evidence that nothing happened. Precision is reported per record: a record at postcode precision sits at a postcode centroid, which can be kilometres from the event."
      tags: [Hazard history]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HazardHistoryRequest' }
      responses:
        '200':
          description: The observed record and declared status for the point. A point outside every Australian LGA returns a null location and an empty declared lane. This response uses one hazard history lookup.
          headers:
            X-DataQuoll-Lookups-Remaining:
              description: Hazard history lookups left this UTC month. Absent for accounts with no lookup ceiling.
              schema: { type: integer, minimum: 0 }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/HazardHistoryResult' }
        '400':
          description: Invalid body, lat, lng, radiusKm, or records. Error detail never repeats submitted values.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '401':
          description: Missing or invalid API key
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '403':
          description: The account has used its hazard history lookups for the month (100 free, then blocks of 1,000)
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '429':
          description: Rate limit exceeded
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
            Retry-After:
              description: Seconds until the monthly quota resets.
              schema:
                type: integer
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }

  /declarations:
    get:
      operationId: listDeclarations
      summary: List/filter declarations
      description: "Browse and historically report on declaration records. Filters and keyset (cursor) pagination. Record status is derived at query time."
      tags: [Declarations]
      parameters:
        - name: jurisdiction
          in: query
          schema: { type: string, example: nsw }
        - name: status
          in: query
          schema: { type: string, enum: [active, expired, all], default: active }
        - name: instrument_type
          in: query
          schema: { type: string }
        - name: disaster_type
          in: query
          schema: { type: string }
        - name: active_on
          in: query
          schema: { type: string, format: date }
        - name: limit
          in: query
          schema: { type: integer, default: 100, maximum: 500 }
        - name: cursor
          in: query
          description: Opaque pagination cursor from links.next.
          schema: { type: string }
      responses:
        '200':
          description: A page of declaration records
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeclarationList' }
        '400':
          description: Invalid active_on date, status, limit, or cursor
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '401':
          description: Missing or invalid API key
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '403':
          description: The declarations API is not on this account (it comes with the Organisation licence)
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '429':
          description: Rate limit exceeded
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
            Retry-After:
              description: Seconds until the monthly quota resets.
              schema:
                type: integer
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }

  /declarations/agrn/{agrn}:
    get:
      operationId: declarationByAgrn
      summary: One disaster event by AGRN (across sources)
      description: "The declaration/activation for an Australian Government Reference Number, merged across sources with the full affected-LGA list."
      tags: [Declarations]
      parameters:
        - name: agrn
          in: path
          required: true
          schema: { type: string, pattern: '^\d{1,6}$' }
          example: '1284'
      responses:
        '200':
          description: The merged declaration record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeclarationDetail' }
        '400':
          description: Malformed AGRN (must be numeric)
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '401':
          description: Missing or invalid API key
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '403':
          description: The declarations API is not on this account (it comes with the Organisation licence)
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '404':
          description: No declaration found for the AGRN
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '429':
          description: Rate limit exceeded
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
            Retry-After:
              description: Seconds until the monthly quota resets.
              schema:
                type: integer
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }

  /status:
    get:
      operationId: apiStatus
      summary: API health check
      description: Returns API health, version, uptime, and a summary of feed statuses.
      tags: [Meta]
      responses:
        '200':
          description: API health status
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  version:
                    type: string
                    example: 1.16.0
                  uptime:
                    type: string
                    example: 86400s
                  timestamp:
                    type: string
                    format: date-time
                  counts:
                    type: object
                    description: "Summary of feed statuses. 'stale' is derived from last_polled_at age (a feed not polled within 2 hours reads as stale, so a dead poller is visible to monitors)."
                    properties:
                      total:
                        type: integer
                      healthy:
                        type: integer
                      degraded:
                        type: integer
                      down:
                        type: integer
                      stale:
                        type: integer
                        description: "Feeds not polled within 2 hours, derived from last_polled_at age regardless of stored status."
                      unknown:
                        type: integer
                  feeds:
                    type: array
                    items:
                      type: object
                      properties:
                        feedId:
                          type: string
                          description: >-
                            The feed's registry id, and the unique key for this entry.
                            `state` and `agency` are NOT unique: two New South Wales RFS
                            feeds and two Tasmanian TasALERT feeds each share a
                            state/agency pair, so use `feedId` when you need to identify
                            one exactly.
                          example: nsw-rfs-cap
                        state:
                          type: string
                        agency:
                          type: string
                        status:
                          type: string
                          enum: [healthy, degraded, down, stale, unknown]

  /attribution:
    get:
      operationId: getAttribution
      summary: Data source attributions
      description: Returns attribution information for all data sources, including licence details. Required for CC BY compliance.
      tags: [Meta]
      responses:
        '200':
          description: Attribution data for all sources
          content:
            application/json:
              schema:
                type: object
                properties:
                  description:
                    type: string
                  disclaimer:
                    type: string
                  count:
                    type: integer
                    description: "Number of sources returned"
                  sources:
                    type: array
                    items:
                      type: object
                      properties:
                        state:
                          type: string
                        agency:
                          type: string
                        name:
                          type: string
                        licence:
                          type: string
                          description: "The published licence. Only recognised CC licences or an explicit 'Used with permission' statement are shown; anything else (internal working notes, unconfirmed sources) is reported as 'Licence under review — see https://dataquoll.io/compliance'."
                        url:
                          type: string
                        official:
                          type: boolean
                          description: "true for official government/statutory feeds, false for community-decoded or unverified sources."
                        attribution:
                          type: string
                          description: "Agency-specified attribution text, present only when the source requires it."

  /events:
    get:
      operationId: listEvents
      summary: List clustered events (incident intelligence)
      description: |
        Returns spatially clustered emergency events as a GeoJSON FeatureCollection.
        Events group related incidents using PostGIS ST_ClusterDBSCAN spatial clustering.
        Each event has a boundary polygon, centroid, affected suburbs, contributing agencies,
        and auto-generated title and summary. Only active events are returned by default.
      tags: [Events]
      parameters:
        - name: limit
          in: query
          description: "Maximum events to return (default 50, max 200)"
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: state
          in: query
          description: "Comma-separated state codes to filter by (e.g. nsw,vic). Uses array overlap matching."
          schema:
            type: string
          example: nsw,vic
        - name: eventType
          in: query
          description: "Comma-separated event types (e.g. bushfire,flood)"
          schema:
            type: string
          example: bushfire,flood
        - name: severity
          in: query
          description: "Comma-separated max severity levels"
          schema:
            type: string
          example: Extreme,Severe
        - name: warningLevel
          in: query
          description: "Comma-separated max warning levels"
          schema:
            type: string
          example: emergency_warning,watch_and_act
      responses:
        '200':
          description: A GeoJSON FeatureCollection of clustered events
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
          content:
            application/geo+json:
              schema:
                $ref: '#/components/schemas/EventCollection'
        '401':
          description: Missing or invalid API key
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '429':
          description: Rate limit exceeded
          headers:
            RateLimit-Limit:
              schema:
                type: integer
            RateLimit-Remaining:
              schema:
                type: integer
            RateLimit-Reset:
              schema:
                type: integer
            Retry-After:
              description: Seconds until the monthly quota resets.
              schema:
                type: integer
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /events/{id}:
    get:
      operationId: getEvent
      summary: Get a single event with optional linked incidents
      description: |
        Returns a single clustered event by ID. Use `include_incidents=true` to also
        retrieve all incidents that belong to this event cluster.
      tags: [Events]
      parameters:
        - name: id
          in: path
          required: true
          description: Event UUID
          schema:
            type: string
            format: uuid
        - name: include_incidents
          in: query
          description: "Include linked incidents in the response"
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: A single event as a GeoJSON Feature
          content:
            application/geo+json:
              schema:
                $ref: '#/components/schemas/EventFeature'
        '401':
          description: Missing or invalid API key
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'
        '404':
          description: Event not found
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /schema:
    get:
      operationId: getSchema
      summary: API schema and valid enum values
      description: Returns all valid enum values for event types, categories, states, warning levels, and other filterable fields. Use this to discover what values are accepted by filter parameters. No authentication required.
      tags: [Meta]
      responses:
        '200':
          description: All valid enum values and category mappings
          content:
            application/json:
              schema:
                type: object
                properties:
                  eventTypes:
                    type: array
                    items:
                      type: string
                  eventCategories:
                    type: array
                    items:
                      type: string
                  categoryMapping:
                    type: object
                    description: "Maps each category to its constituent event types"
                  states:
                    type: array
                    items:
                      type: string
                  warningLevels:
                    type: array
                    items:
                      type: string
                  statuses:
                    type: array
                    items:
                      type: string
                  severities:
                    type: array
                    items:
                      type: string
                  urgencies:
                    type: array
                    items:
                      type: string
                  certainties:
                    type: array
                    items:
                      type: string
                  formats:
                    type: array
                    items:
                      type: string

  /gauges:
    get:
      operationId: listGauges
      summary: River gauge stations with live flood status
      description: >-
        Returns river gauge stations with their latest water level, the Bureau of
        Meteorology flood classification thresholds for each gauge, and a derived
        floodClass (below_minor, minor, moderate, major, or unclassified) computed
        from the latest reading against the published thresholds. Water-level readings
        are in metres relative to each gauge's local datum (each gauge carries its own
        unit, since some report in AHD). readingStatus flags stale data
        (bom-kiwis sourced stations are a 1-2 day archive, stale horizon 72 hours; bcc-telemetry are event-driven council ALERT loggers, stale horizon 48 hours;
        telemetry sources 24 hours). Defaults to water-level gauges; pass
        variable=rainfall for observed rainfall gauges, which report incremental
        millimetres and carry no flood classification. Status reporting, not flood prediction.
      tags: [Gauges]
      parameters:
        - name: variable
          in: query
          description: "Measured variable. Defaults to water_level; rainfall returns observed rain gauges (millimetres, incremental, no flood class)."
          schema:
            type: string
            enum: [water_level, rainfall]
            default: water_level
        - name: state
          in: query
          description: "Lowercase state code (nsw, vic, qld, sa, wa, tas, nt, act)"
          schema:
            type: string
          example: nsw
        - name: floodClass
          in: query
          description: "Filter to gauges at or above this class (>= semantics: minor matches moderate and major too). Water-level only; cannot combine with variable=rainfall."
          schema:
            type: string
            enum: [minor, moderate, major]
        - name: lga
          in: query
          description: "ABS LGA code (the same LGA_2022 vocabulary as the declarations API)"
          schema:
            type: string
          example: "17750"
        - name: source
          in: query
          description: "Polling source (hydstra-nsw, hydstra-vic, hydstra-qld, aquarius-sa, aquarius-tas, aquarius-nt, bom-kiwis, bcc-telemetry)"
          schema:
            type: string
        - name: bbox
          in: query
          description: "Bounding box minLng,minLat,maxLng,maxLat (within Australia)"
          schema:
            type: string
          example: "138.4,-35.4,139.0,-34.6"
        - name: limit
          in: query
          description: "Max results per page (1-500, default 100)"
          schema:
            type: integer
            default: 100
            maximum: 500
        - name: cursor
          in: query
          description: "Pagination cursor from meta.nextCursor"
          schema:
            type: string
      responses:
        '200':
          description: Gauge stations with latest readings and flood status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GaugeCollection'
        '401':
          description: Missing or invalid API key
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /gauges/{id}:
    get:
      operationId: getGauge
      summary: One gauge with a 6-hour recent window
      description: >-
        Returns a single gauge station plus its trailing 6 hours of raw readings
        (enough to see the current trend). Deeper history is the tier-gated
        /gauges/{id}/readings endpoint.
      tags: [Gauges]
      parameters:
        - name: id
          in: path
          required: true
          description: "Gauge id from /gauges (e.g. hydstra-nsw-410001)"
          schema:
            type: string
      responses:
        '200':
          description: The gauge and its recent readings
          content:
            application/json:
              schema:
                type: object
                properties:
                  gauge:
                    $ref: '#/components/schemas/Gauge'
                  recent:
                    type: array
                    items:
                      $ref: '#/components/schemas/GaugeReading'
        '404':
          description: No active gauge with that id
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /gauges/{id}/readings:
    get:
      operationId: listGaugeReadings
      summary: Gauge reading history (paid tier)
      description: >-
        Historical water level readings for one gauge. Free tier is blocked (the
        free surface is current readings on /gauges and the 6-hour window on
        /gauges/{id}). Starter tier limited to 1 year lookback, Developer and
        Business to 5 years, Pro and Enterprise unlimited. Retention ladder: raw
        30 days, hourly rollups 90 days, daily forever — plus full-resolution raw
        inside flood-event windows kept forever. interval=auto picks the finest
        granularity available for the requested window.
      tags: [Gauges]
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
        - name: after
          in: query
          description: "Start of range (ISO 8601, inclusive). Tier-limited."
          schema:
            type: string
            format: date-time
        - name: before
          in: query
          description: "End of range (ISO 8601, exclusive). Defaults to now."
          schema:
            type: string
            format: date-time
        - name: interval
          in: query
          description: "raw, hourly, daily, or auto (default)"
          schema:
            type: string
            enum: [raw, hourly, daily, auto]
            default: auto
        - name: above
          in: query
          description: "Only readings at/above this published flood classification for the gauge"
          schema:
            type: string
            enum: [minor, moderate, major]
        - name: limit
          in: query
          schema:
            type: integer
            default: 500
            maximum: 1000
        - name: cursor
          in: query
          description: "Pagination cursor from meta.nextCursor (ISO timestamp keyset)"
          schema:
            type: string
      responses:
        '200':
          description: Readings for the gauge, newest first
          content:
            application/json:
              schema:
                type: object
                properties:
                  gaugeId:
                    type: string
                  interval:
                    type: string
                  readings:
                    type: array
                    items:
                      $ref: '#/components/schemas/GaugeReading'
        '403':
          description: Free tier blocked or depth limit exceeded
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/ProblemDetails'

  /gauges/summary:
    get:
      operationId: getGaugeSummary
      summary: Flood status counts by state or LGA
      description: >-
        Counts of gauges at or above minor/moderate/major flood classification,
        grouped by state or LGA, plus the full list of at-or-above-minor stations
        (capped at 500). STALE GUARD: only readings within the staleness horizon
        enter class counts — a days-old reading above minor is counted as stale,
        never as a current flood signal.
      tags: [Gauges]
      parameters:
        - name: variable
          in: query
          description: "Measured variable. Defaults to water_level (rain gauges carry no flood class, so they are excluded from the counts by default)."
          schema:
            type: string
            enum: [water_level, rainfall]
            default: water_level
        - name: groupBy
          in: query
          description: "state (default) or lga (requires the state parameter)"
          schema:
            type: string
            enum: [state, lga]
            default: state
        - name: state
          in: query
          schema:
            type: string
          example: sa
      responses:
        '200':
          description: Flood status summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: object
                    properties:
                      total:
                        type: integer
                      minor:
                        type: integer
                      moderate:
                        type: integer
                      major:
                        type: integer
                      stale:
                        type: integer
                      unclassified:
                        type: integer
                      atOrAboveMinor:
                        type: integer
                  groups:
                    type: array
                    items:
                      type: object
                  aboveMinorStations:
                    type: array
                    items:
                      $ref: '#/components/schemas/Gauge'

  /facilities:
    get:
      operationId: listFacilities
      summary: Emergency service facility locations
      description: >
        Where responders come from: the locations of police, ambulance, metropolitan fire,
        rural and country fire, and SES facilities across Australia, from Geoscience Australia
        under CC BY 4.0. A static reference layer, not a live feed.


        Two lanes, and at least one is required. Either a radius search (lat, lng and radius
        together) or a filter (state and/or class). A bare call is rejected rather than
        returning all 8,888 facilities.


        Address columns derived from G-NAF are deliberately not collected, because they carry
        a separate licence to the rest of the dataset.
      tags: [Facilities]
      parameters:
        - name: lat
          in: query
          description: Latitude of the search centre. Required with lng and radius.
          schema: { type: number, minimum: -90, maximum: 90 }
        - name: lng
          in: query
          description: Longitude of the search centre. Required with lat and radius.
          schema: { type: number, minimum: -180, maximum: 180 }
        - name: radius
          in: query
          description: Search radius in kilometres. Required with lat and lng.
          schema: { type: number, exclusiveMinimum: 0, maximum: 500 }
        - name: state
          in: query
          description: >
            Lowercase state code. Note that three Australian Federal Police facilities in the
            Indian Ocean Territories carry no state and are returned only by radius search.
          schema: { type: string, enum: [nsw, vic, qld, sa, wa, tas, nt, act] }
        - name: class
          in: query
          description: Facility class, as a short alias or the full source string.
          schema: { type: string, enum: [police, ambulance, fire, rural_fire, ses, other] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        '200':
          description: Matching facilities
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  disclaimer:
                    type: string
                    description: States that a listed facility is not necessarily staffed or able to respond.
                  facilities:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string }
                        name: { type: string, nullable: true }
                        class:
                          type: string
                          enum:
                            - AMBULANCE STATION
                            - POLICING FACILITY
                            - METRO FIRE FACILITY
                            - RURAL/COUNTRY FIRE SERVICE FACILITY
                            - SES FACILITY
                            - OTHER EMERGENCY MANAGEMENT FACILITY
                        operationalStatus: { type: string, nullable: true }
                        coordinates:
                          type: object
                          nullable: true
                          properties:
                            lat: { type: number }
                            lng: { type: number }
                        location:
                          type: object
                          properties:
                            address: { type: string, nullable: true }
                            suburb: { type: string, nullable: true }
                            state: { type: string, nullable: true }
                            postcode: { type: string, nullable: true }
                        spatialConfidence: { type: string, nullable: true }
                        source:
                          type: object
                          properties:
                            publisher: { type: string }
                            agency:
                              type: string
                              nullable: true
                              description: The state or territory agency Geoscience Australia sourced the record from.
                        fetchedAt: { type: string, format: date-time }
        '400':
          description: Neither lane supplied, or a parameter out of range
        '401':
          description: Missing or invalid API key
        '429':
          description: Rate limit exceeded
  /calendars:
    get:
      operationId: listCalendars
      summary: Industry calendar editions
      description: Every industry-calendar edition on the platform, one per jurisdiction and year (NSW, VIC, QLD, WA, SA, TAS, ACT and NT for 2026), with its publisher, cover range and per-category counts. RDOs, public holidays, school holidays and industry shutdowns, read from each branch calendar and verified against the Fair Work Ombudsman list and the education departments' term dates.
      tags: [Calendars]
      responses:
        '200':
          description: The editions
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  calendars:
                    type: array
                    items:
                      type: object
                      properties:
                        jurisdiction: { type: string }
                        year: { type: integer }
                        title: { type: string }
                        publisher: { type: string }
                        source_url: { type: string, format: uri }
                        applies_to: { type: string, description: Who the calendar is for. An RDO is the contracting industry's day, not a public holiday. }
                        covers: { type: object, properties: { from: { type: string, format: date }, to: { type: string, format: date } } }
                        timezone: { type: string, example: Australia/Adelaide }
                        counts: { type: object, additionalProperties: { type: integer }, description: Rows per category }
                        discrepancies: { type: integer, description: Recorded disagreements between the calendar and the official list }
                        path: { type: string, example: /api/v1/calendars/sa }
  /calendars/{jurisdiction}:
    get:
      operationId: getCalendarDays
      summary: Calendar days for a jurisdiction
      description: The day rows for one jurisdiction across a date range, each date answered by exactly one edition. Defaults to the current calendar year in the jurisdiction's own time zone; a lone from or to extends to the edge of its own year. Recorded disagreements between the calendar and the official list are returned under discrepancies. Rows can be CSV with format=csv.
      tags: [Calendars]
      parameters:
        - name: jurisdiction
          in: path
          required: true
          description: Jurisdiction code
          schema: { type: string, enum: [nsw, vic, qld, sa, wa, tas, act, nt] }
        - name: from
          in: query
          description: First date, YYYY-MM-DD
          schema: { type: string, format: date }
        - name: to
          in: query
          description: Last date, YYYY-MM-DD
          schema: { type: string, format: date }
        - name: category
          in: query
          description: Comma-separated categories to include (default all). Values are the CalendarDay category enum.
          schema: { type: string, example: "rdo,public_holiday" }
        - name: format
          in: query
          description: json (default) or csv
          schema: { type: string, enum: [json, csv] }
      responses:
        '200':
          description: The days in range
          content:
            application/json:
              schema:
                type: object
                properties:
                  jurisdiction: { type: string }
                  timezone: { type: string, example: Australia/Adelaide }
                  from: { type: string, format: date }
                  to: { type: string, format: date }
                  count: { type: integer }
                  days: { type: array, items: { $ref: '#/components/schemas/CalendarDay' } }
                  editions: { type: array, items: { $ref: '#/components/schemas/CalendarEdition' } }
                  discrepancies: { type: array, items: { $ref: '#/components/schemas/CalendarDiscrepancy' } }
            text/csv:
              schema: { type: string }
        '400':
          description: Unknown jurisdiction, malformed date, unknown category, or from after to
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
  /calendars/{jurisdiction}/{date}:
    get:
      operationId: getCalendarDay
      summary: Is this a working day
      description: The answer for one date in one jurisdiction. A jurisdiction-wide public holiday wins, then the weekend, then the calendar's own entries in order (industry shutdown, RDO, part-day holiday, annual leave, industry event); otherwise working. Restricted holidays (a show day for one region, a public-service-only day) never decide it and are returned under restricted for the caller to apply to the employer's area.
      tags: [Calendars]
      parameters:
        - name: jurisdiction
          in: path
          required: true
          description: Jurisdiction code
          schema: { type: string, enum: [nsw, vic, qld, sa, wa, tas, act, nt] }
        - name: date
          in: path
          required: true
          description: The date, YYYY-MM-DD
          schema: { type: string, format: date }
      responses:
        '200':
          description: The verdict
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CalendarVerdict' }
        '400':
          description: Unknown jurisdiction or malformed date
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '404':
          description: No edition covers the date; the detail names the editions that exist
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }

  /publications:
    get:
      operationId: listPublications
      summary: Official documents from the publisher registry
      description: >
        Official documents, newest first: media releases, news, alerts and notices from the
        publishers in the registry (listPublishers). Today that is South Australia Police media
        releases, collected continuously since 26 September 2026 and kept after the publisher
        deletes them. Filter by state, publisher type or source, document kind, a publisher's own
        section, and a UTC date range; page with the cursor in links.next.


        Free keys see the last 7 days, from 00:00 UTC seven days back. A since or until outside
        that window is refused with 403 and counts against quota. Paid plans see the whole archive.


        A media release follows the event, often by hours or days, and an appeal for information
        is not a finding of fact. Place and time extracted from the text appear under extract once
        the extraction pass has run, and are null until then, never guessed. Images are counted in
        imageCount; whether they are served is set per publisher.
      tags: [Publications]
      parameters:
        - name: state
          in: query
          description: Jurisdiction code, or a comma list. A jurisdiction with no publisher yet returns an empty list, not an error.
          example: sa
          schema: { type: string }
        - name: type
          in: query
          description: "Publisher type, or a comma list: police, fire, ses, ambulance, health, transport, council, union, other."
          example: police
          schema: { type: string }
        - name: source
          in: query
          description: Publisher id from listPublishers, or a comma list.
          example: sa-police
          schema: { type: string }
        - name: kind
          in: query
          description: "Document kind, or a comma list: media_release, news, alert, notice."
          example: media_release
          schema: { type: string }
        - name: section
          in: query
          description: One of the publisher's own sections. For South Australia Police, a district, region or topic such as northern-police-district, traffic or missing-persons.
          example: northern-police-district
          schema: { type: string }
        - name: since
          in: query
          description: First UTC date, YYYY-MM-DD, inclusive.
          example: '2026-09-21'
          schema: { type: string, format: date }
        - name: until
          in: query
          description: Last UTC date, YYYY-MM-DD, inclusive.
          example: '2026-09-27'
          schema: { type: string, format: date }
        - name: includeRemoved
          in: query
          description: true to include documents the publisher has since taken down. They stay in the archive with removedAt set. Default false.
          example: false
          schema: { type: boolean, default: false }
        - name: limit
          in: query
          example: 20
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
        - name: cursor
          in: query
          description: The opaque cursor from links.next.
          schema: { type: string }
      responses:
        '200':
          description: The page of publications
          content:
            application/json:
              schema:
                type: object
                properties:
                  publications: { type: array, items: { $ref: '#/components/schemas/Publication' } }
                  meta:
                    type: object
                    properties:
                      generatedAt: { type: string, format: date-time }
                      count: { type: integer }
                      depthDays: { type: [integer, 'null'], description: Days of history this key sees; null for the whole archive. }
                  links: { type: object, properties: { next: { type: [string, 'null'] } } }
                  attribution: { type: string, format: uri }
              example:
                publications:
                  - id: 83c37297-3803-404c-babd-02225d57372e
                    source: sa-police
                    publisher: { name: South Australia Police, agency: SAPOL, type: police, jurisdiction: sa }
                    kind: media_release
                    slug: dashcam-sought-after-eudunda-road-rage
                    title: Dashcam sought after Eudunda road rage
                    summary: Police are seeking witnesses and dashcam footage following a road rage incident at Eudunda.
                    publishedAt: '2026-09-26T00:26:36+00:00'
                    modifiedAt: '2026-09-26T00:26:54+00:00'
                    sections: [barossa-region, more-news, rss-news, yorke-mid-north-region]
                    url: https://www.police.sa.gov.au/sa-police-news-assets/front-page-news/dashcam-sought-after-eudunda-road-rage
                    versionCount: 1
                    removedAt: null
                    attributes: { crimeStoppersRef: '251098', metaTopstory: 'no' }
                    extract: null
                    imageCount: 0
                meta: { generatedAt: '2026-09-27T03:00:00.000Z', count: 1, depthDays: null }
                links: { next: null }
                attribution: https://dataquoll.io/api/v1/attribution
        '400':
          description: Unknown state, type, source or kind, a malformed date, section, limit or cursor
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '401':
          description: Missing or invalid API key
        '403':
          description: A free key asked for dates outside its 7-day window
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '429':
          description: Rate limit exceeded
  /publications/{id}:
    get:
      operationId: getPublication
      summary: One publication in full
      description: >
        One publication in full: the text body, every path it was published under, and its
        version list. Publishers edit releases in place and every edit is kept as a new version;
        the current version is returned unless version is given. images is null while the
        publisher's images are withheld (South Australia Police images are withheld pending legal
        advice on identifiable people) and for a document the publisher has taken down (removedAt
        set); otherwise a list of one-hour signed URLs.
        Free keys can read publications from the last 7 days.
      tags: [Publications]
      parameters:
        - name: id
          in: path
          required: true
          description: Publication id (a UUID) from listPublications.
          example: 83c37297-3803-404c-babd-02225d57372e
          schema: { type: string, format: uuid }
        - name: version
          in: query
          description: A version number from the version list. Default the current version.
          example: 1
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: The publication
          content:
            application/json:
              schema:
                type: object
                properties:
                  publication: { $ref: '#/components/schemas/PublicationFull' }
                  meta: { type: object, properties: { generatedAt: { type: string, format: date-time } } }
                  attribution: { type: string, format: uri }
              example:
                publication:
                  id: 83c37297-3803-404c-babd-02225d57372e
                  source: sa-police
                  publisher: { name: South Australia Police, agency: SAPOL, type: police, jurisdiction: sa }
                  kind: media_release
                  slug: dashcam-sought-after-eudunda-road-rage
                  title: Dashcam sought after Eudunda road rage
                  summary: Police are seeking witnesses and dashcam footage following a road rage incident at Eudunda.
                  publishedAt: '2026-09-26T00:26:36+00:00'
                  modifiedAt: '2026-09-26T00:26:54+00:00'
                  sections: [barossa-region, more-news, rss-news, yorke-mid-north-region]
                  url: https://www.police.sa.gov.au/sa-police-news-assets/front-page-news/dashcam-sought-after-eudunda-road-rage
                  versionCount: 1
                  removedAt: null
                  attributes: { crimeStoppersRef: '251098', metaTopstory: 'no' }
                  extract: null
                  imageCount: 0
                  version: 1
                  body: Police are seeking witnesses and dashcam footage following a road rage incident at Eudunda...
                  paths:
                    - /sa-police-news-assets/barossa-region/dashcam-sought-after-eudunda-road-rage
                    - /sa-police-news-assets/front-page-news/dashcam-sought-after-eudunda-road-rage
                    - /sa-police-news-assets/yorke-mid-north-region/dashcam-sought-after-eudunda-road-rage
                  versions: [{ versionNo: 1, modifiedAt: '2026-09-26T00:26:54+00:00', title: Dashcam sought after Eudunda road rage }]
                  images: null
                meta: { generatedAt: '2026-09-27T03:00:00.000Z' }
                attribution: https://dataquoll.io/api/v1/attribution
        '400':
          description: The id is not a UUID, or version is not a positive integer
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '401':
          description: Missing or invalid API key
        '403':
          description: A free key asked for a publication older than its 7-day window
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '404':
          description: No publication with that id, or no such version
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '429':
          description: Rate limit exceeded
  /publishers:
    get:
      operationId: listPublishers
      summary: Publishers of official documents
      description: >
        Every publisher of official documents on the platform: its jurisdiction and type, licence
        and required attribution, the document kinds it publishes, whether its images are served,
        the publisher-specific fields its documents carry in attributes and what each means, and
        live counts. A new publisher appears here the day it is added.
      tags: [Publications]
      parameters:
        - name: state
          in: query
          description: Jurisdiction code, or a comma list.
          example: sa
          schema: { type: string }
        - name: type
          in: query
          description: Publisher type, or a comma list.
          example: police
          schema: { type: string }
        - name: source
          in: query
          description: Publisher id, or a comma list.
          example: sa-police
          schema: { type: string }
      responses:
        '200':
          description: The publishers
          content:
            application/json:
              schema:
                type: object
                properties:
                  publishers: { type: array, items: { $ref: '#/components/schemas/Publisher' } }
                  meta: { type: object, properties: { generatedAt: { type: string, format: date-time }, count: { type: integer } } }
                  attribution: { type: string, format: uri }
              example:
                publishers:
                  - id: sa-police
                    name: South Australia Police
                    agency: SAPOL
                    type: police
                    jurisdiction: sa
                    homeUrl: https://www.police.sa.gov.au
                    licence: CC BY 4.0
                    licenceUrl: https://creativecommons.org/licenses/by/4.0/
                    attribution: Contains SA Police media releases, (c) South Australia Police, used under CC BY 4.0.
                    commercialUse: true
                    kinds: [media_release]
                    serveImages: false
                    attributes:
                      crimeStoppersRef: The Crime Stoppers reference an appeal asks readers to quote, when the release carries one.
                    collection: { kind: scrape, cadence: "Front-page lists every 5 minutes, district, region and topic sections every 20." }
                    status: active
                    publications: { total: 498, live: 498, latestPublishedAt: '2026-09-27T02:01:36+00:00' }
                    lastCollectedAt: '2026-09-27T03:10:00+00:00'
                meta: { generatedAt: '2026-09-27T03:00:00.000Z', count: 1 }
                attribution: https://dataquoll.io/api/v1/attribution
        '400':
          description: Unknown state, type or source
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/ProblemDetails' }
        '401':
          description: Missing or invalid API key
        '429':
          description: Rate limit exceeded
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: >-
        API key in format: eak_live_... . Keys work from any origin by default.
        A key can optionally be restricted to specific browser origins in the
        dashboard; a request from a browser origin not on the allowlist is
        rejected with 403. Requests without an Origin header (server-side) are
        never blocked by this.

  schemas:
    Publication:
      type: object
      properties:
        id: { type: string, format: uuid }
        source: { type: string, description: Publisher id; see listPublishers. }
        publisher:
          type: object
          properties:
            name: { type: string }
            agency: { type: string }
            type: { type: string }
            jurisdiction: { type: string }
        kind: { type: string, enum: [media_release, news, alert, notice] }
        slug: { type: string }
        title: { type: [string, 'null'] }
        summary: { type: [string, 'null'] }
        publishedAt: { type: string, format: date-time }
        modifiedAt: { type: string, format: date-time, description: The publisher's last edit of the current version. }
        sections: { type: array, items: { type: string }, description: Every section of the publisher's site the document was listed under. }
        url: { type: [string, 'null'], format: uri, description: Where the publisher published it (the earliest path seen). Publishers delete documents, so it can stop resolving; the archive does not. }
        versionCount: { type: integer }
        removedAt: { type: [string, 'null'], format: date-time, description: When the publisher stopped publishing it; null while still published. }
        attributes: { type: object, additionalProperties: true, description: Publisher-specific fields. Their meanings are listed per publisher by listPublishers. }
        extract:
          type: [object, 'null']
          description: Place, time and category extracted from the text. Null until the extraction pass has run; never guessed.
          properties:
            eventAt: { type: [string, 'null'], format: date-time }
            place:
              type: object
              properties:
                suburb: { type: [string, 'null'] }
                street: { type: [string, 'null'] }
                lat: { type: [number, 'null'] }
                lng: { type: [number, 'null'] }
                precision: { type: [string, 'null'], enum: [exact, locality, postcode, lga, state, null] }
            category: { type: [string, 'null'] }
            attributes: { type: object, additionalProperties: true }
            method: { type: string }
            extractedAt: { type: string, format: date-time }
        imageCount: { type: integer, description: How many images the current version carries, served or not. }
    PublicationFull:
      allOf:
        - $ref: '#/components/schemas/Publication'
        - type: object
          properties:
            version: { type: integer }
            body: { type: [string, 'null'], description: The document text. The raw HTML is archived but never served. }
            paths: { type: array, items: { type: string } }
            versions:
              type: array
              items:
                type: object
                properties:
                  versionNo: { type: integer }
                  modifiedAt: { type: string, format: date-time }
                  title: { type: [string, 'null'] }
            images:
              type: [array, 'null']
              description: Null while the publisher's images are withheld, and for a document the publisher has taken down (removedAt set); otherwise one-hour signed URLs.
              items:
                type: object
                properties:
                  position: { type: integer }
                  url: { type: string, format: uri }
                  originalUrl: { type: string, format: uri }
                  sha256: { type: [string, 'null'] }
                  bytes: { type: [integer, 'null'] }
                  contentType: { type: [string, 'null'] }
    Publisher:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        agency: { type: string }
        type: { type: string, enum: [police, fire, ses, ambulance, health, transport, council, union, other] }
        jurisdiction: { type: string, enum: [nsw, vic, qld, sa, wa, tas, act, nt, national] }
        homeUrl: { type: string, format: uri }
        licence: { type: string }
        licenceUrl: { type: string, format: uri }
        attribution: { type: string, description: The attribution the licence requires wherever the documents are reused. }
        commercialUse: { type: boolean }
        kinds: { type: array, items: { type: string } }
        serveImages: { type: boolean }
        attributes: { type: object, additionalProperties: { type: string }, description: Publisher-specific field name to meaning. }
        collection: { type: object, properties: { kind: { type: string, enum: [scrape, feed] }, cadence: { type: string } } }
        status: { type: string, enum: [active, paused, retired] }
        publications:
          type: object
          properties:
            total: { type: integer }
            live: { type: integer }
            latestPublishedAt: { type: [string, 'null'], format: date-time }
        lastCollectedAt: { type: [string, 'null'], format: date-time }
    IncidentFeature:
      type: object
      properties:
        type:
          type: string
          const: Feature
        id:
          type: string
          example: nsw-rfs-1234567
        geometry:
          type: object
          properties:
            type:
              type: string
              enum: [Point, Polygon, MultiPolygon]
            coordinates:
              oneOf:
                - type: array
                  items:
                    type: number
                  description: "[longitude, latitude] for Point"
                - type: array
                  items:
                    type: array
                    items:
                      type: array
                      items:
                        type: number
                  description: "Ring arrays for Polygon"
                - type: array
                  items:
                    type: array
                    items:
                      type: array
                      items:
                        type: array
                        items:
                          type: number
                  description: "Polygon arrays for MultiPolygon (an incident whose area has several parts)"
        properties:
          type: object
          properties:
            source:
              type: object
              properties:
                state:
                  type: string
                agency:
                  type: string
                feedId:
                  type: string
            title:
              type: string
            eventType:
              type: string
              enum: [bushfire, burn_off, fire_ban, structure_fire, vehicle_fire, grass_fire, other_fire, hazmat, rescue, flood, storm, tree_down, cyclone, earthquake, extreme_heat, vehicle_accident, medical, alarm, other]
            eventCategory:
              type: string
              enum: [fire, weather, rescue, hazmat, medical, alarm, planned, earth, hazard, other]
              description: "Groups related event types. See /api/v1/schema for the full mapping."
            featureType:
              type: string
              enum: [incident, incident_area, warning_area, fire_ban_area, road_disruption]
              description: "Point incident or boundary polygon. Defaults to 'incident'; list endpoints return only incidents unless ?featureType= is supplied."
            status:
              type: string
              enum: [active, contained, controlled, safe, completed]
            warningLevel:
              type: string
              enum: [advice, watch_and_act, emergency_warning, none]
            severity:
              type: string
              enum: [Extreme, Severe, Moderate, Minor, Unknown]
            urgency:
              type: string
              enum: [Immediate, Expected, Future, Past, Unknown]
            certainty:
              type: string
              enum: [Observed, Likely, Possible, Unlikely, Unknown]
            location:
              type: object
              properties:
                address:
                  type: string
                suburb:
                  type: string
                state:
                  type: string
                lga:
                  type: string
                latitude:
                  type: number
                  description: "Centroid latitude (extracted from geometry)"
                longitude:
                  type: number
                  description: "Centroid longitude (extracted from geometry)"
                precision:
                  type: string
                  enum: [exact, locality, postcode, lga, state, unstated]
                  description: "How far the point can be trusted. 'exact' = a specific place: the agency's own coordinates, or a point derived from a street address or a map-book grid reference. 'locality' = the centre of the suburb or town the record names. 'postcode' or 'lga' = approximate centroid backfilled for historical archive rows that logged no point. 'state' = the record could not be placed and the point only stands for somewhere in the state; radius and bounding-box searches leave these records out. 'unstated' = a historical archive record whose source never said how it was located (the label /hazard-history/point gives the same record). Absent when no coordinates are available (geometry is null)."
                confidence:
                  type: string
                  enum: [grid_ref, geocoded, suburb, centroid]
                  description: "How the point was placed, on records where the source did not supply it directly: grid_ref = a map-book grid cell, geocoded = an address or agency point resolved to coordinates, suburb = the town or suburb centre, centroid = on a point, not placed (precision 'state'); on a historical archive area, the importer derived the record's location from the shape's centre, which says nothing about the shape. Set by the SA pager and WA DFES feeds, on any record that fell back to a state point, and on some archive fire and flood areas. Absent when the agency's own coordinates were used."
            boundary:
              type: [object, 'null']
              description: "The area the agency published for this incident (a fire perimeter, a warning area, a burn area), or null when it published none. geometry keeps the agency's point. Present on /incidents/{id}, and on list endpoints only with include_boundary=true. Coordinates are rounded to 6 decimal places, like area geometry (points are rounded to 4)."
              properties:
                type:
                  type: string
                  enum: [Polygon, MultiPolygon]
                coordinates:
                  type: array
                  description: "Ring arrays for Polygon; an array of those for MultiPolygon."
                  items: {}
            details:
              type: object
              description: >-
                Agency-specific incident metadata, passed through from the source feed. OPTIONAL and
                varies by feed AND by incident (it is not standardised across agencies, and some feeds
                carry none). Render whatever keys are present rather than expecting a fixed set. The
                properties below are the common ones; the full per-feed breakdown (every field, its
                type and meaning) is documented at
                https://dataquoll.io/guides/incident-details-fields
              properties:
                description:
                  type: string
                  description: "Source detail text; structure varies by feed (NSW RFS alert block, a place name, raw pager text, etc.)."
                resources:
                  type: integer
                  description: "Count of resources (crews/appliances/units) assigned or responding."
                aircraft:
                  type: integer
                  description: "Count of aircraft assigned (SA CFS)."
                region:
                  type: string
                  description: "Agency operational region, e.g. the SA CFS region number 1-6. Not a place name."
                fire_ban_district:
                  type: string
                  description: "Fire ban district, e.g. MOUNT LOFTY RANGES (SA CFS/MFS). Agency spelling, not normalised."
                decoded_units:
                  type: array
                  items:
                    type: string
                  description: "Decoded responding unit names (SA pager feeds)."
                alarm_level_description:
                  type: string
                  description: "Alarm/warning level label (SA pager feeds)."
                priority_description:
                  type: string
                  description: "Case priority label (SA Ambulance pager)."
                size:
                  type: string
                  description: "Incident size label, e.g. Small/Medium/Large (VIC OSOM)."
                cad_id:
                  type: string
                  description: "CAD dispatch identifier (e.g. WA DFES 6-digit number)"
                source_id:
                  type: string
                  description: "Upstream agency record ID"
                source_url:
                  type: string
                  format: uri
                  description: "Deep link to the incident on the source agency website"
              additionalProperties: true
            timestamps:
              type: object
              properties:
                reported:
                  type: string
                  format: date-time
                updated:
                  type: string
                  format: date-time
                fetched:
                  type: string
                  format: date-time
            archive:
              type: object
              description: "EM-DAT / historical enrichment. Present only on deep-archive snapshot results (meta.source = archive). Each field appears only when populated."
              properties:
                eventStartDate:
                  type: string
                  format: date
                eventEndDate:
                  type: string
                  format: date
                magnitude:
                  type: number
                magnitudeScale:
                  type: string
                impactDeaths:
                  type: integer
                impactAffected:
                  type: integer
                impactEconomic:
                  type: number
                glideNumber:
                  type: string
                  description: "GLIDE disaster identifier"

    IncidentCollection:
      type: object
      properties:
        type:
          type: string
          const: FeatureCollection
        features:
          type: array
          items:
            $ref: '#/components/schemas/IncidentFeature'
        meta:
          type: object
          properties:
            total_count:
              type: integer
            has_more:
              type: boolean
            next_cursor:
              type: string
            size_limited:
              type: boolean
              description: "/incidents/history and /incidents/snapshot. True when the answer ended at the ~4 MB size cap rather than at `limit`, so fewer records than asked for came back. On history `has_more` is then true (follow the cursor); on a snapshot `truncated` is true (narrow by state or event_type)."
            generatedAt:
              type: string
              format: date-time
        links:
          type: object
          properties:
            self:
              type: string
            next:
              type: string
        data_freshness:
          type: array
          description: >-
            How current each state's data is, one entry per state. Present on the
            GeoJSON and JSON responses; the csv, cap-au and cap-atom formats return
            before it is built.
          items:
            type: object
            properties:
              state:
                type: string
                example: sa
              lastPolled:
                type: string
                format: date-time
                nullable: true
                description: >-
                  The OLDEST successful poll across that state's feeds, so it bounds
                  how current the state's data can be. It is deliberately not the most
                  recent: taking the newest let one fast feed hide slower siblings and
                  made a down feed invisible behind a healthy one. Null when any feed in
                  the state has never succeeded.
              updateInterval:
                type: string
                description: "Shortest polling interval among that state's feeds, or `unknown`."
                example: 60s
              feedCount:
                type: integer
                description: "How many feeds this state has."
              feedsHealthy:
                type: integer
                description: >-
                  How many of them are currently healthy. Below feedCount means the
                  state is degraded and some sources are not reporting.
        attribution:
          type: string

    EventFeature:
      type: object
      properties:
        type:
          type: string
          const: Feature
        id:
          type: string
          format: uuid
          example: fb6e6418-3edb-40ac-83c3-1afb2550e887
        geometry:
          type: object
          description: "Boundary polygon (convex hull of cluster) or centroid point"
          properties:
            type:
              type: string
              enum: [Polygon, Point]
            coordinates:
              oneOf:
                - type: array
                  items:
                    type: array
                    items:
                      type: array
                      items:
                        type: number
                  description: "Ring arrays for Polygon boundary"
                - type: array
                  items:
                    type: number
                  description: "[longitude, latitude] for Point centroid"
        properties:
          type: object
          properties:
            title:
              type: string
              example: "Cherry Gardens bushfire"
            incidentCount:
              type: integer
              example: 42
            affectedSuburbs:
              type: array
              items:
                type: string
              example: ["Cherry Gardens", "Lobethal", "Woodside"]
            affectedStates:
              type: array
              items:
                type: string
              example: ["sa"]
            agencies:
              type: array
              items:
                type: string
              example: ["CFS", "MFS", "GA-HOTSPOTS"]
            eventTypes:
              type: array
              items:
                type: string
              example: ["bushfire"]
            maxSeverity:
              type: string
              enum: [Extreme, Severe, Moderate, Minor, Unknown]
            maxWarningLevel:
              type: string
              enum: [advice, watch_and_act, emergency_warning, none]
            estimatedHectares:
              type: number
              nullable: true
              example: 2400.5
            summary:
              type: string
              example: "42 incidents from CFS, MFS, GA-HOTSPOTS affecting Cherry Gardens, Lobethal, Woodside."
            firstDetectedAt:
              type: string
              format: date-time
              nullable: true
            lastUpdatedAt:
              type: string
              format: date-time
            isActive:
              type: boolean

    EventCollection:
      type: object
      properties:
        type:
          type: string
          const: FeatureCollection
        features:
          type: array
          items:
            $ref: '#/components/schemas/EventFeature'
        meta:
          type: object
          properties:
            totalEvents:
              type: integer
            generatedAt:
              type: string
              format: date-time
        attribution:
          type: string

    CalendarDay:
      type: object
      description: One row of an industry calendar. Null-valued fields are omitted from this description but always present in the JSON.
      properties:
        date: { type: string, format: date }
        weekday: { type: string, enum: [Mon, Tue, Wed, Thu, Fri, Sat, Sun] }
        category:
          type: string
          enum: [public_holiday, part_day_public_holiday, restricted_public_holiday, poster_holiday, rdo, industry_shutdown, annual_leave, industry_event, union_event, school_holiday, school_term]
          description: public_holiday is official and jurisdiction-wide. restricted_public_holiday is official but only for the area or sector in scope. poster_holiday is a day the source calendar colours as a holiday that is not one, kept for audit.
        subtype: { type: string, description: The calendar's own split where it has one (scheduled, flexible, 36_hour_week, 38_hour_week, industry, eba, contracting, award, construction); null otherwise }
        scope: { type: string, description: For restricted_public_holiday, the area or sector the day applies to; null otherwise }
        name: { type: string }
        poster_marking: { type: string, description: What the source calendar shows for the day; null where it has no cell }
        official_match: { type: boolean, description: For official holidays, whether the source calendar agrees; null where the question does not apply }
        part_day_from: { type: string, example: "19:00", description: Local start time of a part-day holiday; null otherwise }
        range_end: { type: string, format: date, description: Last day of the range this row belongs to (a shutdown, a school break, a term); null otherwise }
        note: { type: string }
    CalendarEdition:
      type: object
      properties:
        jurisdiction: { type: string }
        year: { type: integer }
        title: { type: string }
        publisher: { type: string }
        source_url: { type: string, format: uri }
        applies_to: { type: string }
        covers: { type: object, properties: { from: { type: string, format: date }, to: { type: string, format: date } } }
        timezone: { type: string }
    CalendarDiscrepancy:
      type: object
      properties:
        date: { type: string, format: date }
        issue: { type: string }
    CalendarVerdict:
      type: object
      properties:
        jurisdiction: { type: string }
        date: { type: string, format: date }
        weekday: { type: string, enum: [Mon, Tue, Wed, Thu, Fri, Sat, Sun] }
        timezone: { type: string }
        status:
          type: string
          enum: [public_holiday, weekend, industry_shutdown, rdo, part_day_public_holiday, annual_leave, industry_event, working]
        working_day: { type: boolean }
        applies_to: { type: string }
        entries: { type: array, items: { $ref: '#/components/schemas/CalendarDay' }, description: Every row on the date, including the ones that do not decide the verdict }
        restricted: { type: array, items: { $ref: '#/components/schemas/CalendarDay' }, description: Official holidays that apply only to the area or sector in their scope }
        edition: { $ref: '#/components/schemas/CalendarEdition' }
    ProblemDetails:
      type: object
      description: "RFC 9457 Problem Details"
      properties:
        type:
          type: string
          example: "https://emergencyapi.com/errors/rate-limit-exceeded"
        title:
          type: string
          example: Rate Limit Exceeded
        status:
          type: integer
          example: 429
        detail:
          type: string
        instance:
          type: string

    DeclarationRecord:
      type: object
      properties:
        agrn: { type: string, nullable: true }
        title: { type: string }
        instrumentType: { type: string, enum: [state_natural_disaster_declaration, drfa_activation_agrn, state_of_disaster_emergency_powers] }
        jurisdiction: { type: string }
        disasterTypes: { type: array, items: { type: string } }
        startDate: { type: string, format: date, nullable: true }
        endDate: { type: string, format: date, nullable: true }
        status: { type: string, enum: [active, expired, unknown] }
        source: { type: string, description: Display name of the highest-precedence reporting source }
        sources: { type: array, items: { type: string }, description: "Every government source reporting this AGRN+instrument for this LGA. Multiple entries = multiple sources corroborate it." }
        sourceUrl: { type: string }
        sourcePublishedAt: { type: string, format: date-time, nullable: true }
        scope: { type: string, enum: [part_not_stated], description: "Present only when the source published this record for a whole ABS catch-all area (for example Unincorporated SA) without saying which part it covers. It still counts toward the status, except that a declaration published this way reads uncertain, never declared." }
    PostcodeDeclarationRecord:
      allOf:
        - $ref: '#/components/schemas/DeclarationRecord'
        - type: object
          properties:
            currencyConfidence: { type: string, enum: [strong, moderate, weak], nullable: true, description: "TCDE currency-confidence band for an in-force record (null when not in force). Transparent evidence, never a numeric 'chance you can bill' score." }
            basis: { type: string, description: "Short human-readable basis for this record's verdict (the evidence behind its status)." }
    MatchedLga:
      type: object
      properties:
        lgaCode: { type: string }
        lgaName: { type: string }
        area:
          type: object
          description: "Present only when the location is inside an ABS catch-all code whose parts sources name (NSW and VIC): the part it is in. lgaCode stays the ABS code, and records the sources place in a different part are left out."
          properties:
            code: { type: string, example: '19399-LHI' }
            name: { type: string, example: 'Lord Howe Island' }
        populationShare: { type: number, nullable: true, description: "Share of the postcode's population in this LGA (ABS correspondence). Always the postcode-level share, even when the response is narrowed by suburb (never renormalised). null for point lookups, where population share is not a meaningful concept." }
        declarationStatus: { type: string, enum: [declared, uncertain, activation_only, not_declared] }
        declarations: { type: array, items: { $ref: '#/components/schemas/PostcodeDeclarationRecord' } }
    SuburbNarrowing:
      type: object
      description: Present on postcode responses only when the suburb parameter was supplied.
      properties:
        requested: { type: string, description: The trimmed caller input (up to 100 characters) }
        applied: { type: boolean }
        method: { type: string, enum: [spatial_derivation, abs_sal_lga_correspondence], description: "How suburb-to-LGA membership was derived. spatial_derivation = area-weighted intersection of ABS SAL 2021 and LGA 2022 boundary polygons (the current source; ABS publishes no SAL 2021 to LGA 2022 correspondence); abs_sal_lga_correspondence = a published ABS population-weighted correspondence, if one is adopted later." }
        matchedSuburbs:
          type: array
          items: { type: object, properties: { salCode: { type: string }, salName: { type: string } } }
          description: The ABS SAL suburb(s) the input matched within this postcode. More than one entry means the bare name denotes multiple suburbs (their LGA sets are unioned).
        reason: { type: string, enum: [suburb_not_found_in_postcode, suburb_lgas_outside_postcode, unknown_postcode], description: Present only when applied is false }
        knownSuburbs: { type: array, items: { type: string }, description: Present only when applied is false and the postcode is known — the postcode's suburb names, for typo recovery }
    PostcodeDeclarationResult:
      type: object
      properties:
        postcode: { type: string }
        matchStatus: { type: string, enum: [declared, partial, not_declared, uncertain, activation_only, unknown_postcode] }
        matchStatusReason: { type: string, description: Present when matchStatus is uncertain or activation_only }
        matchedLgas: { type: array, items: { $ref: '#/components/schemas/MatchedLga' } }
        narrowing: { $ref: '#/components/schemas/SuburbNarrowing' }
        asAt: { type: string, format: date, description: Date the status was evaluated against; the requested as_at value or today (AEST) when omitted }
        dataLastUpdated: { type: string, format: date-time, nullable: true }
        stale: { type: boolean }
        disclaimer: { type: string }
        attribution: { type: string }

    HazardHistoryRequest:
      type: object
      required: [lat, lng]
      properties:
        lat:
          type: number
          description: Latitude in decimal degrees, -90 to 90. Never logged or echoed.
          example: -28.8078061
        lng:
          type: number
          description: Longitude in decimal degrees, -180 to 180. Never logged or echoed.
          example: 153.2766097
        radiusKm:
          type: number
          description: Search radius for the observed lane, greater than 0 and no more than 5. Defaults to 1.
          default: 1
          example: 1
        records:
          type: integer
          description: Maximum observed records returned in the list. Aggregates always cover everything in radius. 1 to 100, defaults to 25.
          default: 25
          example: 25
    HazardHistoryResult:
      type: object
      properties:
        location:
          type: object
          description: The ABS local government area containing the point. All fields are null when the point lies outside every Australian LGA.
          properties:
            lgaCode: { type: string, nullable: true, example: '14850' }
            lgaName: { type: string, nullable: true, example: 'Lismore' }
            stateCode: { type: string, nullable: true, example: 'NSW' }
            area:
              type: object
              description: "Present only inside an ABS catch-all code whose parts sources name (NSW and VIC): the part the point is in. Declarations placed in another part are left out."
              properties:
                code: { type: string, example: '19399-LHI' }
                name: { type: string, example: 'Lord Howe Island' }
        radiusKm: { type: number, example: 1 }
        declared:
          type: object
          description: Disaster declarations covering the local government area, counted once per AGRN.
          properties:
            distinctCount:
              type: integer
              description: "Number of distinct AGRNs, not a row count. One event recorded by three authorities counts once."
              example: 27
            earliest: { type: string, format: date, nullable: true }
            latest: { type: string, format: date, nullable: true }
            records:
              type: array
              items:
                type: object
                properties:
                  agrn: { type: string, example: '1012' }
                  title: { type: string }
                  startDate: { type: string, format: date, nullable: true }
                  endDate: { type: string, format: date, nullable: true }
                  endDatePublished:
                    type: boolean
                    description: Whether the authority published an end date. False with a null endDate means no end has been published, which does NOT mean the declaration is in force.
                  jurisdiction: { type: string, nullable: true }
                  sources:
                    type: array
                    description: Every source that recorded this AGRN, so a single row is traceable to the several authorities behind it.
                    items: { type: string }
                  scope: { type: string, enum: [part_not_stated], description: "Present only when the declaration was published for a whole ABS catch-all area without naming the part it covers." }
            sourceUrls:
              type: object
              description: Official source URL per AGRN, where known. Omitted when none are.
              additionalProperties: { type: string }
        observed:
          type: object
          description: What official sources recorded within the radius, from both lanes. byType, byPrecision, earliest, latest and records cover the historical archive and the live-era incident history together; archiveTotal and liveTotal split the count by lane.
          properties:
            archiveTotal: { type: integer, example: 7, description: Records from the historical archive. }
            liveTotal: { type: integer, example: 19, description: Records from the live-era incident history (2026-04-08 onward). }
            earliest: { type: string, format: date, nullable: true }
            latest: { type: string, format: date, nullable: true }
            undatedCount:
              type: integer
              description: Records whose source never supplied a date. Present in the counts, flagged dated=false, never given an invented date.
            byType:
              type: array
              items:
                type: object
                properties:
                  eventType: { type: string }
                  eventCategory: { type: string, nullable: true }
                  count: { type: integer }
                  earliest: { type: string, format: date, nullable: true }
                  latest: { type: string, format: date, nullable: true }
            byPrecision:
              type: array
              description: "How the matched records were located. Postcode-precision records sit at a postcode centroid, not at the event."
              items:
                type: object
                properties:
                  precision: { type: string, example: 'postcode' }
                  geometryType: { type: string, example: 'MultiPolygon' }
                  count: { type: integer }
            records:
              type: array
              items:
                type: object
                properties:
                  id: { type: string }
                  title: { type: string, nullable: true }
                  eventType: { type: string }
                  eventCategory: { type: string, nullable: true }
                  onDate: { type: string, format: date, nullable: true }
                  dated: { type: boolean }
                  datePrecision: { type: string, enum: [month, year], description: "Present only when the source stated less than a day: onDate is then the first day of that month or year, not a stated day." }
                  precision: { type: string }
                  geometryType: { type: string }
                  sourceAgency: { type: string, nullable: true }
                  sourceFeedId: { type: string, nullable: true }
                  lane: { type: string, enum: [archive, live], description: "archive = the historical archive; live = the live-era incident history (2026-04-08 onward)." }
        gauge:
          type: object
          nullable: true
          description: Nearest active river gauge, for context. Not a flood assessment.
          properties:
            name: { type: string }
            jurisdiction: { type: string, nullable: true }
            distanceM: { type: integer }
        coverage:
          type: array
          description: The measured limits of this answer. Rides on every response.
          items: { type: string }
        disclaimer: { type: string }
    PointLookupRequest:
      type: object
      required: [lat, lng]
      properties:
        lat: { type: number, minimum: -90, maximum: 90 }
        lng: { type: number, minimum: -180, maximum: 180 }
        as_at: { type: string, format: date, description: Point-in-time check (YYYY-MM-DD). Defaults to today. }
        include_expired: { type: boolean, default: false, description: Include expired records in the per-LGA lists. Never changes matchStatus. }
        instrument_type: { type: string, enum: [state_natural_disaster_declaration, drfa_activation_agrn, state_of_disaster_emergency_powers], description: Trim the returned record lists to one instrument type. Never changes matchStatus. }
    PointDeclarationResult:
      type: object
      properties:
        matchStatus: { type: string, enum: [declared, partial, not_declared, uncertain, activation_only, unknown_location] }
        matchStatusReason: { type: string, description: Present when matchStatus is uncertain or activation_only }
        matchedLgas:
          type: array
          items: { $ref: '#/components/schemas/MatchedLga' }
          description: "Normally one area. Two areas only when the point lies exactly on a shared boundary (onBoundary=true); partial is then possible and means the covering areas have different statuses."
        onBoundary: { type: boolean, description: Present (true) only when the point lies on a shared LGA boundary and more than one area is returned. }
        boundaryEdition: { type: string, example: LGA_2022, description: "ABS boundary edition the point was resolved against. Informative, not an enum — it changes with ASGS edition refreshes. LGA matching is at ABS code level; point-in-polygon cannot be more precise than the code." }
        asAt: { type: string, format: date, description: Date the status was evaluated against; the requested as_at value or today (AEST) when omitted }
        dataLastUpdated: { type: string, format: date-time, nullable: true }
        stale: { type: boolean }
        disclaimer: { type: string }
        attribution: { type: string }
    DeclarationListItem:
      allOf:
        - $ref: '#/components/schemas/DeclarationRecord'
        - type: object
          properties:
            lgaCount: { type: integer }
    DeclarationList:
      type: object
      properties:
        declarations: { type: array, items: { $ref: '#/components/schemas/DeclarationListItem' } }
        dataLastUpdated: { type: string, format: date-time, nullable: true }
        stale: { type: boolean }
        disclaimer: { type: string }
        links: { type: object, properties: { next: { type: string, nullable: true } } }
        attribution: { type: string }
    DeclarationDetail:
      type: object
      properties:
        agrn: { type: string }
        title: { type: string }
        instrumentType: { type: string }
        jurisdiction: { type: string }
        disasterTypes: { type: array, items: { type: string } }
        startDate: { type: string, format: date, nullable: true }
        endDate: { type: string, format: date, nullable: true }
        status: { type: string, enum: [active, expired, unknown] }
        source: { type: string }
        sourcePublishedAt: { type: string, format: date-time, nullable: true }
        sourceUrl: { type: string }
        contributingSources: { type: array, items: { type: object, properties: { source: { type: string }, instrumentType: { type: string }, sourceUrl: { type: string } } } }
        affectedLgas:
          type: array
          items:
            type: object
            properties:
              lgaCode: { type: string, nullable: true }
              lgaName: { type: string }
              area:
                type: object
                description: "Present only when a source named a part of an ABS catch-all code (for example Lord Howe Island within Unincorporated NSW). lgaCode is then the catch-all's ABS code."
                properties:
                  code: { type: string, example: '19399-LHI' }
                  name: { type: string, example: 'Lord Howe Island' }
        dataLastUpdated: { type: string, format: date-time, nullable: true }
        stale: { type: boolean }
        disclaimer: { type: string }
        attribution: { type: string }

    Gauge:
      type: object
      description: "A gauge station with its latest reading. Water-level readings are metres relative to the gauge's local datum, not sea level; rainfall readings are incremental millimetres. floodClass, thresholds and datum are present only on water-level gauges."
      properties:
        id: { type: string, example: hydstra-nsw-410001 }
        name: { type: string }
        river: { type: string, nullable: true }
        state: { type: string, example: NSW }
        agency: { type: string, nullable: true }
        source: { type: string }
        variable: { type: string, enum: [water_level, rainfall], description: "The measured variable. Rainfall gauges carry no floodClass, thresholds or datum." }
        awrcStationNo: { type: string, nullable: true }
        coordinates: { type: object, properties: { lat: { type: number }, lng: { type: number } } }
        lga: { type: object, nullable: true, properties: { code: { type: string }, name: { type: string, nullable: true } } }
        latest: { type: object, nullable: true, properties: { value: { type: number }, observedAt: { type: string, format: date-time }, quality: { type: string, nullable: true } } }
        readingStatus: { type: string, enum: [live, stale] }
        unit: { type: string, description: "Present on rainfall gauges (always 'mm'). Water-level gauges carry their unit inside thresholds.unit.", example: mm }
        floodClass: { type: string, enum: [below_minor, minor, moderate, major, unclassified], description: "Water-level gauges only. Derived at read time from the latest reading vs the published BoM thresholds; never stored as judgement." }
        thresholds: { type: object, description: "Water-level gauges only.", properties: { minor: { type: number, nullable: true }, moderate: { type: number, nullable: true }, major: { type: number, nullable: true }, unit: { type: string }, source: { type: string, nullable: true }, version: { type: string, nullable: true }, fetchedAt: { type: string, format: date-time, nullable: true } } }
        datum: { type: object, nullable: true, description: "Water-level gauges only.", properties: { gaugeZeroM: { type: number, nullable: true }, datum: { type: string, nullable: true } } }

    GaugeReading:
      type: object
      description: "A raw reading (observedAt/value/quality) or an aggregated bucket (bucket/min/max/mean/nObs), depending on the interval."
      properties:
        observedAt: { type: string, format: date-time }
        value: { type: number }
        quality: { type: string, nullable: true }
        bucket: { type: string }
        min: { type: number }
        max: { type: number }
        mean: { type: number }
        nObs: { type: integer }

    GaugeCollection:
      type: object
      properties:
        gauges: { type: array, items: { $ref: '#/components/schemas/Gauge' } }
        meta: { type: object, properties: { count: { type: integer }, hasMore: { type: boolean }, nextCursor: { type: string }, generatedAt: { type: string, format: date-time } } }
        disclaimer: { type: string }
        attribution: { type: string }

security:
  - BearerAuth: []

tags:
  - name: Incidents
    description: Emergency incident data
  - name: Declarations
    description: "Australian disaster declaration and DRFA funding-activation status by postcode and LGA. Reports declared-area status; the practitioner makes the billing/eligibility decision."
  - name: Events
    description: "Spatially clustered incident intelligence. Groups related incidents into events with boundary polygons, affected areas, and structured summaries."
  - name: Gauges
    description: "River gauge observations against the Bureau of Meteorology flood classifications. Status reporting, not flood prediction, and not a substitute for official warnings."
  - name: Calendars
    description: "Industry calendars for NSW, VIC, QLD, WA, SA, TAS, ACT and NT: RDOs, public holidays, school holidays and shutdowns, verified against the official lists, with every source disagreement recorded."
  - name: Publications
    description: "Official documents from the publisher registry: media releases, news, alerts and notices, versioned and kept after the publisher deletes them. South Australia Police first."
  - name: Meta
    description: API metadata and health
