openapi: 3.1.0
info:
  title: Zimun External API
  version: 0.1.0
  description: OAuth2-protected API for availability and appointment management.
servers:
  - url: https://api.zimun.online/api/v1
security:
  - oauth_client_credentials: [ ]

components:
  securitySchemes:
    oauth_client_credentials:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: /oauth/token
          scopes:
            availability:read: Read availability
            appointments:read: Read appointments
            appointments:write: Create/update appointments
            org:read: Read organization metadata
  schemas:
    ErrorResponse:
      type: object
      properties:
        detail: { type: string }
      example:
        detail: "Unauthorized"
    Organization:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        timezone: { type: string }
        locale: { type: string }
        address:
          type: object
          additionalProperties: true
      example:
        id: "o_123"
        name: "Zimun Demo"
        description: "Barbershop in Nürnberg"
        timezone: "Europe/Berlin"
        locale: "de"
        address:
          formatted: "Esslinger Str. 42, 90441 Nürnberg, Germany"
    Service:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        image: { type: string }
        resource_ids: { type: array, items: { type: string } }
      example:
        id: "s_123"
        name: "Haircut"
        description: "Wash + cut + style"
        image: "/images/service.jpg"
        resource_ids: ["r_chair"]
    Staff:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        email: { type: string, format: email }
        bio: { type: string }
        assigned_service: { type: array, items: { type: string } }
        working_days: { type: array, items: { type: object } }
      example:
        id: "m_123"
        name: "Alex Barber"
        email: "alex@example.com"
        bio: "Senior stylist"
        assigned_service: ["s_123"]
        working_days:
          - day: "monday"
            start: "09:00"
            end: "17:00"
    Resource:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        used_in_services: { type: array, items: { type: string } }
      example:
        id: "r_chair"
        name: "Chair 1"
        used_in_services: ["s_123"]
    HolidayMapResponse:
      type: object
      additionalProperties: { type: string }
      example:
        "2025-12-25": "Christmas Day"
    AvailabilitySlot:
      type: object
      properties:
        date: { type: string, format: date }
        start_time: { type: string, format: date-time }
        end_time: { type: string, format: date-time }
        status: { type: string, enum: [available, onhold, booked] }
        team_member_id: { type: string }
        service_id: { type: string }
        resource_ids: { type: array, items: { type: string } }
      example:
        date: "2025-12-01"
        start_time: "2025-12-01T10:00:00Z"
        end_time: "2025-12-01T10:30:00Z"
        status: "available"
        team_member_id: "m_123"
        service_id: "s_123"
        resource_ids: ["r_chair"]
    Appointment:
      type: object
      properties:
        id: { type: string, format: uuid }
        status: { type: string, enum: [scheduled, cancelled] }
        start_time: { type: string, format: date-time }
        end_time: { type: string, format: date-time }
        service_id: { type: string, format: uuid }
        team_member_id: { type: string, format: uuid }
        staff_id: { type: string, format: uuid, deprecated: true }
        appointment_time: { type: string, format: date-time }
        appointment_duration: { type: string }
        organization_id: { type: string, format: uuid }
        resource_ids: { type: array, items: { type: string } }
        hold_id: { type: string }
        hold_expires_at: { type: string, format: date-time }
        contact_name: { type: string }
        contact_email: { type: string, format: email }
        contact_phone_number: { type: string }
      example:
        id: "a_123"
        status: "scheduled"
        start_time: "2025-12-01T10:00:00Z"
        end_time: "2025-12-01T10:30:00Z"
        service_id: "s_123"
        team_member_id: "m_123"
        organization_id: "o_123"
        contact_name: "Dana Lee"
        contact_email: "dana@example.com"
    HoldRequest:
      type: object
      required: [organization_id, service_id, appointment_time, appointment_duration]
      properties:
        organization_id: { type: string, format: uuid }
        service_id: { type: string, format: uuid }
        appointment_time: { type: string, format: date-time }
        appointment_duration: { type: integer }
        team_member_id: { type: string, format: uuid }
        resource_ids: { type: array, items: { type: string } }
      example:
        organization_id: "o_123"
        service_id: "s_123"
        appointment_time: "2025-12-01T10:00:00Z"
        appointment_duration: 30
    HoldConfirmRequest:
      type: object
      required: [hold_id]
      properties:
        hold_id: { type: string, format: uuid }
        contact_name: { type: string }
        contact_email: { type: string, format: email }
        contact_phone_number: { type: string }
      example:
        hold_id: "hold_abc"
        contact_name: "Dana Lee"
        contact_email: "dana@example.com"
    HoldResponse:
      type: object
      properties:
        hold_id: { type: string, format: uuid }
        expires_at: { type: string, format: date-time }
        appointment_id: { type: string, format: uuid }
      example:
        hold_id: "hold_abc"
        expires_at: "2025-12-01T09:10:00Z"
        appointment_id: "a_123"
    HoldConfirmResponse:
      type: object
      properties:
        status: { type: string, enum: [scheduled] }
        appointment_id: { type: string, format: uuid }
      example:
        status: "scheduled"
        appointment_id: "a_123"
    AppointmentUpdateRequest:
      type: object
      properties:
        status: { type: string, enum: [cancelled] }
        start_time: { type: string, format: date-time }
        end_time: { type: string, format: date-time }
        team_member_id: { type: string, format: uuid }
      example:
        status: "cancelled"
    WebhookCreateRequest:
      type: object
      required: [url, events]
      properties:
        url: { type: string, format: uri }
        events:
          type: array
          items:
            type: string
            enum:
              - appointment.confirmed
              - appointment.updated
              - appointment.cancelled
              - hold.created
              - hold.expired
      example:
        url: "https://example.com/webhook"
        events: ["appointment.confirmed"]
    WebhookResponse:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events: { type: array, items: { type: string } }
        active: { type: boolean }
        created_at: { type: string, format: date-time }
        secret: { type: string, nullable: true }
      example:
        id: "wh_123"
        url: "https://example.com/webhook"
        events: ["appointment.confirmed"]
        active: true
        created_at: "2025-12-01T08:00:00Z"
        secret: "secret"
    ServicesListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Service' }
        next_page_token: { type: string, nullable: true }
      example:
        items:
          - id: "s_123"
            name: "Haircut"
        next_page_token: null
    StaffListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Staff' }
        next_page_token: { type: string, nullable: true }
      example:
        items:
          - id: "m_123"
            name: "Alex Barber"
        next_page_token: null
    ResourcesListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Resource' }
        next_page_token: { type: string, nullable: true }
      example:
        items:
          - id: "r_chair"
            name: "Chair 1"
        next_page_token: null
    AvailabilityListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/AvailabilitySlot' }
        next_page_token: { type: string, nullable: true }
      example:
        items:
          - date: "2025-12-01"
            start_time: "2025-12-01T10:00:00Z"
            end_time: "2025-12-01T10:30:00Z"
            status: "available"
        next_page_token: null
    AppointmentsListResponse:
      type: object
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Appointment' }
        next_page_token: { type: string, nullable: true }
      example:
        items:
          - id: "a_123"
            status: "scheduled"
        next_page_token: null

paths:
  /organizations/me:
    get:
      tags: [Organization]
      security:
        - oauth_client_credentials: [org:read]
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Organization' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /services:
    get:
      tags: [Services]
      security:
        - oauth_client_credentials: [org:read]
      parameters:
        - in: query
          name: page_size
          schema: { type: integer, minimum: 1, maximum: 200 }
        - in: query
          name: page_token
          schema: { type: string }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ServicesListResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /staff:
    get:
      tags: [Staff]
      security:
        - oauth_client_credentials: [org:read]
      parameters:
        - in: query
          name: page_size
          schema: { type: integer, minimum: 1, maximum: 200 }
        - in: query
          name: page_token
          schema: { type: string }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/StaffListResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /resources:
    get:
      tags: [Resources]
      security:
        - oauth_client_credentials: [org:read]
      parameters:
        - in: query
          name: page_size
          schema: { type: integer, minimum: 1, maximum: 200 }
        - in: query
          name: page_token
          schema: { type: string }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/ResourcesListResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /availability:
    get:
      tags: [Availability]
      security:
        - oauth_client_credentials: [availability:read]
      parameters:
        - in: query
          name: organization_id
          schema: { type: string }
        - in: query
          name: service_id
          schema: { type: string }
        - in: query
          name: date
          schema: { type: string, format: date }
        - in: query
          name: days_ahead
          schema: { type: integer, minimum: 1, maximum: 30 }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/HolidayMapResponse' } } } }
        '400': { description: Invalid request, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /appointments/hold:
    post:
      tags: [Appointments]
      security:
        - oauth_client_credentials: [appointments:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HoldRequest' }
      responses:
        '201': { description: Hold created, content: { application/json: { schema: { $ref: '#/components/schemas/HoldResponse' } } } }
        '400': { description: Invalid request, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /appointments/confirm:
    post:
      tags: [Appointments]
      security:
        - oauth_client_credentials: [appointments:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/HoldConfirmRequest' }
      responses:
        '200': { description: Appointment confirmed, content: { application/json: { schema: { $ref: '#/components/schemas/HoldConfirmResponse' } } } }
        '400': { description: Invalid request, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '409': { description: Conflict/expired hold, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /holds/{hold_id}:
    delete:
      tags: [Appointments]
      security:
        - oauth_client_credentials: [appointments:write]
      parameters:
        - in: path
          name: hold_id
          required: true
          schema: { type: string }
      responses:
        '204': { description: Hold released }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '404': { description: Hold not found, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /appointments:
    get:
      tags: [Appointments]
      security:
        - oauth_client_credentials: [appointments:read]
      parameters:
        - in: query
          name: status
          schema: { type: string, enum: [scheduled, cancelled, onhold] }
        - in: query
          name: staff_id
          deprecated: true
          description: "Deprecated; use team_member_id"
          schema: { type: string }
        - in: query
          name: team_member_id
          schema: { type: string }
        - in: query
          name: service_id
          schema: { type: string }
        - in: query
          name: start_date
          schema: { type: string, format: date }
        - in: query
          name: end_date
          schema: { type: string, format: date }
        - in: query
          name: page_size
          schema: { type: integer, minimum: 1, maximum: 200 }
        - in: query
          name: page_token
          schema: { type: string }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/AppointmentsListResponse' } } } }
        '400': { description: Invalid request, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /webhooks:
    post:
      tags: [Webhooks]
      security:
        - oauth_client_credentials: [appointments:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookCreateRequest' }
      responses:
        '201': { description: Webhook created, content: { application/json: { schema: { $ref: '#/components/schemas/WebhookResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '422': { description: Validation error, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
    get:
      tags: [Webhooks]
      security:
        - oauth_client_credentials: [appointments:write]
      responses:
        '200': { description: OK, content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/WebhookResponse' } } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /webhooks/{webhook_id}:
    delete:
      tags: [Webhooks]
      security:
        - oauth_client_credentials: [appointments:write]
      parameters:
        - in: path
          name: webhook_id
          required: true
          schema: { type: string }
      responses:
        '204': { description: Webhook revoked }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
  /appointments/{appointment_id}:
    get:
      tags: [Appointments]
      security:
        - oauth_client_credentials: [appointments:read]
      parameters:
        - in: path
          name: appointment_id
          required: true
          schema: { type: string }
      responses:
        '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Appointment' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '404': { description: Not found, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
    patch:
      tags: [Appointments]
      security:
        - oauth_client_credentials: [appointments:write]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AppointmentUpdateRequest' }
      responses:
        '200': { description: Updated, content: { application/json: { schema: { $ref: '#/components/schemas/Appointment' } } } }
        '400': { description: Invalid request, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '401': { description: Unauthorized, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '403': { description: Forbidden, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '404': { description: Not found, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
        '409': { description: Conflict, content: { application/json: { schema: { $ref: '#/components/schemas/ErrorResponse' } } } }
