{
  "openapi": "3.1.0",
  "info": {
    "title": "VettedTen API",
    "version": "1.0.0",
    "summary": "UK product rankings: ten products per buying guide, scored out of 10",
    "description": "Keyless, read-only API for VettedTen (vettedten.com), a UK buying-guide site that ranks ten products in every guide from owner ratings. Search 17 guides and every product in them, read a whole ranking with each product's score, badge, pros and cons, and browse categories and \"which type?\" hubs. No prices and no Amazon ratings: scores are VettedTen's own. No authentication except the bulk export (a free, self-serve OAuth token: https://vettedten.com/auth.md). Open CORS, no write endpoints. Fair use is 300 requests a minute per IP, advertised in RateLimit headers (429 with Retry-After beyond it). Every endpoint is stateless, so every request is idempotent; send an Idempotency-Key header and it is echoed back. Versioning and deprecation: the version is in the URL (/v1). Breaking changes only ever ship as a new version, announced in this description and on https://vettedten.com/developers/ at least six months before the old version is retired, and responses from a retiring version carry Deprecation and Sunset headers. Additive changes (new fields, new endpoints) can arrive in v1 at any time, so ignore fields you don't know. A sandbox with the same endpoints is at https://vettedten.com/api/sandbox/v1.",
    "contact": {
      "name": "VettedTen",
      "email": "hello@vettedten.com",
      "url": "https://vettedten.com/developers/"
    },
    "termsOfService": "https://vettedten.com/developers/#terms"
  },
  "externalDocs": {
    "description": "Developer docs",
    "url": "https://vettedten.com/developers/"
  },
  "servers": [
    {
      "url": "https://vettedten.com/api",
      "description": "Production"
    },
    {
      "url": "https://vettedten.com/api/sandbox",
      "description": "Sandbox: identical and stateless, marks responses X-Sandbox: true"
    }
  ],
  "tags": [
    {
      "name": "rankings",
      "description": "Guides, products, categories and type hubs"
    },
    {
      "name": "meta",
      "description": "The API itself, the method and contact details"
    },
    {
      "name": "bulk",
      "description": "Batch requests and the bulk export"
    }
  ],
  "paths": {
    "/v1": {
      "get": {
        "operationId": "getApiIndex",
        "summary": "API index",
        "description": "Every endpoint, with links to the spec, the docs and the MCP servers.",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/guides": {
      "get": {
        "operationId": "searchGuides",
        "summary": "Search buying guides",
        "description": "Guides matching a product type or need, best match first; without q, every guide, newest first. Each result names its No. 1 and badge winners.",
        "tags": [
          "rankings"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GuidePage"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, for example a bad cursor or limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "What the shopper wants, e.g. \"air fryer\" or \"quiet dehumidifier\"",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "A category key or name, e.g. \"kitchen\"",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Results per page, 1-50 (default 10)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "nextCursor from the previous page",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/guides/{slug}": {
      "get": {
        "operationId": "getGuide",
        "summary": "Get a guide",
        "description": "One buying guide: all ten products in rank order with score, badge, summary, pros, cons and Amazon link, plus the FAQ and method note.",
        "tags": [
          "rankings"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Guide"
                }
              }
            }
          },
          "404": {
            "description": "No such guide, category, hub or endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The guide slug, e.g. \"air-fryers\"",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ]
      }
    },
    "/v1/products": {
      "get": {
        "operationId": "searchProducts",
        "summary": "Search products",
        "description": "Products across every guide matching a name, brand or model, with the guide each ranks in and its rank there.",
        "tags": [
          "rankings"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductPage"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, for example a bad cursor or limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Product name, brand or model code, e.g. \"ninja af300\"",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "brand",
            "in": "query",
            "required": false,
            "description": "Only this brand",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Results per page, 1-50 (default 10)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "nextCursor from the previous page",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/categories": {
      "get": {
        "operationId": "listCategories",
        "summary": "List categories",
        "description": "The site's categories with their guides and type hubs.",
        "tags": [
          "rankings"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "categories"
                  ],
                  "properties": {
                    "categories": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Category"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, for example a bad cursor or limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "withGuides",
            "in": "query",
            "required": false,
            "description": "true to leave out categories with no guides yet",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ]
      }
    },
    "/v1/categories/{key}": {
      "get": {
        "operationId": "getCategory",
        "summary": "Get a category",
        "description": "One category with summaries of its guides.",
        "tags": [
          "rankings"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryDetail"
                }
              }
            }
          },
          "404": {
            "description": "No such guide, category, hub or endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Category key, e.g. \"kitchen\"",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/families": {
      "get": {
        "operationId": "listFamilies",
        "summary": "List type hubs",
        "description": "\"Which type?\" hubs: a product split into types (pod, bean-to-cup, espresso), each type ranked as its own guide.",
        "tags": [
          "rankings"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "families"
                  ],
                  "properties": {
                    "families": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Family"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such guide, category, hub or endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Only the hubs in this category (key or name), e.g. \"kitchen\"",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/families/{key}": {
      "get": {
        "operationId": "getFamily",
        "summary": "Get a type hub",
        "description": "One type hub: every type, who it suits, and each type's No. 1.",
        "tags": [
          "rankings"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Family"
                }
              }
            }
          },
          "404": {
            "description": "No such guide, category, hub or endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "description": "Hub key, e.g. \"coffee-machines\"",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v1/methodology": {
      "get": {
        "operationId": "getMethodology",
        "summary": "How the rankings work",
        "description": "The scoring principles, the badges and the independence rules.",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Methodology"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/contact": {
      "get": {
        "operationId": "getContact",
        "summary": "Contact details",
        "description": "How to reach VettedTen with a correction or a product suggestion. There are no write endpoints.",
        "tags": [
          "meta"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Contact"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/batch": {
      "post": {
        "operationId": "batchRequests",
        "summary": "Batch GET requests",
        "description": "Up to 20 GET paths in one round trip. Each answer carries its own status.",
        "tags": [
          "bulk"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BatchResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request is invalid, for example a bad cursor or limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchRequest"
              }
            }
          }
        }
      }
    },
    "/v1/export": {
      "get": {
        "operationId": "getExport",
        "summary": "Bulk export",
        "description": "Every guide, type hub and category in one document, or one category's. Needs a free bearer token (see /auth.md); every other endpoint is keyless.",
        "tags": [
          "bulk"
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Export"
                }
              }
            }
          },
          "401": {
            "description": "A bearer token is missing, expired or revoked",
            "headers": {
              "WWW-Authenticate": {
                "description": "Bearer resource_metadata=\"…\": where to find out how to get a token",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such guide, category, hub or endpoint",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Over the fair-use limit of 300 requests a minute per IP",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "default": {
            "description": "Any other error, in the same shape",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Only this category (key or name), e.g. \"household\"",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "vettedtenOAuth": [
              "export"
            ]
          }
        ]
      }
    }
  },
  "components": {
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "description": "Optional. Every endpoint is stateless, so repeats are safe; the key is echoed back.",
        "schema": {
          "type": "string",
          "maxLength": 255
        }
      }
    },
    "securitySchemes": {
      "vettedtenOAuth": {
        "type": "oauth2",
        "description": "Free and self-serve. Register (anonymous agent registration at /agent/identity, or RFC 7591 at /oauth/register), then exchange at /oauth/token. Only the bulk export needs it. Walkthrough: /auth.md",
        "flows": {
          "clientCredentials": {
            "tokenUrl": "https://vettedten.com/oauth/token",
            "refreshUrl": "https://vettedten.com/oauth/token",
            "scopes": {
              "export": "Read the whole dataset in one call"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Machine-readable code, e.g. not_found"
              },
              "message": {
                "type": "string",
                "description": "What went wrong and how to fix it"
              },
              "docs": {
                "type": "string",
                "description": "Documentation URL",
                "format": "uri"
              }
            }
          }
        }
      },
      "ApiIndex": {
        "type": "object",
        "required": [
          "name",
          "version",
          "endpoints"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "API name"
          },
          "version": {
            "type": "string",
            "description": "API version"
          },
          "description": {
            "type": "string",
            "description": "What the API offers"
          },
          "openapi": {
            "type": "string",
            "description": "This spec",
            "format": "uri"
          },
          "documentation": {
            "type": "string",
            "description": "Docs",
            "format": "uri"
          },
          "agentGuide": {
            "type": "string",
            "description": "agents.md",
            "format": "uri"
          },
          "mcp": {
            "type": "object",
            "properties": {
              "product": {
                "type": "string",
                "description": "Rankings MCP server",
                "format": "uri"
              },
              "docs": {
                "type": "string",
                "description": "Docs MCP server",
                "format": "uri"
              }
            }
          },
          "endpoints": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "auth": {
            "type": "string",
            "description": "Authentication summary"
          }
        }
      },
      "Badge": {
        "type": "string",
        "enum": [
          "Best Choice",
          "Best Premium",
          "Value for Money",
          "Best Budget"
        ],
        "description": "A badge the ranking model gives at most one product per guide"
      },
      "Pick": {
        "type": "object",
        "required": [
          "badge",
          "rank",
          "name"
        ],
        "properties": {
          "badge": {
            "$ref": "#/components/schemas/Badge"
          },
          "rank": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10
          },
          "name": {
            "type": "string",
            "description": "Product name"
          }
        }
      },
      "Product": {
        "type": "object",
        "required": [
          "rank",
          "name",
          "brand",
          "score",
          "url",
          "amazon"
        ],
        "properties": {
          "rank": {
            "type": "integer",
            "minimum": 1,
            "maximum": 10
          },
          "id": {
            "type": "string",
            "description": "Anchor id on the page"
          },
          "name": {
            "type": "string",
            "description": "Product name"
          },
          "brand": {
            "type": "string",
            "description": "Brand"
          },
          "score": {
            "type": "number",
            "minimum": 0,
            "maximum": 10,
            "description": "VettedTen's score out of 10 (not an Amazon rating)"
          },
          "badge": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Badge"
              },
              {
                "type": "null"
              }
            ]
          },
          "summary": {
            "type": "string",
            "description": "The review summary"
          },
          "pros": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Why it's here"
          },
          "cons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Keep in mind"
          },
          "url": {
            "type": "string",
            "description": "The review on the guide page",
            "format": "uri"
          },
          "amazon": {
            "type": "string",
            "description": "Amazon UK listing (no prices are given; this link shows the current one)",
            "format": "uri"
          },
          "image": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "description": "Product photo, or null where the brand publishes none"
          }
        }
      },
      "FaqItem": {
        "type": "object",
        "required": [
          "q",
          "a"
        ],
        "properties": {
          "q": {
            "type": "string",
            "description": "Question"
          },
          "a": {
            "type": "string",
            "description": "Answer"
          }
        }
      },
      "LinkTile": {
        "type": "object",
        "required": [
          "name",
          "url"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Guide label"
          },
          "url": {
            "type": "string",
            "description": "Guide URL",
            "format": "uri"
          },
          "topPick": {
            "type": "string",
            "description": "Its No. 1"
          }
        }
      },
      "GuideFamily": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Type hub key"
          },
          "name": {
            "type": "string",
            "description": "Product, e.g. Coffee Machines"
          },
          "type": {
            "type": "string",
            "description": "This guide's type, e.g. Pod"
          },
          "hub": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          }
        }
      },
      "Guide": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "url",
          "category",
          "updated",
          "verdict",
          "products"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "Guide slug"
          },
          "title": {
            "type": "string",
            "description": "Guide title"
          },
          "heading": {
            "type": "string",
            "description": "Page heading"
          },
          "description": {
            "type": "string",
            "description": "Summary"
          },
          "url": {
            "type": "string",
            "description": "Page URL (cite this)",
            "format": "uri"
          },
          "markdown": {
            "type": "string",
            "description": "Markdown twin",
            "format": "uri"
          },
          "api": {
            "type": "string",
            "description": "This resource",
            "format": "uri"
          },
          "category": {
            "type": "string",
            "description": "Category name"
          },
          "categoryUrl": {
            "type": "string",
            "description": "Category page",
            "format": "uri"
          },
          "label": {
            "type": "string",
            "description": "Product type, e.g. Air Fryers"
          },
          "family": {
            "$ref": "#/components/schemas/GuideFamily"
          },
          "updated": {
            "type": "string",
            "description": "Date the picks were last researched",
            "format": "date"
          },
          "modelsCompared": {
            "type": [
              "integer",
              "null"
            ]
          },
          "verdict": {
            "type": "string",
            "description": "The verdict line"
          },
          "picks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Pick"
            }
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Product"
            },
            "minItems": 1,
            "maxItems": 10
          },
          "faq": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FaqItem"
            }
          },
          "method": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "otherTypes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LinkTile"
            }
          },
          "related": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LinkTile"
            }
          }
        }
      },
      "TopPick": {
        "type": "object",
        "required": [
          "name",
          "score"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name"
          },
          "score": {
            "type": "number"
          },
          "badge": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Badge"
              },
              {
                "type": "null"
              }
            ]
          },
          "amazon": {
            "type": "string",
            "description": "Amazon UK listing",
            "format": "uri"
          }
        }
      },
      "GuideSummary": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "url",
          "updated",
          "topPick"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "Guide slug"
          },
          "title": {
            "type": "string",
            "description": "Guide title"
          },
          "url": {
            "type": "string",
            "description": "Page URL",
            "format": "uri"
          },
          "markdown": {
            "type": "string",
            "description": "Markdown twin",
            "format": "uri"
          },
          "api": {
            "type": "string",
            "description": "Full guide",
            "format": "uri"
          },
          "category": {
            "type": "string",
            "description": "Category"
          },
          "label": {
            "type": "string",
            "description": "Product type"
          },
          "family": {
            "$ref": "#/components/schemas/GuideFamily"
          },
          "updated": {
            "type": "string",
            "description": "Research date",
            "format": "date"
          },
          "verdict": {
            "type": "string",
            "description": "The verdict line"
          },
          "topPick": {
            "$ref": "#/components/schemas/TopPick"
          },
          "picks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "badge": {
                  "$ref": "#/components/schemas/Badge"
                },
                "rank": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "score": {
                  "type": "number"
                }
              }
            }
          },
          "relevance": {
            "type": "number",
            "description": "Search score, when q was given"
          }
        }
      },
      "GuidePage": {
        "type": "object",
        "required": [
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "count": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ],
            "description": "Pass as cursor for the next page; null on the last page"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GuideSummary"
            }
          }
        }
      },
      "ProductHit": {
        "type": "object",
        "required": [
          "name",
          "guide",
          "rank",
          "score"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Product name"
          },
          "brand": {
            "type": "string",
            "description": "Brand"
          },
          "rank": {
            "type": "integer"
          },
          "score": {
            "type": "number"
          },
          "badge": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Badge"
              },
              {
                "type": "null"
              }
            ]
          },
          "guide": {
            "type": "string",
            "description": "Guide slug"
          },
          "guideTitle": {
            "type": "string",
            "description": "Guide title"
          },
          "url": {
            "type": "string",
            "description": "The review",
            "format": "uri"
          },
          "amazon": {
            "type": "string",
            "description": "Amazon UK listing",
            "format": "uri"
          },
          "relevance": {
            "type": "number"
          }
        }
      },
      "ProductPage": {
        "type": "object",
        "required": [
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "type": [
              "string",
              "null"
            ]
          },
          "count": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "nextCursor": {
            "type": [
              "string",
              "null"
            ]
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductHit"
            }
          }
        }
      },
      "Category": {
        "type": "object",
        "required": [
          "key",
          "name",
          "url"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Category key"
          },
          "name": {
            "type": "string",
            "description": "Category name"
          },
          "url": {
            "type": "string",
            "description": "Category page",
            "format": "uri"
          },
          "markdown": {
            "type": "string",
            "description": "Markdown twin",
            "format": "uri"
          },
          "llms": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "guides": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "families": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CategoryDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Category"
          },
          {
            "type": "object",
            "properties": {
              "guideSummaries": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/GuideSummary"
                }
              }
            }
          }
        ]
      },
      "FamilyType": {
        "type": "object",
        "required": [
          "key",
          "name"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Type key"
          },
          "name": {
            "type": "string",
            "description": "Type name"
          },
          "badge": {
            "type": "string",
            "description": "The badge its No. 1 wears on the hub"
          },
          "what": {
            "type": "string",
            "description": "What this type does"
          },
          "bestFor": {
            "type": "string",
            "description": "Who it suits"
          },
          "guide": {
            "type": [
              "string",
              "null"
            ]
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri"
          },
          "label": {
            "type": [
              "string",
              "null"
            ]
          },
          "topPick": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "score": {
                "type": "number"
              },
              "why": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "amazon": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "Family": {
        "type": "object",
        "required": [
          "key",
          "title",
          "url",
          "types"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Hub key"
          },
          "title": {
            "type": "string",
            "description": "Hub title"
          },
          "heading": {
            "type": "string",
            "description": "Page heading"
          },
          "description": {
            "type": "string",
            "description": "Summary"
          },
          "url": {
            "type": "string",
            "description": "Hub page",
            "format": "uri"
          },
          "markdown": {
            "type": "string",
            "description": "Markdown twin",
            "format": "uri"
          },
          "api": {
            "type": "string",
            "description": "This resource",
            "format": "uri"
          },
          "category": {
            "type": "string",
            "description": "Category"
          },
          "name": {
            "type": "string",
            "description": "Product"
          },
          "updated": {
            "type": "string",
            "description": "Newest research date",
            "format": "date"
          },
          "verdict": {
            "type": "string",
            "description": "The verdict line"
          },
          "intro": {
            "type": "string",
            "description": "Which type suits whom"
          },
          "types": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FamilyType"
            }
          },
          "faq": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FaqItem"
            }
          },
          "method": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "related": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LinkTile"
            }
          }
        }
      },
      "Methodology": {
        "type": "object",
        "required": [
          "summary",
          "principles"
        ],
        "properties": {
          "summary": {
            "type": "string"
          },
          "principles": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                }
              }
            }
          },
          "badges": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "$ref": "#/components/schemas/Badge"
                },
                "meaning": {
                  "type": "string"
                }
              }
            }
          },
          "independence": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Contact": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "page": {
            "type": "string",
            "format": "uri"
          },
          "for": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "note": {
            "type": "string"
          }
        }
      },
      "BatchRequest": {
        "type": "object",
        "required": [
          "requests"
        ],
        "properties": {
          "requests": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "object",
              "required": [
                "path"
              ],
              "properties": {
                "path": {
                  "type": "string",
                  "description": "A GET path, e.g. /v1/guides/kettles or /v1/guides?q=kettle"
                }
              }
            }
          }
        }
      },
      "BatchResponse": {
        "type": "object",
        "required": [
          "responses"
        ],
        "properties": {
          "responses": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "path",
                "status",
                "body"
              ],
              "properties": {
                "path": {
                  "type": "string"
                },
                "status": {
                  "type": "integer"
                },
                "body": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "Export": {
        "type": "object",
        "required": [
          "exportedAt",
          "guides"
        ],
        "properties": {
          "exportedAt": {
            "type": "string",
            "format": "date-time"
          },
          "client": {
            "type": "string"
          },
          "guides": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Guide"
            }
          },
          "families": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Family"
            }
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Category"
            }
          }
        }
      }
    }
  }
}
