openapi: 3.1.0
info:
  title: Passdesk Public API
  version: 1.0.0
  summary: Programmatic access to your driving school's data on Passdesk.
  description: |
    The Passdesk API gives Scale-tier schools programmatic access to their own
    students, schedules, products, purchases, progress, instructors, and
    employees. v1 is read-mostly — listings and detail views across every
    primary resource — with focused write operations to follow in v1.1.

    All requests are authenticated with a bearer API key. Keys are minted from
    the Passdesk app at `/client/:id/api-keys` and can be rotated or revoked at
    any time. Rotation immediately invalidates the previous key.
servers:
  - url: https://passdesk.co.uk/api/v1
    description: Production
  - url: http://localhost:4000/api/v1
    description: Local development

security:
  - bearerAuth: []

paths:
  /me:
    get:
      summary: Verify the bearer key
      description: |
        Lightweight pre-flight that returns the tenant id, the calling key id,
        and whether the key is read-only. Useful for sanity-checking a key
        before exercising any of the resource endpoints.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      clientId: { type: integer, example: 42 }
                      apiKeyId: { type: integer, example: 7 }
                      readOnly: { type: boolean, example: false }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }

  /students:
    get:
      summary: List students
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ListEnvelope'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Student' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/PaymentRequired' }
  /students/{id}:
    parameters: [{ $ref: '#/components/parameters/Id' }]
    get:
      summary: Get a student by id
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ItemEnvelope'
                  - properties:
                      data: { $ref: '#/components/schemas/Student' }
        '404': { $ref: '#/components/responses/NotFound' }

  /schedules:
    get:
      summary: List lessons
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - in: query
          name: from
          schema: { type: string, format: date-time }
          description: Filter to lessons on or after this date.
        - in: query
          name: to
          schema: { type: string, format: date-time }
          description: Filter to lessons on or before this date.
        - in: query
          name: status
          schema: { type: string }
          example: COMPLETED
        - in: query
          name: studentId
          schema: { type: integer }
        - in: query
          name: instructorId
          schema: { type: integer }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ListEnvelope'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Schedule' }
  /schedules/{id}:
    parameters: [{ $ref: '#/components/parameters/Id' }]
    get:
      summary: Get a lesson by id
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ItemEnvelope'
                  - properties:
                      data: { $ref: '#/components/schemas/Schedule' }
        '404': { $ref: '#/components/responses/NotFound' }

  /products:
    get:
      summary: List products
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - in: query
          name: sellable
          schema: { type: string, enum: ['true'] }
          description: When set to `true`, only sellable-online products are returned.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ListEnvelope'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Product' }
  /products/{id}:
    parameters: [{ $ref: '#/components/parameters/Id' }]
    get:
      summary: Get a product by id
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }

  /purchases:
    get:
      summary: List purchases
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - in: query
          name: studentId
          schema: { type: integer }
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: kind
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ListEnvelope'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Purchase' }
  /purchases/{id}:
    parameters: [{ $ref: '#/components/parameters/Id' }]
    get:
      summary: Get a purchase by id
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }

  /progress:
    get:
      summary: List progress entries
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - in: query
          name: studentId
          schema: { type: integer }
        - in: query
          name: from
          schema: { type: string, format: date-time }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ListEnvelope'
                  - properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Progress' }
  /progress/{id}:
    parameters: [{ $ref: '#/components/parameters/Id' }]
    get:
      summary: Get a progress entry by id
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }

  /instructors:
    get:
      summary: List instructors
      responses:
        '200': { description: OK }
  /instructors/{id}:
    parameters: [{ $ref: '#/components/parameters/Id' }]
    get:
      summary: Get an instructor by id
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }

  /employees:
    get:
      summary: List office employees (non-instructor staff)
      responses:
        '200': { description: OK }
  /employees/{id}:
    parameters: [{ $ref: '#/components/parameters/Id' }]
    get:
      summary: Get an employee by id
      responses:
        '200': { description: OK }
        '404': { $ref: '#/components/responses/NotFound' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: pdsk_live_<prefix>_<secret>
      description: |
        Issue from the Passdesk app at `/client/:id/api-keys`. The full token
        is shown once at creation. Send as `Authorization: Bearer <token>` on
        every request.

  parameters:
    Id:
      in: path
      name: id
      required: true
      schema: { type: integer }
    Limit:
      in: query
      name: limit
      schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
    Offset:
      in: query
      name: offset
      schema: { type: integer, minimum: 0, default: 0 }

  responses:
    Unauthorized:
      description: Missing, malformed, revoked, or expired API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    PaymentRequired:
      description: Tenant is not on the Scale plan, or subscription is inactive.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Resource does not exist or is not in the caller's tenant.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      properties:
        error: { type: string, example: Invalid API key }
    ListEnvelope:
      type: object
      properties:
        data: { type: array, items: {} }
        pagination:
          type: object
          properties:
            limit: { type: integer }
            offset: { type: integer }
            total: { type: integer }
    ItemEnvelope:
      type: object
      properties:
        data: {}

    Student:
      type: object
      properties:
        id: { type: integer }
        firstName: { type: string }
        lastName: { type: string }
        email: { type: string, nullable: true }
        phone: { type: string, nullable: true }
        address: { type: string, nullable: true }
        dateOfBirth: { type: string, format: date, nullable: true }
        gender: { type: string, nullable: true }
        licenseCategory: { type: string }
        profileComplete: { type: boolean }
        provisionalLicenceNumber: { type: string, nullable: true }
        theoryTestPassDate: { type: string, format: date, nullable: true }
        practicalTestDate: { type: string, format: date, nullable: true }
        practicalTestResult: { type: string, nullable: true }
        testCentreId: { type: integer, nullable: true }
        guardianName: { type: string, nullable: true }
        guardianPhone: { type: string, nullable: true }
        transmission: { type: string, nullable: true }

    Schedule:
      type: object
      properties:
        id: { type: integer }
        date: { type: string, format: date-time }
        startTime: { type: string, format: date-time }
        endTime: { type: string, format: date-time }
        studentId: { type: integer, nullable: true }
        instructorId: { type: integer }
        status: { type: string, example: SCHEDULED }
        lessonType: { type: string, nullable: true }
        billableMinutes: { type: integer, nullable: true }
        canceledAt: { type: string, format: date-time, nullable: true }

    Product:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        description: { type: string, nullable: true }
        price: { type: number, description: 'Major currency units (e.g. £35.00 = 35.00)' }
        totalMinutes: { type: integer }
        sellableOnline: { type: boolean }

    Purchase:
      type: object
      properties:
        id: { type: integer }
        studentId: { type: integer }
        productId: { type: integer }
        amount: { type: number }
        status: { type: string, example: CONFIRMED }
        kind: { type: string, example: PACKAGE }
        confirmedAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }

    Progress:
      type: object
      properties:
        id: { type: integer }
        studentId: { type: integer }
        date: { type: string, format: date-time }
        competency: { type: string, example: vehicle_control }
        rating: { type: integer, minimum: 1, maximum: 5 }
        category: { type: string, example: practical }
        notes: { type: string, nullable: true }

x-rate-limits:
  description: |
    Per-key buckets, refreshed every 60 seconds:
      - 600 reads/min on GET / HEAD
      - 120 writes/min on POST / PUT / PATCH / DELETE
    Each Passdesk-issued key gets its own bucket — partners can be
    isolated by minting one key per integration.
    Standard `RateLimit-Limit` and `RateLimit-Remaining` headers are
    returned on every response. Overage produces a 429 with `Retry-After`.

x-pagination:
  description: |
    All list endpoints accept `?limit` (max 200) and `?offset` (default 0).
    The total count is included in `pagination.total` of the response body
    and as the `X-Total-Count` response header.
