openapi: "3.1.0"
info:
  title: Orbinaut Public REST API
  version: "1"
  summary: Lesende öffentliche REST-API von Orbinaut.
  description: |
    Lesender Server-zu-Server-Vertrag (Beta). Authentifizierung über Bearer-Keys
    mit Präfix `orb_api_`, erzeugt unter Integrationen.

    Scopes: `courses:read`, `bookings:read`, `participants:read`
    (`participants:read` setzt `bookings:read` voraus).

    Diese Spezifikation ist die Quelle der Wahrheit für `/api/public/v1/`.
    Die Widget-API unter `/api/widget/v1/` ist ein getrennter Vertrag.
    Kalenderfeeds unter `/api/calendar/v1/{token}.ics` sind iCal, kein JSON.
  contact:
    name: Orbinaut
    url: https://orbinaut.ccl-dev.com
  license:
    name: Proprietary
externalDocs:
  description: Orbinaut REST-API-Dokumentation
  url: https://docs.orbinaut.ccl-dev.com/rest-api/
servers:
  - url: https://api.orbinaut.ccl-dev.com
    description: Produktions-API
tags:
  - name: courses
    description: Veröffentlichte Kurse und geplante Termine
  - name: bookings
    description: Buchungen des Key-Workspaces
security:
  - bearerAuth: []
paths:
  /api/public/v1/courses:
    get:
      operationId: listPublicCourses
      tags: [courses]
      summary: Veröffentlichte Kurse lesen
      description: |
        Liefert veröffentlichte Kursdurchführungen mit geplanten Sitzungen
        des Workspaces, der am API-Key hängt. Erfordert `courses:read`.
        Keine Pagination.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Liste veröffentlichter Kurse
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPrivateNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CourseListResponse"
              examples:
                default:
                  value:
                    data:
                      - id: course-run-id
                        title: Aquarellkurs
                        description: Einführung ins Aquarell
                        sessions:
                          - id: session-id
                            startsAt: "2026-09-01T17:00:00.000Z"
                            durationMinutes: 90
                            timeZone: Europe/Berlin
                            location: Studio Mitte
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
  /api/public/v1/bookings:
    get:
      operationId: listPublicBookings
      tags: [bookings]
      summary: Buchungen lesen
      description: |
        Liefert die bis zu 100 neuesten Buchungen des Key-Workspaces,
        sortiert nach `createdAt` absteigend. Erfordert `bookings:read`.

        Es gibt keine Cursor-Pagination und keine Filterparameter.
        Das Objekt `participant` mit `name` und `email` ist nur enthalten,
        wenn der Key zusätzlich `participants:read` besitzt.
      security:
        - bearerAuth: []
      responses:
        "200":
          description: Liste der neuesten Buchungen, höchstens 100
          headers:
            Cache-Control:
              $ref: "#/components/headers/CacheControlPrivateNoStore"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BookingListResponse"
              examples:
                withoutParticipants:
                  summary: Ohne Scope participants:read
                  value:
                    data:
                      - id: booking-id
                        courseRunId: course-run-id
                        source: widget
                        status: confirmed
                        createdAt: "2026-08-17T12:00:00.000Z"
                        updatedAt: "2026-08-17T12:00:00.000Z"
                withParticipants:
                  summary: Mit Scope participants:read
                  value:
                    data:
                      - id: booking-id
                        courseRunId: course-run-id
                        source: widget
                        status: confirmed
                        createdAt: "2026-08-17T12:00:00.000Z"
                        updatedAt: "2026-08-17T12:00:00.000Z"
                        participant:
                          name: Erika Mustermann
                          email: erika@example.test
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/TooManyRequests"
webhooks:
  bookingCreated:
    post:
      operationId: bookingCreatedWebhook
      summary: booking.created
      description: |
        Ausgehendes Event nach persistierter Buchung. Zustellung per HTTPS
        POST, Header `x-orbinaut-signature` im Format `v1=<hex-hmac>`,
        höchstens 8 Versuche. `data.participant` nur mit Webhook-Scope
        `participants:read`.
      parameters:
        - name: x-orbinaut-event-id
          in: header
          required: true
          schema:
            type: string
        - name: x-orbinaut-timestamp
          in: header
          required: true
          schema:
            type: string
            description: Unix-Zeit in Sekunden
        - name: x-orbinaut-signature
          in: header
          required: true
          schema:
            type: string
            pattern: "^v1=[0-9a-f]+$"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BookingCreatedEvent"
      responses:
        "200":
          description: Empfänger hat das Event akzeptiert
components:
  headers:
    CacheControlPrivateNoStore:
      description: Antworten dürfen nicht zwischengespeichert werden.
      required: true
      schema:
        type: string
        const: "private, no-store"
    RetryAfter:
      description: Sekunden bis zur nächsten erlaubten Anfrage.
      required: true
      schema:
        type: integer
        const: 60
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: orb_api_
      description: |
        API-Key aus Integrationen. Erforderliche Scopes je Endpunkt:
        `courses:read`, `bookings:read`, optional `participants:read`.
        Limit 120 Anfragen / 60 Sekunden / Key.
  schemas:
    ErrorBody:
      type: object
      additionalProperties: false
      required: [error]
      properties:
        error:
          type: object
          additionalProperties: false
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - invalid_api_key
                - missing_scope
                - rate_limit_exceeded
            message:
              type: string
    Session:
      type: object
      additionalProperties: false
      required:
        - id
        - startsAt
        - durationMinutes
        - timeZone
        - location
      properties:
        id:
          type: string
        startsAt:
          type: string
          format: date-time
        durationMinutes:
          type: integer
          minimum: 1
        timeZone:
          type: string
          examples: [Europe/Berlin]
        location:
          type: string
    Course:
      type: object
      additionalProperties: false
      required: [id, title, description, sessions]
      properties:
        id:
          type: string
          description: ID der Kursdurchführung
        title:
          type: string
        description:
          type: string
        sessions:
          type: array
          items:
            $ref: "#/components/schemas/Session"
    CourseListResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Course"
    Participant:
      type: object
      additionalProperties: false
      required: [name, email]
      properties:
        name:
          type: string
        email:
          type: string
          format: email
    Booking:
      type: object
      additionalProperties: false
      required:
        - id
        - courseRunId
        - source
        - status
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
        courseRunId:
          type: string
        source:
          type: string
          enum: [admin, public_page, widget, api]
        status:
          type: string
          enum: [pending, confirmed, waitlisted, cancelled, expired]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        participant:
          $ref: "#/components/schemas/Participant"
          description: Nur vorhanden, wenn der Key `participants:read` besitzt.
    BookingListResponse:
      type: object
      additionalProperties: false
      required: [data]
      properties:
        data:
          type: array
          maxItems: 100
          items:
            $ref: "#/components/schemas/Booking"
    BookingCreatedEvent:
      type: object
      additionalProperties: false
      required: [id, type, occurredAt, data]
      properties:
        id:
          type: string
        type:
          type: string
          const: booking.created
        occurredAt:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: false
          required: [booking, course]
          properties:
            booking:
              type: object
              additionalProperties: false
              required: [id, source, status]
              properties:
                id:
                  type: string
                source:
                  type: string
                status:
                  type: string
            course:
              type: object
              additionalProperties: false
              required: [id, title]
              properties:
                id:
                  type: string
                title:
                  type: string
            participant:
              $ref: "#/components/schemas/Participant"
  responses:
    Unauthorized:
      description: Fehlender, ungültiger oder widerrufener API-Key
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            error:
              code: invalid_api_key
              message: A valid API key is required
    Forbidden:
      description: Gültiger Key ohne den für den Endpunkt nötigen Scope
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            error:
              code: missing_scope
              message: The bookings:read scope is required
    TooManyRequests:
      description: Mehr als 120 Anfragen in 60 Sekunden für diesen Key
      headers:
        Retry-After:
          $ref: "#/components/headers/RetryAfter"
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorBody"
          example:
            error:
              code: rate_limit_exceeded
              message: API rate limit exceeded
