{
  "openapi": "3.1.0",
  "info": {
    "title": "TestBuddy public read API",
    "version": "1.0.0",
    "summary": "Admission-test catalogue, exam structure, prices and FAQ, as JSON.",
    "description": "The same data the pages on testbuddy.ai render in HTML, in JSON, for agents and\nintegrations.\n\n**No API key, no account, read-only.** Every endpoint is a GET and nothing here\nwrites anything. Please keep request rates reasonable.\n\n**`locale` decides what exists.** TestBuddy serves around thirty markets from one\ncatalogue, so the same query returns different exams depending on it. Pass the\nlocale of the market you are answering for.\n\n**Italian-language exams are not on this API.** TOLC, Semestre Filtro, Professioni\nSanitarie and the other exams sat in Italian are covered on testbuddy.it, which has\nits own catalogue, prices and app — and its own copy of this API at\nhttps://testbuddy.it/openapi.json. Asking for one of them here returns 404 with the\naddress to go to.\n\n**Versioning and deprecation.** Every response carries `API-Version`. A breaking\nchange ships as a new version; the previous one keeps answering for at least six\nmonths, marked with `Deprecation` and `Sunset` response headers (RFC 9745 and\nRFC 8594) so an integration finds out from the wire rather than from a broken\nparse. Additive changes — a new field, a new optional parameter — do not bump the\nversion, so treat unknown fields as forward-compatible.\n\n**Rate limit.** 120 requests per minute per IP, shared across the endpoints. Every\nresponse carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`\n(seconds until the window resets); a 429 adds `Retry-After` with the same number.\nThe counter is per serverless instance, so the real ceiling is higher than the\npublished one and never lower: stay under it and you will not be blocked. Read\n`RateLimit-Remaining` as a floor, not an exact balance.\n\nBrowser-based agents can call the same four lookups as WebMCP tools on\n`document.modelContext`; see https://testbuddy.ai/developers.",
    "contact": {
      "name": "TestBuddy support",
      "email": "support@testbuddy.ai",
      "url": "https://testbuddy.ai/developers"
    },
    "license": {
      "name": "Data may be used with attribution to TestBuddy",
      "url": "https://testbuddy.ai/terms"
    }
  },
  "servers": [
    {
      "url": "https://testbuddy.ai",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Developer page",
    "url": "https://testbuddy.ai/developers"
  },
  "tags": [
    {
      "name": "tests",
      "description": "The admission-test catalogue and how each exam is built"
    },
    {
      "name": "pricing",
      "description": "Subscription plans"
    },
    {
      "name": "help",
      "description": "The official FAQ"
    }
  ],
  "paths": {
    "/api/tests": {
      "get": {
        "operationId": "searchTests",
        "tags": [
          "tests"
        ],
        "summary": "Search admission tests, or fetch one by slug",
        "description": "Pass `q` to search the catalogue for the given market, or `slug` to get one\ntest in full — subjects, question counts, timing, scoring, exam dates.\nExactly one of the two is required.\n\nWith `q` you get the market's own exams first, then international ones that\nare reachable but belong elsewhere (`relevance` tells you which is which).\n`available: false` means the product is not out yet and the page collects an\nemail instead of selling.\n\nWith `slug`, an Italian-language exam returns 404 and a `coveredElsewhere`\nbody pointing at testbuddy.it rather than the exam data.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free text: an exam name (\"UCAT\"), a subject (\"medicine\"), or a country.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "ucat"
          },
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "description": "The `slug` of one test, as returned by a search.",
            "schema": {
              "type": "string"
            },
            "example": "ucat"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "Which market's catalogue to answer for. This is not cosmetic: one catalogue serves ~30 markets, so `en-gb` returns UCAT while `de` returns MedAT and TMS. Defaults to `en` (US + international). Italian-language exams are never returned here — they live on testbuddy.it.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "en-gb",
                "en-us",
                "en-au",
                "en-ie",
                "en-ca",
                "en-nz",
                "de",
                "fr",
                "es",
                "pt",
                "nl",
                "pl",
                "ja",
                "ko",
                "zh",
                "hi",
                "bn",
                "ta",
                "te",
                "fi",
                "cs",
                "el",
                "hu",
                "id",
                "ro",
                "ru",
                "sv",
                "th",
                "tl",
                "tr",
                "uk",
                "vi",
                "ar"
              ],
              "default": "en"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results. Defaults to 8, capped at 25.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 8
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Search results, or one test in full.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/TestSearchResponse"
                    },
                    {
                      "$ref": "#/components/schemas/TestDetails"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The request could not be served. The body carries a machine-readable code and a hint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No test with that slug, or the test is Italian-language and covered on testbuddy.it.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "$ref": "#/components/schemas/CoveredElsewhere"
                    }
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/api/pricing": {
      "get": {
        "operationId": "getPricing",
        "tags": [
          "pricing"
        ],
        "summary": "Subscription plans and prices for a market",
        "description": "Plans for 1, 3, 6 and 12 months with the total in euro and the equivalent\nmonthly cost. Omit `q` for the whole price list of that market.\n\nWhen `available` is false the product is not on sale yet: the prices are a\nplaceholder the page does not show, so do not quote them as real.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "The test to price. Omit for the whole list.",
            "schema": {
              "type": "string"
            },
            "example": "ucat"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "Which market's catalogue to answer for. This is not cosmetic: one catalogue serves ~30 markets, so `en-gb` returns UCAT while `de` returns MedAT and TMS. Defaults to `en` (US + international). Italian-language exams are never returned here — they live on testbuddy.it.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "en-gb",
                "en-us",
                "en-au",
                "en-ie",
                "en-ca",
                "en-nz",
                "de",
                "fr",
                "es",
                "pt",
                "nl",
                "pl",
                "ja",
                "ko",
                "zh",
                "hi",
                "bn",
                "ta",
                "te",
                "fi",
                "cs",
                "el",
                "hu",
                "id",
                "ro",
                "ru",
                "sv",
                "th",
                "tl",
                "tr",
                "uk",
                "vi",
                "ar"
              ],
              "default": "en"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results. Defaults to 10, capped at 25.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching price lists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PricingResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request could not be served. The body carries a machine-readable code and a hint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/help": {
      "get": {
        "operationId": "searchHelp",
        "tags": [
          "help"
        ],
        "summary": "Search the official FAQ",
        "description": "Returns the question, the full answer and the canonical URL.\n\nThe FAQ on this site is short and product-focused — early access, which tests\nare covered, what the AI assistant does, devices, getting started. It does not\ncover billing, cancellation or refunds: an empty result for those means the\nanswer does not exist, not that the search failed.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "The user's question, in their own words.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "early access"
          },
          {
            "name": "locale",
            "in": "query",
            "required": false,
            "description": "Which market's catalogue to answer for. This is not cosmetic: one catalogue serves ~30 markets, so `en-gb` returns UCAT while `de` returns MedAT and TMS. Defaults to `en` (US + international). Italian-language exams are never returned here — they live on testbuddy.it.",
            "schema": {
              "type": "string",
              "enum": [
                "en",
                "en-gb",
                "en-us",
                "en-au",
                "en-ie",
                "en-ca",
                "en-nz",
                "de",
                "fr",
                "es",
                "pt",
                "nl",
                "pl",
                "ja",
                "ko",
                "zh",
                "hi",
                "bn",
                "ta",
                "te",
                "fi",
                "cs",
                "el",
                "hu",
                "id",
                "ro",
                "ru",
                "sv",
                "th",
                "tl",
                "tr",
                "uk",
                "vi",
                "ar"
              ],
              "default": "en"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of results. Defaults to 5, capped at 15.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 15,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching FAQ entries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HelpResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request could not be served. The body carries a machine-readable code and a hint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable code.",
                "examples": [
                  "missing_parameter",
                  "endpoint_not_found",
                  "test_not_found"
                ]
              },
              "message": {
                "type": "string",
                "description": "What went wrong, in one sentence."
              },
              "hint": {
                "type": "string",
                "description": "What to do about it."
              },
              "documentation": {
                "type": "string",
                "format": "uri"
              },
              "specification": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "CoveredElsewhere": {
        "type": "object",
        "description": "The test exists, but on the Italian site. Returned with status 404 so it is never mistaken for data this site owns.",
        "required": [
          "coveredElsewhere",
          "site",
          "url",
          "message"
        ],
        "properties": {
          "coveredElsewhere": {
            "type": "boolean",
            "const": true
          },
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "site": {
            "type": "string",
            "examples": [
              "testbuddy.it"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "TestSummary": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "category",
          "url",
          "relevance",
          "available"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "Stable identifier, used by the other endpoints."
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The exam page."
          },
          "libraryUrl": {
            "type": "string",
            "format": "uri",
            "description": "The study manual."
          },
          "practiseUrl": {
            "type": "string",
            "format": "uri",
            "description": "The practice simulator."
          },
          "relevance": {
            "type": "string",
            "enum": [
              "local",
              "international"
            ],
            "description": "`local`: the exam is sat in this market. `international`: reachable, but it belongs to another market or to none."
          },
          "available": {
            "type": "boolean",
            "description": "false = coming soon; the page collects an email instead of selling."
          }
        }
      },
      "TestSearchResponse": {
        "type": "object",
        "required": [
          "query",
          "locale",
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TestSummary"
            }
          }
        }
      },
      "TestDetails": {
        "allOf": [
          {
            "$ref": "#/components/schemas/TestSummary"
          },
          {
            "type": "object",
            "properties": {
              "officialTitle": {
                "type": "string"
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "countries": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "ISO country codes. `INTL` and `EU` are virtual codes for exams without one country."
              },
              "examStructure": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "subjects": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "questions": {
                          "type": "integer"
                        },
                        "timeMinutes": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "totalQuestions": {
                    "type": "integer"
                  },
                  "totalTimeMinutes": {
                    "type": "integer"
                  },
                  "optionsPerQuestion": {
                    "type": "integer"
                  },
                  "mode": {
                    "type": "string",
                    "examples": [
                      "cbt",
                      "paper"
                    ]
                  },
                  "scoring": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "correctAnswer": {
                        "type": "number",
                        "description": "Marks for a correct answer."
                      },
                      "wrongAnswerPenalty": {
                        "type": "number",
                        "description": "Marks SUBTRACTED for a wrong answer. 0 means no penalty."
                      },
                      "blankAnswer": {
                        "type": "number",
                        "description": "Marks for leaving it blank."
                      }
                    }
                  }
                }
              },
              "examDate": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Free text, as published."
              },
              "examDates": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "string",
                  "format": "date"
                },
                "description": "ISO dates when the calendar is confirmed."
              },
              "examDateConfirmed": {
                "type": "boolean"
              },
              "keyDates": {
                "type": [
                  "array",
                  "null"
                ],
                "items": {
                  "type": "object",
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "value": {
                      "type": "string"
                    }
                  }
                }
              },
              "keyDatesNote": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          }
        ]
      },
      "PricingPlan": {
        "type": "object",
        "required": [
          "period",
          "months",
          "price",
          "pricePerMonth"
        ],
        "properties": {
          "period": {
            "type": "string",
            "enum": [
              "monthly",
              "quarterly",
              "semiannual",
              "annual"
            ]
          },
          "months": {
            "type": "integer"
          },
          "price": {
            "type": "number",
            "description": "Total for the period, in euro."
          },
          "pricePerMonth": {
            "type": "number",
            "description": "price / months."
          }
        }
      },
      "PricingResponse": {
        "type": "object",
        "required": [
          "locale",
          "currency",
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "type": [
              "string",
              "null"
            ]
          },
          "locale": {
            "type": "string"
          },
          "currency": {
            "type": "string",
            "const": "EUR"
          },
          "count": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "slug",
                "available",
                "plans"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "slug": {
                  "type": "string"
                },
                "category": {
                  "type": [
                    "string",
                    "null"
                  ]
                },
                "available": {
                  "type": "boolean",
                  "description": "false = not on sale yet; the prices are a placeholder."
                },
                "plans": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PricingPlan"
                  }
                },
                "includes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "HelpResponse": {
        "type": "object",
        "required": [
          "query",
          "locale",
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "type": "string"
          },
          "locale": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "slug",
                "question",
                "answer",
                "url"
              ],
              "properties": {
                "slug": {
                  "type": "string"
                },
                "question": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "answer": {
                  "type": "string",
                  "description": "The full answer, as plain text."
                }
              }
            }
          }
        }
      }
    }
  }
}