openapi: 3.1.0
info:
  title: Werken aan Boord API
  version: 0.3.0
  description: |
    API for permanent jobs, relief work, availability, bookings, and watches.
    Public reads need no account. Private reads and all writes need an account.
    Browser sessions and OAuth 2.1 bearer tokens identify each record owner.
servers:
  - url: https://werkenaanboord.nl
    description: Production
  - url: http://localhost:3000
    description: Local development
paths:
  /v1/taxonomy:
    get:
      operationId: getTaxonomy
      summary: Get canonical terms and aliases
      responses:
        '200': { $ref: '#/components/responses/DataObject' }

  /v1/jobs:
    get:
      operationId: searchJobs
      summary: Search active public jobs
      parameters:
        - $ref: '#/components/parameters/Role'
        - { name: work_type, in: query, schema: { $ref: '#/components/schemas/JobType' } }
        - { name: available_from, in: query, schema: { type: string, format: date } }
        - { name: available_to, in: query, schema: { type: string, format: date } }
        - { name: engagement, in: query, schema: { $ref: '#/components/schemas/Engagement' } }
        - { name: sailing_area, in: query, schema: { type: string } }
        - { name: certificate, in: query, description: Exact certificate name (case-insensitive), schema: { type: string } }
        - { name: updated_since, in: query, schema: { type: string, format: date-time } }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: cursor, in: query, schema: { type: string } }
      responses:
        '200':
          description: Matching jobs
          content:
            application/json: { schema: { $ref: '#/components/schemas/JobList' } }
        '422': { $ref: '#/components/responses/ValidationError' }
    post:
      operationId: publishJob
      summary: Publish the exact immutable job draft
      security:
        - OAuth2: ['jobs:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/PublishDraft' } }
      responses:
        '201': { $ref: '#/components/responses/JobMutation' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /v1/jobs/preview:
    post:
      operationId: prepareJob
      summary: Create an immutable private job draft
      security:
        - OAuth2: ['jobs:write']
        - SessionCookie: []
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/JobInput' } }
      responses:
        '200':
          description: Normalized preview with an immutable draft identifier
          content:
            application/json: { schema: { $ref: '#/components/schemas/JobDraftResponse' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /v1/employer-verifications:
    post:
      operationId: requestEmployerVerification
      summary: Verify an employer with the signed-in account email domain
      security:
        - OAuth2: ['jobs:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [employer_name, website_url]
              properties:
                employer_name: { type: string, maxLength: 160 }
                website_url: { type: string, format: uri, maxLength: 2048 }
      responses:
        '201': { $ref: '#/components/responses/DataObject' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /v1/jobs/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getJob
      summary: Get one active public job
      responses:
        '200':
          description: Job
          content:
            application/json: { schema: { $ref: '#/components/schemas/JobResponse' } }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      operationId: updateJob
      summary: Update or change the status of an owned job
      description: Use a body with only status to activate, pause, or close a job.
      security:
        - OAuth2: ['jobs:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/JobPatch'
                - $ref: '#/components/schemas/JobStatusPatch'
      responses:
        '200': { $ref: '#/components/responses/JobMutation' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteJob
      summary: Hide an owned job with deleted status
      security:
        - OAuth2: ['jobs:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { $ref: '#/components/responses/JobMutation' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/workers/availability/preview:
    post:
      operationId: prepareWorkerAvailability
      summary: Create one immutable profile and availability draft
      security:
        - OAuth2: ['profile:write']
        - SessionCookie: []
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/WorkerAvailabilityInput' } }
      responses:
        '200':
          description: Normalized preview with an immutable draft identifier
          content:
            application/json: { schema: { $ref: '#/components/schemas/WorkerAvailabilityDraftResponse' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /v1/workers/availability:
    post:
      operationId: publishWorkerAvailability
      summary: Publish the exact profile and availability draft atomically
      description: Both records are saved, or neither record is saved.
      security:
        - OAuth2: ['profile:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/PublishDraft' } }
      responses:
        '201':
          description: Published worker and availability
          content:
            application/json: { schema: { $ref: '#/components/schemas/WorkerAvailabilityResponse' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/DraftExpired' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /v1/workers/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    get:
      operationId: getWorkerProfile
      summary: Get one public worker profile and its availability
      responses:
        '200':
          description: Worker and availability
          content:
            application/json: { schema: { $ref: '#/components/schemas/WorkerResponse' } }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      operationId: updateWorker
      summary: Update an owned worker profile
      security:
        - OAuth2: ['profile:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/WorkerPatch' } }
      responses:
        '200': { $ref: '#/components/responses/WorkerMutation' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteWorker
      summary: Delete an owned profile, availability, and related booking requests
      security:
        - OAuth2: ['profile:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Deleted identifiers
          content:
            application/json: { schema: { $ref: '#/components/schemas/DeletedWorkerResponse' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/availability:
    get:
      operationId: findAvailableWorkers
      summary: Find workers who cover the full requested period
      parameters:
        - $ref: '#/components/parameters/RequiredRole'
        - { name: needed_from, in: query, required: true, schema: { type: string, format: date } }
        - { name: needed_to, in: query, required: true, schema: { type: string, format: date } }
      responses:
        '200':
          description: Matching availability with worker profiles
          content:
            application/json: { schema: { $ref: '#/components/schemas/AvailabilityList' } }
        '422': { $ref: '#/components/responses/ValidationError' }

  /v1/availability/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateAvailability
      summary: Update owned availability
      security:
        - OAuth2: ['profile:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/AvailabilityPatch' } }
      responses:
        '200': { $ref: '#/components/responses/AvailabilityMutation' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteAvailability
      summary: Delete owned availability
      security:
        - OAuth2: ['profile:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { $ref: '#/components/responses/AvailabilityMutation' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/booking-requests:
    post:
      operationId: requestBooking
      summary: Create an owned booking request
      security:
        - OAuth2: ['booking:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/BookingRequestInput' } }
      responses:
        '201': { $ref: '#/components/responses/DataObject' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /v1/watches:
    post:
      operationId: createJobWatch
      summary: Create an owned job watch
      security:
        - OAuth2: ['watches:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/WatchInput' } }
      responses:
        '201': { $ref: '#/components/responses/DataObject' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /v1/watches/{id}:
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      operationId: updateJobWatch
      summary: Update an owned job watch
      security:
        - OAuth2: ['watches:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json: { schema: { $ref: '#/components/schemas/WatchPatch' } }
      responses:
        '200': { $ref: '#/components/responses/DataObject' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      operationId: deleteJobWatch
      summary: Delete an owned job watch
      security:
        - OAuth2: ['watches:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200': { $ref: '#/components/responses/DataObject' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/watches/{id}/matches:
    get:
      operationId: getJobWatchMatches
      summary: Get current matches for an owned watch
      security:
        - OAuth2: ['watches:write']
        - SessionCookie: []
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          description: Current matches
          content:
            application/json: { schema: { $ref: '#/components/schemas/JobList' } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /v1/me:
    get:
      operationId: listMyRecords
      summary: List records visible to the authenticated owner
      description: Includes owned records and requests sent to an owned worker.
      security:
        - OAuth2: []
        - SessionCookie: []
      responses:
        '200':
          description: Owned jobs, workers, availability, booking requests, and watches
          content:
            application/json: { schema: { $ref: '#/components/schemas/MyRecordsResponse' } }
        '401': { $ref: '#/components/responses/Unauthorized' }

components:
  securitySchemes:
    OAuth2:
      type: oauth2
      description: OAuth 2.1 authorization code flow with PKCE.
      flows:
        authorizationCode:
          authorizationUrl: https://werkenaanboord.nl/api/auth/oauth2/authorize
          tokenUrl: https://werkenaanboord.nl/api/auth/oauth2/token
          scopes:
            jobs:write: Create and manage owned jobs.
            profile:write: Create and manage owned profiles and availability.
            booking:write: Create owned booking requests.
            watches:write: Create and manage owned watches.
    SessionCookie:
      type: apiKey
      in: cookie
      name: better-auth.session_token
      description: Browser session. State-changing requests also need a same-origin Origin header.

  parameters:
    Id:
      name: id
      in: path
      required: true
      description: Stable identifier. Job and worker routes also accept a slug.
      schema: { type: string }
    Role:
      name: role
      in: query
      schema: { type: string }
      example: schipper
    RequiredRole:
      name: role
      in: query
      required: true
      schema: { type: string }
      example: schipper
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: One unique value for one exact mutation. Use 8 through 200 characters.
      schema: { type: string, minLength: 8, maxLength: 200 }

  schemas:
    JobType: { type: string, enum: [permanent, relief] }
    Engagement: { type: string, enum: [employee, freelance, either] }
    JobStatus: { type: string, enum: [active, paused, closed, deleted] }
    JobInput:
      type: object
      required: [type, role, employer_name, start_date]
      properties:
        type: { $ref: '#/components/schemas/JobType' }
        role: { type: string }
        slug: { type: string, pattern: '^[a-z0-9-]{3,100}$' }
        title: { type: string }
        employer_name: { type: string }
        vessel_name: { type: string }
        vessel_type: { type: string }
        start_date: { type: string, format: date }
        end_date: { type: [string, 'null'], format: date, description: Required for relief work. }
        schedule: { type: string }
        sailing_area: { type: string }
        engagement: { $ref: '#/components/schemas/Engagement' }
        salary_text: { type: string }
        required_certificates: { type: array, items: { type: string } }
        description: { type: string }
        source_name: { type: string }
        source_url: { type: string, format: uri }
        contact_url: { type: string, format: uri }
        date_posted: { type: string, format: date }
        valid_through: { type: [string, 'null'], format: date }
    JobPatch:
      type: object
      properties:
        type: { $ref: '#/components/schemas/JobType' }
        role: { type: string }
        slug: { type: string, pattern: '^[a-z0-9-]{3,100}$' }
        title: { type: string }
        employer_name: { type: string }
        vessel_name: { type: string }
        vessel_type: { type: string }
        start_date: { type: string, format: date }
        end_date: { type: [string, 'null'], format: date }
        schedule: { type: string }
        sailing_area: { type: string }
        engagement: { $ref: '#/components/schemas/Engagement' }
        salary_text: { type: string }
        required_certificates: { type: array, items: { type: string } }
        description: { type: string }
        source_name: { type: string }
        source_url: { type: string, format: uri }
        contact_url: { type: string, format: uri }
        date_posted: { type: string, format: date }
        valid_through: { type: [string, 'null'], format: date }
    JobStatusPatch:
      type: object
      additionalProperties: false
      required: [status]
      properties:
        status: { type: string, enum: [active, paused, closed] }
    Job:
      allOf:
        - $ref: '#/components/schemas/JobInput'
        - type: object
          required: [id, status, created_at, updated_at, job_url, markdown_url]
          properties:
            id: { type: string }
            status: { $ref: '#/components/schemas/JobStatus' }
            match_start_date: { type: string, format: date }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
            last_checked_at: { type: string, format: date-time }
            source_check_status: { type: string, enum: [reachable, missing, unknown] }
            employer_verification_status: { type: string, enum: [verified, unverified] }
            moderation_status: { type: string, enum: [approved, pending_review, rejected] }
            job_url: { type: string, format: uri, readOnly: true }
            markdown_url: { type: string, format: uri, readOnly: true }
    WorkerInput:
      type: object
      required: [display_name, slug, roles]
      properties:
        display_name: { type: string }
        slug: { type: string, pattern: '^[a-z0-9-]{3,80}$' }
        roles: { type: array, minItems: 1, items: { type: string } }
        bio: { type: string }
        home_port: { type: string }
        sailing_areas: { type: array, items: { type: string } }
        certificates: { type: array, items: { type: string } }
        engagement: { $ref: '#/components/schemas/Engagement' }
        booking_url: { type: string, format: uri }
    WorkerPatch:
      type: object
      properties:
        display_name: { type: string }
        slug: { type: string, pattern: '^[a-z0-9-]{3,80}$' }
        roles: { type: array, minItems: 1, items: { type: string } }
        bio: { type: string }
        home_port: { type: string }
        sailing_areas: { type: array, items: { type: string } }
        certificates: { type: array, items: { type: string } }
        engagement: { $ref: '#/components/schemas/Engagement' }
        booking_url: { type: string, format: uri }
    Worker:
      allOf:
        - $ref: '#/components/schemas/WorkerInput'
        - type: object
          required: [id, created_at, updated_at]
          properties:
            id: { type: string }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
            availability: { type: array, items: { $ref: '#/components/schemas/Availability' } }
    AvailabilityInput:
      type: object
      required: [role, start_date, end_date]
      properties:
        role: { type: string }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        engagement: { $ref: '#/components/schemas/Engagement' }
        notes: { type: string }
    AvailabilityPatch:
      type: object
      properties:
        role: { type: string }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        engagement: { $ref: '#/components/schemas/Engagement' }
        notes: { type: string }
    Availability:
      allOf:
        - $ref: '#/components/schemas/AvailabilityInput'
        - type: object
          required: [id, worker_id, created_at, updated_at]
          properties:
            id: { type: string }
            worker_id: { type: string }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
    WorkerAvailabilityInput:
      type: object
      additionalProperties: false
      required: [worker, availability]
      properties:
        worker: { $ref: '#/components/schemas/WorkerInput' }
        availability: { $ref: '#/components/schemas/AvailabilityInput' }
    PublishDraft:
      type: object
      additionalProperties: false
      required: [draft_id]
      properties:
        draft_id: { type: string, format: uuid }
    BookingRequestInput:
      type: object
      required: [worker_id, role, start_date, end_date, employer_name, contact]
      properties:
        worker_id: { type: string }
        role: { type: string }
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        employer_name: { type: string }
        vessel_name: { type: string }
        message: { type: string }
        contact: { type: string }
    WatchInput:
      type: object
      required: [role, available_from, destination]
      properties:
        role: { type: string }
        work_type: { $ref: '#/components/schemas/JobType' }
        available_from: { type: string, format: date }
        available_to: { type: string, format: date }
        engagement: { $ref: '#/components/schemas/Engagement' }
        sailing_area: { type: string }
        channel: { type: string, enum: [email, webhook], default: webhook }
        destination: { type: string }
    WatchPatch:
      type: object
      properties:
        role: { type: string }
        work_type: { $ref: '#/components/schemas/JobType' }
        available_from: { type: string, format: date }
        available_to: { type: string, format: date }
        engagement: { $ref: '#/components/schemas/Engagement' }
        sailing_area: { type: string }
        channel: { type: string, enum: [email, webhook] }
        destination: { type: string }
        active: { type: boolean }
    JobDraftResponse:
      type: object
      required: [data, confirmation_required]
      properties:
        data:
          type: object
          required: [draft_id, preview, expires_at]
          properties:
            draft_id: { type: string, format: uuid }
            preview: { $ref: '#/components/schemas/JobInput' }
            expires_at: { type: string, format: date-time }
        confirmation_required: { const: true }
    WorkerAvailabilityDraftResponse:
      type: object
      required: [data, confirmation_required]
      properties:
        data:
          type: object
          required: [draft_id, preview, expires_at]
          properties:
            draft_id: { type: string, format: uuid }
            preview: { $ref: '#/components/schemas/WorkerAvailabilityInput' }
            expires_at: { type: string, format: date-time }
        confirmation_required: { const: true }
    JobResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/Job' }
    JobList:
      type: object
      required: [data, count]
      properties:
        data: { type: array, items: { $ref: '#/components/schemas/Job' } }
        count: { type: integer }
        query: { type: object, additionalProperties: true }
        next_cursor: { type: [string, 'null'] }
        watch_id: { type: string }
    WorkerResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/Worker' }
    AvailabilityResponse:
      type: object
      required: [data]
      properties:
        data: { $ref: '#/components/schemas/Availability' }
    WorkerAvailabilityResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [worker, availability]
          properties:
            worker: { $ref: '#/components/schemas/Worker' }
            availability: { $ref: '#/components/schemas/Availability' }
    DeletedWorkerResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [worker_id, availability_ids]
          properties:
            worker_id: { type: string }
            availability_ids: { type: array, items: { type: string } }
    AvailabilityList:
      type: object
      required: [data, count, query]
      properties:
        data:
          type: array
          items:
            allOf:
              - $ref: '#/components/schemas/Availability'
              - type: object
                required: [worker]
                properties:
                  worker: { $ref: '#/components/schemas/Worker' }
        count: { type: integer }
        query: { type: object, additionalProperties: true }
    MyRecordsResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [jobs, workers, availability, booking_requests, watches]
          properties:
            jobs: { type: array, items: { $ref: '#/components/schemas/Job' } }
            workers: { type: array, items: { $ref: '#/components/schemas/Worker' } }
            availability: { type: array, items: { $ref: '#/components/schemas/Availability' } }
            booking_requests: { type: array, items: { type: object, additionalProperties: true } }
            watches: { type: array, items: { type: object, additionalProperties: true } }
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code]
          properties:
            code: { type: string }
            message: { type: string }
            issues:
              type: array
              items:
                type: object
                required: [field, message]
                properties:
                  field: { type: string }
                  message: { type: string }

  responses:
    DataObject:
      description: Data object
      content:
        application/json:
          schema:
            type: object
            required: [data]
            properties:
              data: { type: object, additionalProperties: true }
    JobMutation:
      description: Changed job
      headers:
        Idempotency-Replayed:
          schema: { type: string, enum: ['true', 'false'] }
      content:
        application/json: { schema: { $ref: '#/components/schemas/JobResponse' } }
    WorkerMutation:
      description: Changed worker
      content:
        application/json: { schema: { $ref: '#/components/schemas/WorkerResponse' } }
    AvailabilityMutation:
      description: Changed availability
      content:
        application/json: { schema: { $ref: '#/components/schemas/AvailabilityResponse' } }
    ValidationError:
      description: Invalid input
      content:
        application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } }
    Unauthorized:
      description: Authentication is missing or invalid
      content:
        application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } }
    Forbidden:
      description: Scope is missing, or a browser write is not same-origin
      content:
        application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } }
    NotFound:
      description: Record does not exist or belongs to another account
      content:
        application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } }
    Conflict:
      description: Draft, slug, idempotency key, or requested period conflicts
      content:
        application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } }
    DraftExpired:
      description: Immutable draft expired before publication
      content:
        application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } }
