{
  "openapi": "3.1.0",
  "info": {
    "title": "TestBuddy — API pubblica di lettura",
    "version": "1.0.0",
    "summary": "Catalogo dei test di ammissione italiani, struttura d'esame, prezzi e FAQ, in JSON.",
    "description": "Gli stessi dati che le pagine di testbuddy.it mostrano in HTML, in JSON, per\nagenti e integrazioni.\n\n**Nessuna chiave, nessun account, sola lettura.** Sono tutte GET e non scrivono\nniente. Si chiede solo di tenere un ritmo di richieste ragionevole.\n\n**Il perimetro è quello del sito, non del catalogo interno.** `data/tests.json` è\ncondiviso con l'app e con testbuddy.ai e contiene anche test esteri e dismessi:\nda qui escono solo i test e i concorsi che su questo sito hanno una pagina. Un\ntest che non c'è risponde 404, non un risultato inventato.\n\n**I test internazionali stanno sull'altro dominio.** UCAT, MCAT, MedAT, TMS,\nGAMSAT e gli altri esami non italiani sono su testbuddy.ai, che ha il suo\ncatalogo e la sua copia di questa spec: https://testbuddy.ai/openapi.json.\n\nGli agenti che girano nel browser trovano le stesse quattro letture come tool\nWebMCP su `document.modelContext`; vedi https://testbuddy.it/developers.",
    "contact": {
      "name": "Assistenza TestBuddy",
      "email": "support@testbuddy.it",
      "url": "https://testbuddy.it/developers"
    },
    "license": {
      "name": "Dati utilizzabili citando TestBuddy",
      "url": "https://testbuddy.it/termini-e-condizioni"
    }
  },
  "servers": [
    {
      "url": "https://testbuddy.it",
      "description": "Produzione"
    }
  ],
  "externalDocs": {
    "description": "Pagina per sviluppatori",
    "url": "https://testbuddy.it/developers"
  },
  "tags": [
    {
      "name": "tests",
      "description": "Il catalogo dei test e come è fatta ogni prova"
    },
    {
      "name": "pricing",
      "description": "I piani di abbonamento"
    },
    {
      "name": "help",
      "description": "Le FAQ ufficiali"
    }
  ],
  "paths": {
    "/api/tests": {
      "get": {
        "operationId": "searchTests",
        "tags": [
          "tests"
        ],
        "summary": "Cerca un test, oppure prendine uno per slug",
        "description": "Passa `q` per cercare nel catalogo, oppure `slug` per avere un test intero:\nmaterie, numero di domande per materia, tempi, punteggio, soglia minima,\ndate chiave e sito ufficiale. Serve uno dei due.\n\n`simulatorUrl` compare solo dove esiste davvero un simulatore, `pricingId`\nsolo dove c'è un listino da passare a getPricing.\n\nIn `examStructure`, `questions` è `null` quando il bando pubblica solo il\ntotale della prova e non lo split per materia: in quel caso vale\n`totalQuestions` e le materie sono aree, senza un conteggio da inventare.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Testo libero: nome del test (\"TOLC-I\"), corso di laurea o professione (\"medicina\", \"infermieristica\"), ateneo (\"Bocconi\") o concorso (\"allievi agenti polizia\").",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "medicina"
          },
          {
            "name": "slug",
            "in": "query",
            "required": false,
            "description": "Lo `slug` di un test, come restituito dalla ricerca.",
            "schema": {
              "type": "string"
            },
            "example": "tolc-i"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Numero massimo di risultati. Default 8, tetto 25.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 8
            }
          }
        ],
        "responses": {
          "200": {
            "description": "I risultati della ricerca, oppure un test intero.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/TestSearchResponse"
                    },
                    {
                      "$ref": "#/components/schemas/TestDetails"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "La richiesta non è servibile. Il corpo porta un codice leggibile a macchina e un suggerimento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Nessun test con quello slug fra quelli che il sito copre.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/pricing": {
      "get": {
        "operationId": "getPricing",
        "tags": [
          "pricing"
        ],
        "summary": "Piani e prezzi del Piano Completo",
        "description": "Piani da 1, 3, 6 e 12 mesi con il prezzo totale in euro e il costo mensile\nequivalente. Senza `q` restituisce tutto il listino.\n\nL'abbonamento si acquista dentro l'app, non da questo sito: qui ci sono i\nprezzi, non un checkout.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Il test di cui servono i prezzi. Omettilo per l'intero listino.",
            "schema": {
              "type": "string"
            },
            "example": "medicina"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Numero massimo di risultati. Default 10, tetto 25.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "I listini che corrispondono.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PricingResponse"
                }
              }
            }
          },
          "400": {
            "description": "La richiesta non è servibile. Il corpo porta un codice leggibile a macchina e un suggerimento.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/help": {
      "get": {
        "operationId": "searchHelp",
        "tags": [
          "help"
        ],
        "summary": "Cerca nelle FAQ ufficiali",
        "description": "Restituisce domanda, risposta completa in markdown e URL canonico.\n\nCopre come funziona il servizio, abbonamenti e disdetta, rimborsi, app e\ndispositivi, contenuti, funzioni AI e assistenza.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "La domanda dell'utente, con le sue parole.",
            "schema": {
              "type": "string",
              "minLength": 1
            },
            "example": "come disdire l'abbonamento"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Numero massimo di risultati. Default 5, tetto 15.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 15,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Le FAQ che corrispondono.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HelpResponse"
                }
              }
            }
          },
          "400": {
            "description": "La richiesta non è servibile. Il corpo porta un codice leggibile a macchina e un suggerimento.",
            "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": "Codice stabile, leggibile a macchina.",
                "examples": [
                  "missing_parameter",
                  "endpoint_not_found",
                  "test_not_found"
                ]
              },
              "message": {
                "type": "string",
                "description": "Cosa è andato storto, in una frase."
              },
              "hint": {
                "type": "string",
                "description": "Cosa fare per rimediare."
              },
              "documentation": {
                "type": "string",
                "format": "uri"
              },
              "specification": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "TestSummary": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "category",
          "url"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "Identificatore stabile, usato dalle altre rotte."
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "La pagina del test."
          },
          "simulatorUrl": {
            "type": "string",
            "format": "uri",
            "description": "Presente solo dove esiste davvero un simulatore."
          },
          "pricingId": {
            "type": "string",
            "description": "Presente solo dove esiste un listino: passalo a getPricing."
          }
        }
      },
      "TestSearchResponse": {
        "type": "object",
        "required": [
          "query",
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "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",
                  "null"
                ]
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "examStructure": {
                "type": [
                  "object",
                  "null"
                ],
                "properties": {
                  "subjects": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "questions": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "null quando il bando dà solo il totale: usa totalQuestions e tratta le materie come aree."
                        },
                        "timeMinutes": {
                          "type": "integer"
                        }
                      }
                    }
                  },
                  "totalQuestions": {
                    "type": "integer"
                  },
                  "totalTimeMinutes": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "scoring": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "correctAnswer": {
                        "type": "number",
                        "description": "Punti per risposta corretta."
                      },
                      "wrongAnswerPenalty": {
                        "type": "number",
                        "description": "Punti SOTTRATTI per risposta errata. 0 = nessuna penalità."
                      },
                      "blankAnswer": {
                        "type": "number",
                        "description": "Punti per risposta non data."
                      }
                    }
                  }
                }
              },
              "minimumScore": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Testo libero: la soglia, oppure perché non esiste."
              },
              "hasOfficialQuestionBank": {
                "type": [
                  "boolean",
                  "null"
                ]
              },
              "keyDates": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Testo libero multi-riga."
              },
              "examDate": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "officialWebsite": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri"
              }
            }
          }
        ]
      },
      "PricingPlan": {
        "type": "object",
        "required": [
          "period",
          "months",
          "price",
          "pricePerMonth"
        ],
        "properties": {
          "period": {
            "type": "string",
            "enum": [
              "monthly",
              "quarterly",
              "semiannual",
              "annual"
            ]
          },
          "months": {
            "type": "integer"
          },
          "price": {
            "type": "number",
            "description": "Totale del periodo, in euro, IVA inclusa."
          },
          "pricePerMonth": {
            "type": "number",
            "description": "price / months."
          }
        }
      },
      "PricingResponse": {
        "type": "object",
        "required": [
          "currency",
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "type": [
              "string",
              "null"
            ]
          },
          "currency": {
            "type": "string",
            "const": "EUR"
          },
          "count": {
            "type": "integer"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "label",
                "plans"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                },
                "plans": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PricingPlan"
                  }
                },
                "includes": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      },
      "HelpResponse": {
        "type": "object",
        "required": [
          "query",
          "count",
          "results"
        ],
        "properties": {
          "query": {
            "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": "La risposta intera, in markdown."
                }
              }
            }
          }
        }
      }
    }
  }
}