{
  "openapi": "3.1.0",
  "info": {
    "title": "Heldivo API",
    "version": "1.0",
    "summary": "Lesplanning en instructievideo's voor rijscholen en zelfstandige rij-instructeurs.",
    "description": "Alle paden onder /api/v1. Antwoorden zijn application/json; charset=utf-8. Fouten hebben één vaste vorm met een code in het Engels en een message in het Nederlands, plus een request_id en de header X-Request-Id. Authenticatie kan met een sessiecookie (na inloggen, met CSRF-header voor wijzigingen) of met een persoonlijk Bearer-token."
  },
  "servers": [
    {
      "url": "https://api.heldivo.nl",
      "description": "Productie (per regio een eigen host en database)"
    }
  ],
  "tags": [
    {
      "name": "Systeem",
      "description": "Gezondheid, status en specificatie."
    },
    {
      "name": "Authenticatie",
      "description": "Registreren, inloggen, uitloggen."
    },
    {
      "name": "Account",
      "description": "Profiel, wachtwoord, tokens, account verwijderen."
    },
    {
      "name": "Leerlingen",
      "description": "Leerlingen beheren."
    },
    {
      "name": "Lessen",
      "description": "Lessen plannen met overlapcontrole."
    },
    {
      "name": "Lesboekje",
      "description": "Niveaus 0–4 per onderdeel met geschiedenis."
    },
    {
      "name": "Video's",
      "description": "Korte instructievideo's en publieke links."
    },
    {
      "name": "Export",
      "description": "Je gegevens als CSV en zip."
    },
    {
      "name": "Wachtlijst",
      "description": "Wachtlijst voor betaalde pakketten."
    },
    {
      "name": "Contact",
      "description": "Contactformulier."
    }
  ],
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Persoonlijk token uit het dashboard, als 'Authorization: Bearer hld_...'."
      },
      "sessie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "__Host-heldivo_sessie",
        "description": "Sessiecookie na inloggen. Wijzigende verzoeken vereisen de header X-CSRF-Token."
      }
    },
    "schemas": {
      "Fout": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "description": "Machinecode in het Engels."
              },
              "message": {
                "type": "string",
                "description": "Uitleg in het Nederlands."
              },
              "fields": {
                "type": "object",
                "description": "Per veld een melding (bij 422)."
              }
            }
          },
          "request_id": {
            "type": "string"
          }
        },
        "example": {
          "error": {
            "code": "validation_failed",
            "message": "Een of meer velden kloppen niet.",
            "fields": {
              "wachtwoord": "Minstens 10 tekens."
            }
          },
          "request_id": "req_6f1c2a9b"
        }
      },
      "Ok": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          }
        },
        "example": {
          "ok": true
        }
      },
      "Health": {
        "type": "object",
        "example": {
          "status": "ok",
          "version": "1",
          "uptime_seconds": 8123,
          "checks": {
            "database": "ok",
            "disk": "ok",
            "achtergrondtaak": "ok"
          }
        }
      },
      "Status": {
        "type": "object",
        "example": {
          "status": "operational",
          "measured_at": "2026-11-09T10:00:00+00:00",
          "db_latency_ms": 2,
          "queue": 0,
          "disk_used_percent": 41,
          "components": [
            {
              "id": "webapp",
              "name": "Webapp en dashboard",
              "status": "operational",
              "detail": ""
            }
          ]
        }
      },
      "Account": {
        "type": "object",
        "example": {
          "id": "acc_1a2b3c",
          "email": "info@rijschoolnoord.nl",
          "naam": "Rijschool Noord",
          "role": "instructeur",
          "created_at": "2026-11-09T09:00:00+00:00"
        }
      },
      "Sessie": {
        "type": "object",
        "example": {
          "ok": true,
          "account": {
            "id": "acc_1a2b3c",
            "naam": "Rijschool Noord"
          }
        }
      },
      "Mij": {
        "type": "object",
        "example": {
          "account": {
            "id": "acc_1a2b3c",
            "email": "info@rijschoolnoord.nl",
            "naam": "Rijschool Noord"
          },
          "regio": "Utrecht",
          "csrf_token": "…",
          "sessies": 1
        }
      },
      "TokenLijst": {
        "type": "object",
        "example": {
          "tokens": [
            {
              "id": "tok_1",
              "naam": "Koppeling boekhouding",
              "prefix": "a1b2c3",
              "created_at": "2026-11-09T09:00:00+00:00",
              "last_used": null
            }
          ]
        }
      },
      "TokenGeheim": {
        "type": "object",
        "example": {
          "id": "tok_1",
          "naam": "Koppeling boekhouding",
          "secret": "hld_a1b2c3_…",
          "let_op": "Bewaar dit nu; we tonen het niet opnieuw."
        }
      },
      "Leerling": {
        "type": "object",
        "example": {
          "id": "leer_1a2b3c",
          "naam": "Sanne de Groot",
          "email": "",
          "telefoon": "",
          "startdatum": "2026-08-25",
          "created_at": "2026-08-25T09:00:00+00:00"
        }
      },
      "LeerlingLijst": {
        "type": "object",
        "example": {
          "leerlingen": [],
          "aantal": 0,
          "limiet": 25
        }
      },
      "Les": {
        "type": "object",
        "example": {
          "id": "les_1a2b3c",
          "leerling_id": "leer_1a2b3c",
          "leerling": "Sanne de Groot",
          "datum": "2026-11-10",
          "start": "09:00",
          "eind": "10:00",
          "duur": 60,
          "locatie": "Station Utrecht Centraal",
          "notitie": "Rotondes",
          "status": "gepland"
        }
      },
      "LesLijst": {
        "type": "object",
        "example": {
          "lessen": [],
          "week": "2026-11-09"
        }
      },
      "Lesboekje": {
        "type": "object",
        "example": {
          "leerling_id": "leer_1a2b3c",
          "schaal": {
            "0": "Nog niet geoefend",
            "4": "Beheerst, examengereed"
          },
          "groepen": [
            {
              "naam": "Bediening",
              "onderdelen": [
                {
                  "id": "schakelen",
                  "titel": "Schakelen",
                  "niveau": 3
                }
              ]
            }
          ]
        }
      },
      "LesboekjeRegel": {
        "type": "object",
        "example": {
          "onderdeel": "schakelen",
          "niveau": 3,
          "oud": 2
        }
      },
      "Historie": {
        "type": "object",
        "example": {
          "onderdeel": "schakelen",
          "historie": [
            {
              "niveau": 3,
              "oud": 2,
              "ts": "2026-11-09T10:00:00+00:00"
            }
          ]
        }
      },
      "Video": {
        "type": "object",
        "example": {
          "id": "vid_1a2b3c",
          "titel": "Achteruit inparkeren",
          "content_type": "video/mp4",
          "grootte": 4211233,
          "link": "https://video.heldivo.nl/v/AbC123",
          "bekeken_count": 0
        }
      },
      "VideoLijst": {
        "type": "object",
        "example": {
          "videos": [],
          "aantal": 0,
          "limiet": 12,
          "opslag_gebruikt": 0,
          "opslag_limiet": 209715200
        }
      },
      "WachtlijstOk": {
        "type": "object",
        "example": {
          "ok": true,
          "plan": "rijschool",
          "bericht": "Genoteerd. We nemen contact op via het contactformulier zodra Rijschool opengaat."
        }
      },
      "ContactOk": {
        "type": "object",
        "example": {
          "ok": true,
          "ref": "HD-7Q2M4A",
          "bericht": "Ontvangen. We reageren binnen enkele werkdagen."
        }
      }
    }
  },
  "security": [
    {
      "bearer": []
    },
    {
      "sessie": []
    }
  ],
  "paths": {
    "/api/v1/health": {
      "get": {
        "tags": [
          "Systeem"
        ],
        "summary": "Gezondheidscontrole",
        "description": "Controleert of de database schrijf- en leesbaar is, hoeveel vrije schijfruimte er is en hoe lang geleden de achtergrondtaak liep. Geeft 503 bij degradatie.",
        "responses": {
          "200": {
            "description": "Alles in orde",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          },
          "503": {
            "description": "Gedegradeerd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "health",
        "security": []
      }
    },
    "/api/v1/status": {
      "get": {
        "tags": [
          "Systeem"
        ],
        "summary": "Publieke status",
        "description": "Status van de componenten op basis van echte metingen: reactietijd van de database, lengte van de wachtrij en schijfgebruik. Aantallen accounts worden niet prijsgegeven.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Status"
                }
              }
            }
          }
        },
        "operationId": "status",
        "security": []
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "tags": [
          "Systeem"
        ],
        "summary": "Deze specificatie",
        "description": "Geeft deze OpenAPI 3.1-specificatie terug.",
        "responses": {
          "200": {
            "description": "De specificatie"
          }
        },
        "operationId": "openapi",
        "security": []
      }
    },
    "/api/v1/auth/register": {
      "post": {
        "tags": [
          "Authenticatie"
        ],
        "summary": "Account aanmaken",
        "description": "Maakt direct een account aan met de rol instructeur. Er wordt geen e-mail verstuurd; je kunt meteen inloggen. Het wachtwoord is minstens 10 tekens.",
        "responses": {
          "201": {
            "description": "Account aangemaakt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "409": {
            "description": "E-mailadres al in gebruik",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "400": {
            "description": "Verzoek ongeldig",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "429": {
            "description": "Te veel verzoeken",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "register",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "email": "info@rijschoolnoord.nl",
                "wachtwoord": "een-sterk-wachtwoord",
                "naam": "Rijschool Noord"
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/v1/auth/login": {
      "post": {
        "tags": [
          "Authenticatie"
        ],
        "summary": "Inloggen",
        "description": "Logt in met e-mail en wachtwoord. Zet een sessiecookie (__Host-prefix, HttpOnly, Secure, SameSite=Lax). Na vijf mislukte pogingen op hetzelfde e-mailadres volgt 15 minuten lang 429.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Sessie"
                }
              }
            }
          },
          "401": {
            "description": "Onjuiste inloggegevens",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "429": {
            "description": "Te veel pogingen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "login",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "email": "info@rijschoolnoord.nl",
                "wachtwoord": "een-sterk-wachtwoord"
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/v1/auth/logout": {
      "post": {
        "tags": [
          "Authenticatie"
        ],
        "summary": "Uitloggen",
        "description": "Beëindigt de huidige sessie en wist de cookie.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          }
        },
        "operationId": "logout"
      }
    },
    "/api/v1/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Mijn profiel",
        "description": "Geeft je account, je regio (zone) en een CSRF-token voor cookie-sessies.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Mij"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "me"
      },
      "patch": {
        "tags": [
          "Account"
        ],
        "summary": "Profiel bijwerken",
        "description": "Wijzigt je naam.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "me_update",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "naam": "Rijschool Noord Utrecht"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Account"
        ],
        "summary": "Account verwijderen",
        "description": "Verwijdert je account en alle gegevens: leerlingen, lessen, lesboekjes en video's. Bevestig met je wachtwoord. Dit kan niet ongedaan worden gemaakt.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "403": {
            "description": "Wachtwoord onjuist",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "me_delete",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "wachtwoord": "een-sterk-wachtwoord"
              }
            }
          }
        }
      }
    },
    "/api/v1/me/password": {
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Wachtwoord wijzigen",
        "description": "Wijzigt je wachtwoord. Je geeft je huidige en je nieuwe wachtwoord op.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "403": {
            "description": "Huidig wachtwoord onjuist",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "me_password",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "huidig": "een-sterk-wachtwoord",
                "nieuw": "een-nog-sterker-wachtwoord"
              }
            }
          }
        }
      }
    },
    "/api/v1/tokens": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Tokens tonen",
        "description": "Geeft je API-tokens zonder de geheime waarde.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenLijst"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "tokens_list"
      },
      "post": {
        "tags": [
          "Account"
        ],
        "summary": "Token aanmaken",
        "description": "Maakt een persoonlijk Bearer-token. De geheime waarde wordt één keer getoond en daarna alleen als hash bewaard.",
        "responses": {
          "201": {
            "description": "Token aangemaakt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenGeheim"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "tokens_create",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "naam": "Koppeling boekhouding"
              }
            }
          }
        }
      }
    },
    "/api/v1/tokens/{id}": {
      "delete": {
        "tags": [
          "Account"
        ],
        "summary": "Token intrekken",
        "description": "Trekt een token in. Verzoeken met dat token worden daarna geweigerd.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Token niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "tokens_delete",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/leerlingen": {
      "get": {
        "tags": [
          "Leerlingen"
        ],
        "summary": "Leerlingen tonen",
        "description": "Geeft je leerlingen, nieuwste eerst.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeerlingLijst"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "leerlingen_list"
      },
      "post": {
        "tags": [
          "Leerlingen"
        ],
        "summary": "Leerling toevoegen",
        "description": "Voegt een leerling toe. Alleen naam is verplicht; e-mail, telefoon en startdatum zijn optioneel. In de gratis bèta kun je tot 25 leerlingen hebben.",
        "responses": {
          "201": {
            "description": "Toegevoegd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Leerling"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "409": {
            "description": "Leerlinglimiet bereikt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "leerlingen_create",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "naam": "Sanne de Groot",
                "telefoon": "",
                "email": "",
                "startdatum": "2026-11-01"
              }
            }
          }
        }
      }
    },
    "/api/v1/leerlingen/{id}": {
      "get": {
        "tags": [
          "Leerlingen"
        ],
        "summary": "Leerling tonen",
        "description": "Geeft één leerling.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Leerling"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "leerling_get",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "patch": {
        "tags": [
          "Leerlingen"
        ],
        "summary": "Leerling bijwerken",
        "description": "Wijzigt velden van een leerling.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Leerling"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "leerling_update",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "telefoon": "",
                "startdatum": "2026-10-01"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Leerlingen"
        ],
        "summary": "Leerling verwijderen",
        "description": "Verwijdert de leerling, met diens lessen en lesboekje.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "leerling_delete",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/lessen": {
      "get": {
        "tags": [
          "Lessen"
        ],
        "summary": "Lessen tonen",
        "description": "Geeft lessen, optioneel voor één week (maandag als startdatum via de parameter week).",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LesLijst"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "lessen_list",
        "parameters": [
          {
            "name": "week",
            "in": "query",
            "required": false,
            "description": "Maandag van de week, formaat JJJJ-MM-DD.",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "tags": [
          "Lessen"
        ],
        "summary": "Les plannen",
        "description": "Plant een les van 30, 45, 60 of 90 minuten. Als de les in tijd overlapt met een bestaande les, krijg je 409 en wordt er niets opgeslagen.",
        "responses": {
          "201": {
            "description": "Gepland",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Les"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "409": {
            "description": "Overlapt met een bestaande les",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Leerling niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "lessen_create",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "leerling_id": "leer_xxxxxxxx",
                "datum": "2026-11-10",
                "start": "09:00",
                "duur": 60,
                "locatie": "Station Utrecht Centraal",
                "notitie": "Rotondes"
              }
            }
          }
        }
      }
    },
    "/api/v1/lessen/{id}": {
      "get": {
        "tags": [
          "Lessen"
        ],
        "summary": "Les tonen",
        "description": "Geeft één les.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Les"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "les_get",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "patch": {
        "tags": [
          "Lessen"
        ],
        "summary": "Les wijzigen",
        "description": "Wijzigt tijd, duur, locatie, notitie of status. Overlapcontrole geldt opnieuw.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Les"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "409": {
            "description": "Overlapt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "les_update",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "start": "10:30",
                "duur": 90,
                "status": "afgerond"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Lessen"
        ],
        "summary": "Les verwijderen",
        "description": "Verwijdert een les.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "les_delete",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/leerlingen/{id}/lesboekje": {
      "get": {
        "tags": [
          "Lesboekje"
        ],
        "summary": "Lesboekje tonen",
        "description": "Geeft alle onderdelen met het huidige niveau (0–4) voor deze leerling.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Lesboekje"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "lesboekje_get",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/leerlingen/{id}/lesboekje/{onderdeel}": {
      "put": {
        "tags": [
          "Lesboekje"
        ],
        "summary": "Niveau instellen",
        "description": "Zet het niveau van één onderdeel (0 t/m 4). De vorige waarde wordt met datum in de geschiedenis bewaard.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LesboekjeRegel"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "lesboekje_set",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "onderdeel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "niveau": 3
              }
            }
          }
        }
      }
    },
    "/api/v1/leerlingen/{id}/lesboekje/{onderdeel}/historie": {
      "get": {
        "tags": [
          "Lesboekje"
        ],
        "summary": "Geschiedenis van een onderdeel",
        "description": "Geeft de niveauwijzigingen van één onderdeel met datum en tijd.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Historie"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "lesboekje_historie",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "onderdeel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/v1/videos": {
      "get": {
        "tags": [
          "Video's"
        ],
        "summary": "Video's tonen",
        "description": "Geeft je instructievideo's met de publieke link.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VideoLijst"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "videos_list"
      },
      "post": {
        "tags": [
          "Video's"
        ],
        "summary": "Video uploaden",
        "description": "Uploadt een mp4- of webm-bestand tot 50 MB als multipart/form-data (veld 'bestand', optioneel 'titel'). Tot 12 video's en 200 MB per account in de bèta.",
        "responses": {
          "201": {
            "description": "Geüpload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Video"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "409": {
            "description": "Limiet bereikt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "413": {
            "description": "Bestand te groot",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "415": {
            "description": "Alleen mp4 en webm",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "videos_create",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "bestand": {
                    "type": "string",
                    "format": "binary"
                  },
                  "titel": {
                    "type": "string"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/videos/{id}": {
      "get": {
        "tags": [
          "Video's"
        ],
        "summary": "Video tonen",
        "description": "Geeft de gegevens van één video.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Video"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "video_get",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "delete": {
        "tags": [
          "Video's"
        ],
        "summary": "Video verwijderen",
        "description": "Verwijdert de video en het bestand van de schijf.",
        "responses": {
          "200": {
            "description": "Gelukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Ok"
                }
              }
            }
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "404": {
            "description": "Niet gevonden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "video_delete",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/v/{token}": {
      "get": {
        "tags": [
          "Video's"
        ],
        "summary": "Publieke lespagina",
        "description": "Openbare HTML-pagina met de video voor de leerling, zonder account. De bijbehorende mediastroom staat op /v/{token}/media en ondersteunt Range.",
        "responses": {
          "200": {
            "description": "HTML-pagina"
          },
          "404": {
            "description": "Onbekende of ingetrokken link"
          }
        },
        "operationId": "video_public",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": []
      }
    },
    "/v/{token}/media": {
      "get": {
        "tags": [
          "Video's"
        ],
        "summary": "Mediastroom",
        "description": "Levert de videobytes met het juiste Content-Type en ondersteuning voor Range (206). Het eerste verzoek markeert de video als bekeken.",
        "responses": {
          "200": {
            "description": "Volledige stroom"
          },
          "206": {
            "description": "Gedeeltelijke inhoud"
          },
          "404": {
            "description": "Onbekend"
          },
          "416": {
            "description": "Ongeldig bereik"
          }
        },
        "operationId": "video_media",
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": []
      }
    },
    "/api/v1/export.zip": {
      "get": {
        "tags": [
          "Export"
        ],
        "summary": "Alles als zip",
        "description": "Levert een zip met je leerlingen, lessen en lesboekjes als CSV, plus een overzicht van je video's.",
        "responses": {
          "200": {
            "description": "Zip-bestand"
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "export_zip"
      }
    },
    "/api/v1/export/leerlingen.csv": {
      "get": {
        "tags": [
          "Export"
        ],
        "summary": "Leerlingen als CSV",
        "description": "Levert je leerlingen als CSV.",
        "responses": {
          "200": {
            "description": "CSV"
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "export_leerlingen"
      }
    },
    "/api/v1/export/lessen.csv": {
      "get": {
        "tags": [
          "Export"
        ],
        "summary": "Lessen als CSV",
        "description": "Levert je lessen als CSV.",
        "responses": {
          "200": {
            "description": "CSV"
          },
          "401": {
            "description": "Niet ingelogd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "export_lessen"
      }
    },
    "/api/v1/waitlist": {
      "post": {
        "tags": [
          "Wachtlijst"
        ],
        "summary": "Op de wachtlijst",
        "description": "Zet je op de wachtlijst voor een betaald pakket. Er wordt niets afgeschreven; we nemen contact op via het contactformulier zodra het pakket opengaat.",
        "responses": {
          "201": {
            "description": "Genoteerd",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WachtlijstOk"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "429": {
            "description": "Te veel verzoeken",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "waitlist",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "email": "info@rijschoolnoord.nl",
                "plan": "rijschool",
                "rijschool": "Rijschool Noord",
                "plaats": "Utrecht",
                "aantal": "3",
                "bericht": ""
              }
            }
          }
        },
        "security": []
      }
    },
    "/api/v1/contact": {
      "post": {
        "tags": [
          "Contact"
        ],
        "summary": "Contactformulier",
        "description": "Stuurt een bericht naar Heldivo. Je krijgt een referentienummer terug. Maximaal drie berichten per uur per e-mailadres.",
        "responses": {
          "201": {
            "description": "Ontvangen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ContactOk"
                }
              }
            }
          },
          "422": {
            "description": "Validatie mislukt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          },
          "429": {
            "description": "Te veel berichten",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fout"
                }
              }
            }
          }
        },
        "operationId": "contact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              },
              "example": {
                "naam": "Sanne de Groot",
                "email": "sanne@example.nl",
                "onderwerp": "Vraag over de bèta",
                "bericht": "Graag meer informatie."
              }
            }
          }
        },
        "security": []
      }
    }
  }
}