{
  "openapi": "3.1.0",
  "info": {
    "title": "Quarrion Public API",
    "version": "0.1.0",
    "summary": "Read-only access to Quarrion’s public pricing, FAQ and salary data.",
    "description": "EXPERIMENTAL. These endpoints are published so agents and integrators can read\nthe same public data the website shows, and for no other reason. They may change\nshape, move or disappear without notice, and carry no availability commitment.\nBuild against them at your own risk; if you depend on one, tell us at\nsupport@quarrion.ai and we will at least know you exist.\n\nEvery endpoint is unauthenticated and returns the envelope\n`{ \"success\": boolean, \"data\": <payload>, \"error\": string | null }`.\nField casing follows whatever produces the payload: the plan and FAQ endpoints\nreturn database column names (snake_case); the salary endpoints return\ncamelCase values built by the aggregation service.\n\nAll but /api/health send RFC 9331 `RateLimit` and `RateLimit-Policy` headers.\nAnonymous callers are keyed by IP and get 30 requests per\n60 seconds.\n\nHuman-readable notes: https://quarrion.ai/developers",
    "contact": {
      "name": "Quarrion",
      "url": "https://quarrion.ai/developers",
      "email": "support@quarrion.ai"
    },
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "https://quarrion.ai",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Pricing",
      "description": "Plans, prices and per-metric limits."
    },
    {
      "name": "Content",
      "description": "Published editorial content."
    },
    {
      "name": "Salary",
      "description": "Cross-tenant salary research data."
    },
    {
      "name": "Operations",
      "description": "Service health."
    }
  ],
  "paths": {
    "/api/subscription/plans": {
      "get": {
        "operationId": "listSubscriptionPlans",
        "summary": "List the active subscription plans",
        "description": "The live plan, price and per-metric limit table behind /pricing. Prices are in GBP. Owner-only rows are excluded. Served from a one-hour cache.",
        "tags": [
          "Pricing"
        ],
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "True on a 2xx response."
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SubscriptionPlan"
                      }
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on success."
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this IP. Retry-After gives the seconds to wait.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "An unexpected fault. The message never carries internal detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/faq": {
      "get": {
        "operationId": "listFaqEntries",
        "summary": "List the published FAQ entries",
        "description": "Every published entry, ordered by `sort_order` then newest first. Both filters are optional and combine; `search` is a case-insensitive substring match across the question and the answer.",
        "tags": [
          "Content"
        ],
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Exact-match filter on the entry category.",
            "schema": {
              "type": "string"
            },
            "example": "product"
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring match on question or answer.",
            "schema": {
              "type": "string"
            },
            "example": "scoring"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "True on a 2xx response."
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/FaqEntry"
                      }
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on success."
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this IP. Retry-After gives the seconds to wait.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "An unexpected fault. The message never carries internal detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/salary/advertised": {
      "get": {
        "operationId": "listAdvertisedSalaries",
        "summary": "Advertised-salary medians computed from real job postings",
        "description": "The p25 / median / p75 of the annual pay employers STATED in public job advertisements, per sector or job title per market, over a rolling 90-day window. Every cell carries its own sample size, its date range, the nightly recompute run that produced it and the URL of its human-readable page. Advertised pay, not paid pay; an advertisement quoting no salary is excluded rather than estimated, and a cell is withdrawn rather than frozen when it falls below 30 advertisements. Covers every sector, not only technology. Both filters are optional and combine; `location` is an alias of `city` and `role` of `sector`, and either spelling — URL slug or raw value — resolves. Call /api/salary/advertised/locations first to learn the valid values.",
        "tags": [
          "Salary"
        ],
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "city",
            "in": "query",
            "required": false,
            "description": "Market: the URL slug (`london`, `uk`, `remote-uk`) or the raw location (`London`, `GB`, `remote-GB`). `location` is accepted as an alias.",
            "schema": {
              "type": "string"
            },
            "example": "london"
          },
          {
            "name": "sector",
            "in": "query",
            "required": false,
            "description": "Sector or job title, as the URL slug (`hospitality-retail`, `chef-de-partie`) or its human form. `role` is accepted as an alias.",
            "schema": {
              "type": "string"
            },
            "example": "chef-de-partie"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "True on a 2xx response."
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdvertisedSalaryCell"
                      }
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on success."
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A filter is empty or longer than 120 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this IP. Retry-After gives the seconds to wait.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "An unexpected fault. The message never carries internal detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/salary/advertised/locations": {
      "get": {
        "operationId": "listAdvertisedSalaryLocations",
        "summary": "Markets with advertised-salary data, and the filters they accept",
        "description": "One entry per market, with how many cells it publishes, how many advertisements sit behind them, and the exact `city` / `sector` slugs to pass back to /api/salary/advertised. London leads because the corpus does; the rest follow by weight of evidence.",
        "tags": [
          "Salary"
        ],
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "True on a 2xx response."
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AdvertisedSalaryMarket"
                      }
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on success."
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this IP. Retry-After gives the seconds to wait.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "An unexpected fault. The message never carries internal detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/salary/globe": {
      "get": {
        "operationId": "getSalaryByCountry",
        "summary": "Modelled salary estimates for a role, by country",
        "description": "Country-level MODELLED ESTIMATES for a role, normalised to USD and inflation-adjusted. These are curated estimates calibrated against BLS / ONS / Eurostat reference data — not survey data, not observed pay, and not sourced from those agencies; every row carries a confidence score and its citations, and in production today every row resolves to `provenance.kind = \"estimate\"`. For advertised medians computed from real job postings, use /api/salary/advertised. Passing `country` switches the response to that country’s cities — the same payload as /api/salary/globe/cities. Anonymous callers read cross-tenant research data only; no user data is ever returned.",
        "tags": [
          "Salary"
        ],
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "role",
            "in": "query",
            "required": true,
            "description": "Role title. Matched against a synonym list, so close variants resolve.",
            "schema": {
              "type": "string"
            },
            "example": "Software Engineer"
          },
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "ISO 3166-1 alpha-2. When present the response is an array of cities instead.",
            "schema": {
              "type": "string"
            },
            "example": "GB"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "True on a 2xx response."
                    },
                    "data": {
                      "oneOf": [
                        {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CountrySalary"
                          }
                        },
                        {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/CitySalary"
                          }
                        }
                      ]
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on success."
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`role` is missing, empty or longer than 200 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this IP. Retry-After gives the seconds to wait.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "An unexpected fault. The message never carries internal detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/salary/globe/cities": {
      "get": {
        "operationId": "getSalaryByCity",
        "summary": "Modelled salary estimates for a role, by city within one country",
        "description": "City-level drill-down of the same modelled estimates as /api/salary/globe — not observed pay. `median`, `p25` and `p75` are null for a city that is plotted but has no salary data for this role.",
        "tags": [
          "Salary"
        ],
        "security": [
          {}
        ],
        "parameters": [
          {
            "name": "role",
            "in": "query",
            "required": true,
            "description": "Role title. Matched against a synonym list, so close variants resolve.",
            "schema": {
              "type": "string"
            },
            "example": "Software Engineer"
          },
          {
            "name": "country",
            "in": "query",
            "required": true,
            "description": "ISO 3166-1 alpha-2 (alpha-3 is also accepted).",
            "schema": {
              "type": "string"
            },
            "example": "GB"
          }
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "True on a 2xx response."
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CitySalary"
                      }
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on success."
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`role` or `country` is missing or malformed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this IP. Retry-After gives the seconds to wait.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "An unexpected fault. The message never carries internal detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/salary/globe/roles": {
      "get": {
        "operationId": "listResearchedRoles",
        "summary": "List the role titles that have salary data",
        "description": "Distinct role titles with researched salary data, so a client can suggest roles that will actually return results. Low-confidence free-text titles are filtered out.",
        "tags": [
          "Salary"
        ],
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "True on a 2xx response."
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ResearchedRole"
                      }
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on success."
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded for this IP. Retry-After gives the seconds to wait.",
            "headers": {
              "RateLimit": {
                "description": "RFC 9331 structured field naming the budget in force for this request. A live per-IP counter: r is what remains of 30 requests per 60s and t the seconds until it resets. Counted per serving instance, so it is a close floor rather than a globally exact figure.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";r=29;t=60"
                }
              },
              "RateLimit-Policy": {
                "description": "Every quota this API enforces, so a client can see what authenticating buys it.",
                "schema": {
                  "type": "string",
                  "example": "\"free\";q=30;w=60, \"pro\";q=120;w=60, \"enterprise\";q=300;w=60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "500": {
            "description": "An unexpected fault. The message never carries internal detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Service health",
        "description": "Liveness of the web tier and its database connection. Always answers 200 — a degraded database is reported in the body, not as a status code. This is the one public endpoint that does not send RateLimit headers.",
        "tags": [
          "Operations"
        ],
        "security": [
          {}
        ],
        "responses": {
          "200": {
            "description": "Success.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "description": "True on a 2xx response."
                    },
                    "data": {
                      "$ref": "#/components/schemas/Health"
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null on success."
                    }
                  },
                  "required": [
                    "success",
                    "data"
                  ]
                }
              }
            }
          },
          "500": {
            "description": "An unexpected fault. The message never carries internal detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SalaryProvenance": {
        "type": "object",
        "description": "Where the displayed number came from, after winner-takes-all precedence between official statistics, on-demand research and pipeline estimates. TASK-949: in production today every globe row resolves to a curated estimate (kind 'estimate', source 'llm-seed', confidence 0.45), with a small number of AI-researched rows at 0.75. No row currently carries kind 'official'.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "official",
              "estimate",
              "pipeline",
              "mixed",
              "none"
            ],
            "description": "Badge category. 'mixed' only occurs on a country roll-up. LIVE VALUES: 'estimate', 'mixed', 'none'. RESERVED and not reachable in production today: 'official' (no statistics-agency row has been loaded — the seed data is calibrated against BLS/ONS/Eurostat, which is not the same as being sourced from them) and 'pipeline' (per-user benchmark overlay, which the anonymous endpoints never return). Both are kept in the enum because removing a value a client already handles is a breaking change."
          },
          "layer": {
            "type": "string",
            "enum": [
              "base",
              "research",
              "pipeline",
              "none"
            ],
            "description": "Winning display layer ('base' = official statistics or seed data; in production it is seed data). 'pipeline' is reserved for the per-user benchmark overlay and never appears on an anonymous response."
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Raw source label of the winning row."
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ],
            "minimum": 0,
            "maximum": 1,
            "description": "Confidence of the winning row."
          },
          "effectiveDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "Reference period of the source, as 'YYYY-MM-DD'."
          },
          "citations": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string",
              "format": "uri"
            },
            "description": "Source citation URLs, when the winning row carries them."
          }
        },
        "required": [
          "kind",
          "layer",
          "source",
          "confidence",
          "effectiveDate",
          "citations"
        ]
      },
      "CountrySalary": {
        "type": "object",
        "description": "Salary aggregate for one country, in USD after FX conversion.",
        "properties": {
          "countryCode": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2.",
            "example": "GB"
          },
          "countryName": {
            "type": "string",
            "example": "United Kingdom"
          },
          "avgMedian": {
            "type": "number",
            "description": "Mean of the per-city medians, FX-converted then inflation-adjusted."
          },
          "currency": {
            "type": "string",
            "description": "Always USD — values are normalised.",
            "example": "USD"
          },
          "sampleSize": {
            "type": "integer",
            "description": "Rows contributing to the average."
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "provenance": {
            "$ref": "#/components/schemas/SalaryProvenance"
          }
        },
        "required": [
          "countryCode",
          "countryName",
          "avgMedian",
          "currency",
          "sampleSize",
          "lat",
          "lng",
          "provenance"
        ]
      },
      "CitySalary": {
        "type": "object",
        "description": "Salary aggregate for one city, in USD after FX conversion.",
        "properties": {
          "city": {
            "type": "string",
            "example": "London"
          },
          "countryCode": {
            "type": "string",
            "description": "ISO 3166-1 alpha-2.",
            "example": "GB"
          },
          "median": {
            "type": [
              "number",
              "null"
            ],
            "description": "Null when the city has no salary data."
          },
          "p25": {
            "type": [
              "number",
              "null"
            ]
          },
          "p75": {
            "type": [
              "number",
              "null"
            ]
          },
          "currency": {
            "type": "string",
            "example": "USD"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "source": {
            "type": "string",
            "enum": [
              "base",
              "research",
              "pipeline",
              "none"
            ],
            "description": "Coarse display layer the value came from."
          },
          "provenance": {
            "$ref": "#/components/schemas/SalaryProvenance"
          }
        },
        "required": [
          "city",
          "countryCode",
          "median",
          "p25",
          "p75",
          "currency",
          "lat",
          "lng",
          "source",
          "provenance"
        ]
      },
      "AdvertisedSalaryCell": {
        "type": "object",
        "description": "One published advertised-salary cell: the percentiles of the annual pay employers STATED for one sector or job title in one market, over a rolling window. Advertised pay, not paid pay, and never imputed.",
        "properties": {
          "location": {
            "type": "string",
            "description": "Canonical market key: a city name, an ISO 3166-1 alpha-2 code for a national aggregate, or remote-<CC> for a remote market.",
            "example": "London"
          },
          "locationLabel": {
            "type": "string",
            "description": "Human market name.",
            "example": "London"
          },
          "citySlug": {
            "type": "string",
            "description": "First URL segment of the cell page, and a valid `city` filter value.",
            "example": "london"
          },
          "dimensionType": {
            "type": "string",
            "enum": [
              "sector",
              "title"
            ],
            "description": "Which of `sector` / `role` carries the value; the other is null."
          },
          "sector": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human sector name, or null on a job-title cell."
          },
          "role": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human job title, or null on a sector cell."
          },
          "slug": {
            "type": "string",
            "description": "Second URL segment of the cell page, and a valid `sector` filter value.",
            "example": "chef-de-partie"
          },
          "median": {
            "type": "integer",
            "description": "Median advertised annual salary."
          },
          "p25": {
            "type": "integer",
            "description": "25th percentile."
          },
          "p75": {
            "type": "integer",
            "description": "75th percentile."
          },
          "currency": {
            "type": "string",
            "description": "ISO 4217 code the figures are quoted in.",
            "example": "GBP"
          },
          "postingCount": {
            "type": "integer",
            "description": "Advertisements behind this cell. Published, never rounded — a cell below the threshold is withdrawn rather than shown with a thin sample."
          },
          "windowDays": {
            "type": "integer",
            "description": "Length of the rolling window.",
            "example": 90
          },
          "windowStart": {
            "type": "string",
            "format": "date",
            "description": "Earliest posting date in the sample."
          },
          "windowEnd": {
            "type": "string",
            "format": "date",
            "description": "Latest posting date in the sample."
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "generationId": {
            "type": [
              "string",
              "null"
            ],
            "description": "The nightly recompute run that wrote this row (`run-<ISO-8601>`), printed verbatim on every surface that renders it. Null on rows written before runs were stamped. Compare for equality; never parse."
          },
          "sourceBoards": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Job boards the underlying advertisements were aggregated from."
          },
          "canonicalUrl": {
            "type": "string",
            "format": "uri",
            "description": "Human-readable page for this cell."
          },
          "methodology": {
            "type": "string",
            "description": "One-sentence method statement, repeated on every cell so a figure lifted out of the array still says what it is."
          }
        },
        "required": [
          "location",
          "locationLabel",
          "citySlug",
          "dimensionType",
          "sector",
          "role",
          "slug",
          "median",
          "p25",
          "p75",
          "currency",
          "postingCount",
          "windowDays",
          "windowStart",
          "windowEnd",
          "computedAt",
          "generationId",
          "sourceBoards",
          "canonicalUrl",
          "methodology"
        ]
      },
      "AdvertisedSalaryMarket": {
        "type": "object",
        "description": "One market with advertised-salary data, and the exact filter values it accepts.",
        "properties": {
          "location": {
            "type": "string",
            "description": "Canonical market key.",
            "example": "GB"
          },
          "locationLabel": {
            "type": "string",
            "example": "United Kingdom"
          },
          "citySlug": {
            "type": "string",
            "description": "Pass back as `city`.",
            "example": "uk"
          },
          "currency": {
            "type": "string",
            "description": "A market is priced in one currency.",
            "example": "GBP"
          },
          "cellCount": {
            "type": "integer",
            "description": "Published cells in this market."
          },
          "postingCount": {
            "type": "integer",
            "description": "Advertisements summed across those cells."
          },
          "sectors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Sector slugs published here; pass any of them back as `sector`."
          },
          "roles": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Job-title slugs published here; pass any of them back as `sector`."
          }
        },
        "required": [
          "location",
          "locationLabel",
          "citySlug",
          "currency",
          "cellCount",
          "postingCount",
          "sectors",
          "roles"
        ]
      },
      "ResearchedRole": {
        "type": "object",
        "description": "A role title that has salary data, for autocomplete.",
        "properties": {
          "role": {
            "type": "string",
            "example": "Software Engineer"
          },
          "countryCount": {
            "type": "integer",
            "description": "Distinct countries with data for this role."
          },
          "maxConfidence": {
            "type": "number",
            "minimum": 0,
            "maximum": 1
          },
          "lastUpdated": {
            "type": "string",
            "description": "ISO timestamp of the freshest backing row, or '' if unknown."
          },
          "class": {
            "type": "string",
            "enum": [
              "curated",
              "researched"
            ],
            "description": "TASK-950. curated = at least one backing row is official statistics or the seeded catalog (authoritative, always listed); researched = free-text on-demand research only, listed when it covers 2+ countries or clears a 0.7 confidence bar. Entries are sorted by countryCount desc with curated first, so the head of the list is the best-covered role."
          }
        },
        "required": [
          "role",
          "countryCount",
          "maxConfidence",
          "lastUpdated",
          "class"
        ]
      },
      "FaqEntry": {
        "type": "object",
        "description": "One published FAQ entry. Field names are the database column names.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "question": {
            "type": "string"
          },
          "answer": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "example": "product"
          },
          "source": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the entry was drafted from, when recorded."
          },
          "helpful_count": {
            "type": "integer"
          },
          "not_helpful_count": {
            "type": "integer"
          },
          "is_published": {
            "type": "boolean",
            "description": "Always true on this endpoint."
          },
          "sort_order": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "question",
          "answer",
          "category",
          "sort_order"
        ]
      },
      "SubscriptionPlan": {
        "type": "object",
        "description": "One active subscription plan. Field names are the database column names. Owner-only rows are excluded.",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "stripe_price_id": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "example": "pro_monthly"
          },
          "billing_period": {
            "type": "string",
            "enum": [
              "monthly",
              "annual",
              "weekly",
              "none"
            ]
          },
          "price_gbp": {
            "type": "number",
            "description": "Price in GBP. 0 for the free plan."
          },
          "limits": {
            "type": "object",
            "description": "Per-metric allowance for one billing period. Numeric metrics use -1 for unlimited; `analytics` is a capability level and `email_outreach` a boolean. Keys are the 13 metered usage metrics plus a few capability flags, and new keys are added over time — read it as a map, not a fixed record.",
            "additionalProperties": {
              "oneOf": [
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                },
                {
                  "type": "string"
                }
              ]
            }
          },
          "features": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Marketing feature bullets shown on /pricing."
          },
          "is_active": {
            "type": "boolean",
            "description": "Always true on this endpoint."
          },
          "sort_order": {
            "type": "integer"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "billing_period",
          "price_gbp",
          "limits",
          "features"
        ]
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "degraded"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "version": {
            "type": "string",
            "example": "0.2.0"
          },
          "database": {
            "type": "string",
            "enum": [
              "connected",
              "degraded"
            ]
          },
          "runtime": {
            "type": "string",
            "example": "next.js"
          }
        },
        "required": [
          "status",
          "timestamp",
          "version",
          "database",
          "runtime"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "description": "The failure form of the envelope. `error` is a human-readable message.",
        "properties": {
          "success": {
            "type": "boolean",
            "description": "Always false on an error response."
          },
          "data": {
            "type": "null"
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "success",
          "data",
          "error"
        ]
      }
    }
  }
}
