{
  "openapi": "3.1.0",
  "info": {
    "title": "Truely API",
    "version": "1.0.0",
    "summary": "UK postcode public-records intelligence, delivered as JSON.",
    "description": "The Truely API exposes the same UK government public-record figures that power the truely.uk website — council tax, deprivation, air quality, broadband, flood risk, greenspace and more — keyed by UK postcode. Every figure is sourced from an official UK publisher and licensed under the Open Government Licence v3.0.\n\nThe API is stateless, JSON-only, and authenticated by Bearer token. Errors follow RFC 7807 (application/problem+json). Timestamps are ISO 8601 UTC. Every response carries an `X-Request-ID` for support correlation. Rate limiting is per-key on a rolling 24-hour window.\n\nFull human-readable documentation: https://truely.uk/api",
    "termsOfService": "https://truely.uk/terms",
    "contact": {
      "name": "Truely API support",
      "email": "hello@truely.uk",
      "url": "https://truely.uk/api"
    },
    "license": {
      "name": "Data: Open Government Licence v3.0",
      "url": "https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/"
    }
  },
  "servers": [
    {
      "url": "https://truely.uk/api/v1",
      "description": "Production"
    }
  ],
  "security": [{ "bearerAuth": [] }],
  "tags": [
    { "name": "Postcode", "description": "Per-postcode public-records reports." },
    { "name": "Service",  "description": "Service-level health and metadata." }
  ],
  "paths": {
    "/postcode/{pc}": {
      "get": {
        "tags": ["Postcode"],
        "summary": "Full public-records report for a UK postcode",
        "description": "Returns the assembled public-records report for the given postcode. The postcode is normalised server-side, so `SW181PB`, `sw18 1pb` and `SW18%201PB` all resolve to the same record. Successful responses are CDN-cached for one hour with stale-while-revalidate; auth and rate-limit checks always run before the cache is consulted.",
        "operationId": "getPostcodeReport",
        "parameters": [
          {
            "name": "pc",
            "in": "path",
            "required": true,
            "description": "UK postcode. URL-encode the inner space (`SW18%201PB`) or omit it entirely (`SW181PB`).",
            "schema": { "type": "string", "example": "SW18 1PB" }
          }
        ],
        "responses": {
          "200": {
            "description": "Postcode found.",
            "headers": {
              "X-Request-ID":           { "$ref": "#/components/headers/XRequestId" },
              "X-RateLimit-Limit":      { "$ref": "#/components/headers/RateLimitLimit" },
              "X-RateLimit-Remaining":  { "$ref": "#/components/headers/RateLimitRemaining" },
              "X-RateLimit-Reset":      { "$ref": "#/components/headers/RateLimitReset" }
            },
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PostcodeReport" },
                "example": {
                  "postcode": "SW18 1PB",
                  "borough": "Wandsworth",
                  "ward": "Thamesfield",
                  "parliamentary_constituency": "Putney",
                  "geo": { "lat": 51.45, "lng": -0.19 },
                  "council_tax": {
                    "band_d_gbp_per_year": 1028,
                    "most_common_band": "C",
                    "measurement_year": 2025,
                    "source": "MHCLG / VOA"
                  },
                  "deprivation": { "imd_decile": 7, "imd_release": "2025", "source": "MHCLG English Indices of Deprivation" },
                  "air_quality": { "pm25_ug_per_m3": 7.0, "no2_ug_per_m3": 20.6, "source": "DEFRA Pollution Climate Mapping" },
                  "broadband":   { "pct_gigabit_capable": 100, "source": "Ofcom Connected Nations" },
                  "flood_risk":  { "rivers_and_sea": "very_low", "surface_water": "low", "source": "Environment Agency NaFRA2" },
                  "greenspace":  { "nearest_park_distance_m": 31, "source": "Ordnance Survey Open Greenspace" },
                  "meta": {
                    "request_id": "3f0e2c8b-7c4d-4f3b-9c5b-1d2e0d3a91aa",
                    "fetched_at": "2026-05-19T12:34:56Z",
                    "license":    "Open Government Licence v3.0",
                    "docs":       "https://truely.uk/api"
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/MalformedInput" },
          "401": { "$ref": "#/components/responses/Unauthorised" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "500": { "$ref": "#/components/responses/UpstreamError" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "tk_live_XXXX or tk_test_XXXX",
        "description": "API key minted at https://truely.uk/account#developer. Sent in the `Authorization: Bearer <key>` header on every request. Keys are hashed (SHA-256) at rest — Truely never stores plaintext."
      }
    },
    "headers": {
      "XRequestId": {
        "description": "Unique identifier for this request. Quote it in any support enquiry.",
        "schema": { "type": "string", "example": "3f0e2c8b-7c4d-4f3b-9c5b-1d2e0d3a91aa" }
      },
      "RateLimitLimit": {
        "description": "Daily request ceiling for the calling key.",
        "schema": { "type": "integer", "example": 25 }
      },
      "RateLimitRemaining": {
        "description": "Requests left in the current 24-hour window after this one.",
        "schema": { "type": "integer", "example": 24 }
      },
      "RateLimitReset": {
        "description": "Unix seconds when the window will be fully clear (worst case).",
        "schema": { "type": "integer", "example": 1747700400 }
      }
    },
    "schemas": {
      "PostcodeReport": {
        "type": "object",
        "description": "Full public-records report for a single UK postcode. Any block may be `null` if no data is available for that postcode on that dataset — treat every field as optional in your client.",
        "required": ["postcode", "meta"],
        "properties": {
          "postcode": {
            "type": "string",
            "description": "Normalised postcode in space-separated upper-case form, e.g. `SW18 1PB`.",
            "example": "SW18 1PB"
          },
          "borough": {
            "type": ["string", "null"],
            "description": "Local authority district name. Source: ONS postcodes.io.",
            "example": "Wandsworth"
          },
          "ward": {
            "type": ["string", "null"],
            "description": "Electoral ward name. Source: ONS.",
            "example": "Thamesfield"
          },
          "parliamentary_constituency": {
            "type": ["string", "null"],
            "description": "UK Parliament constituency.",
            "example": "Putney"
          },
          "geo": {
            "type": ["object", "null"],
            "description": "WGS84 decimal-degrees coordinates.",
            "properties": {
              "lat": { "type": "number", "format": "double", "example": 51.45 },
              "lng": { "type": "number", "format": "double", "example": -0.19 }
            }
          },
          "council_tax": {
            "type": ["object", "null"],
            "description": "Local-authority council tax.",
            "properties": {
              "band_d_gbp_per_year": { "type": "integer", "description": "Annual Band D rate in GBP for the LA the postcode sits in.", "example": 1028 },
              "most_common_band":    { "type": "string",  "description": "Modal council tax band across properties in the LA.", "example": "C" },
              "measurement_year":    { "type": "integer", "description": "Financial year the figure applies to.", "example": 2025 },
              "source":              { "type": "string", "example": "MHCLG / VOA" }
            }
          },
          "deprivation": {
            "type": ["object", "null"],
            "description": "MHCLG English Indices of Multiple Deprivation, at LSOA level.",
            "properties": {
              "imd_decile":  { "type": "integer", "minimum": 1, "maximum": 10, "description": "10 = least deprived, 1 = most deprived.", "example": 7 },
              "imd_release": { "type": "string",  "description": "Release year of the IMD data (`2025` or `2019`).", "example": "2025" },
              "source":      { "type": "string", "example": "MHCLG English Indices of Deprivation" }
            }
          },
          "air_quality": {
            "type": ["object", "null"],
            "description": "Annual mean pollutant concentrations at postcode level.",
            "properties": {
              "pm25_ug_per_m3": { "type": "number", "format": "double", "description": "PM2.5 annual mean, µg/m³.", "example": 7.0 },
              "no2_ug_per_m3":  { "type": "number", "format": "double", "description": "NO₂ annual mean, µg/m³.", "example": 20.6 },
              "source":         { "type": "string", "example": "DEFRA Pollution Climate Mapping" }
            }
          },
          "broadband": {
            "type": ["object", "null"],
            "description": "Ofcom Connected Nations availability snapshot.",
            "properties": {
              "pct_gigabit_capable": { "type": "number", "format": "double", "minimum": 0, "maximum": 100, "description": "Percentage of premises with gigabit-capable broadband available.", "example": 100 },
              "source":              { "type": "string", "example": "Ofcom Connected Nations" }
            }
          },
          "flood_risk": {
            "type": ["object", "null"],
            "description": "Environment Agency worst-band ratings at postcode level.",
            "properties": {
              "rivers_and_sea": { "type": "string", "enum": ["very_low", "low", "medium", "high"], "example": "very_low" },
              "surface_water":  { "type": "string", "enum": ["very_low", "low", "medium", "high"], "example": "low" },
              "source":         { "type": "string", "example": "Environment Agency NaFRA2" }
            }
          },
          "greenspace": {
            "type": ["object", "null"],
            "description": "Nearest publicly accessible park from OS Open Greenspace.",
            "properties": {
              "nearest_park_distance_m": { "type": "integer", "minimum": 0, "description": "Straight-line distance to the nearest accessible greenspace, in metres.", "example": 31 },
              "source":                  { "type": "string", "example": "Ordnance Survey Open Greenspace" }
            }
          },
          "meta": {
            "type": "object",
            "description": "Per-response metadata.",
            "required": ["request_id", "fetched_at", "license", "docs"],
            "properties": {
              "request_id": { "type": "string", "description": "Matches the X-Request-ID header.", "example": "3f0e2c8b-..." },
              "fetched_at": { "type": "string", "format": "date-time", "description": "ISO 8601 UTC time the response was assembled.", "example": "2026-05-19T12:34:56Z" },
              "license":    { "type": "string", "example": "Open Government Licence v3.0" },
              "docs":       { "type": "string", "format": "uri", "example": "https://truely.uk/api" }
            }
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 7807 problem+json error body.",
        "required": ["type", "title", "status", "request_id"],
        "properties": {
          "type":       { "type": "string", "format": "uri", "description": "Stable identifier for the error class.", "example": "https://truely.uk/api/errors/429" },
          "title":      { "type": "string", "description": "Short human-readable summary.", "example": "Rate limit exceeded" },
          "status":     { "type": "integer", "description": "HTTP status code, duplicated in the body for convenience.", "example": 429 },
          "detail":     { "type": "string", "description": "Long human-readable explanation, often actionable.", "example": "You have used 25 of your 25 requests/day quota for the free tier." },
          "request_id": { "type": "string", "example": "3f0e2c8b-..." }
        },
        "additionalProperties": true
      }
    },
    "responses": {
      "MalformedInput": {
        "description": "Input failed validation before any work was done.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Unauthorised": {
        "description": "Missing, malformed, or unknown API key.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "Forbidden": {
        "description": "Non-HTTPS request, or CORS origin not in the calling key's allow-list.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "NotFound": {
        "description": "Resource (postcode, borough, etc.) is well-formed but has no record.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "RateLimited": {
        "description": "Per-key 24-hour quota reached. Body includes `limit`, `used`, `tier`, `window`, `upgrade`.",
        "headers": {
          "X-RateLimit-Limit":     { "$ref": "#/components/headers/RateLimitLimit" },
          "X-RateLimit-Remaining": { "$ref": "#/components/headers/RateLimitRemaining" },
          "X-RateLimit-Reset":     { "$ref": "#/components/headers/RateLimitReset" }
        },
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      },
      "UpstreamError": {
        "description": "Transient failure reading from an upstream data source. Retry with exponential back-off.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      }
    }
  }
}
