openapi: 3.1.0
info:
  title: There There API
  description: REST API for the There There ticket management system.
  version: 1.0.0

servers:
  - url: https://there-there.app/api

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Sanctum API token. Generate one from Settings > API Tokens.

  schemas:
    Ticket:
      type: object
      properties:
        id:
          type: integer
        ulid:
          type: string
        subject:
          type: string
        status:
          type: string
          enum: [open, closed, spam]
        channel:
          $ref: '#/components/schemas/Channel'
        assignee:
          oneOf:
            - $ref: '#/components/schemas/User'
            - type: 'null'
        tags:
          type: array
          items:
            $ref: '#/components/schemas/Tag'
        latest_message_preview:
          type: string
          nullable: true
        visitor_last_read_at:
          type: string
          format: date-time
          nullable: true
        widget_session:
          type: object
          nullable: true
        summary:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    TicketDetail:
      allOf:
        - $ref: '#/components/schemas/Ticket'
        - type: object
          properties:
            messages:
              type: array
              items:
                $ref: '#/components/schemas/Message'
            activities:
              type: array
              items:
                $ref: '#/components/schemas/Activity'

    Message:
      type: object
      properties:
        id:
          type: integer
        ulid:
          type: string
        type:
          type: string
          enum: [inbound, outbound, note]
        body_html:
          type: string
        sender:
          type: object
          properties:
            name:
              type: string
            email:
              type: string
            avatar_url:
              type: string
              nullable: true
        attachments:
          type: array
          items:
            type: object
            properties:
              id:
                type: integer
              name:
                type: string
              url:
                type: string
              size:
                type: integer
              mime_type:
                type: string
        is_forward:
          type: boolean
        created_at:
          type: string
          format: date-time

    Activity:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
        user:
          oneOf:
            - $ref: '#/components/schemas/User'
            - type: 'null'
        properties:
          type: object
          nullable: true
          additionalProperties: true
        description:
          type: string
        created_at:
          type: string
          format: date-time

    Tag:
      type: object
      properties:
        id:
          type: integer
        ulid:
          type: string
        name:
          type: string
        color:
          type: string

    Channel:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        type:
          type: string
        color:
          type: string

    Contact:
      type: object
      properties:
        id:
          type: integer
        ulid:
          type: string
        name:
          type: string
          nullable: true
        email:
          type: string
        avatar_url:
          type: string
          nullable: true
        ticket_count:
          type: integer
          nullable: true
        last_activity_at:
          type: string
          format: date-time
          nullable: true
        notes:
          type: string
          nullable: true

    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
        avatar_url:
          type: string
          nullable: true
        timezone:
          type: string
          nullable: true

    PaginationMeta:
      type: object
      properties:
        current_page:
          type: integer
        last_page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer

    ValidationError:
      type: object
      properties:
        message:
          type: string
        errors:
          type: object
          additionalProperties:
            type: array
            items:
              type: string

  responses:
    Unauthenticated:
      description: Unauthenticated
      content:
        application/json:
          schema:
            type: object
            properties:
              message:
                type: string
                example: Unauthenticated.
    NotFound:
      description: Resource not found
    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'

paths:
  /me:
    get:
      operationId: getMe
      summary: Get current user and workspace
      tags: [Authentication]
      responses:
        '200':
          description: Authenticated user and workspace info
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: '#/components/schemas/User'
                  workspace:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                      slug:
                        type: string
        '401':
          $ref: '#/components/responses/Unauthenticated'

  /tickets:
    get:
      operationId: listTickets
      summary: List tickets
      tags: [Tickets]
      parameters:
        - name: filter[status]
          in: query
          description: Filter by status (comma separated)
          schema:
            type: string
            example: open
        - name: filter[tag_ids]
          in: query
          description: Filter by tag IDs (comma separated)
          schema:
            type: string
            example: '1,2'
        - name: filter[channel_ids]
          in: query
          description: Filter by channel IDs (comma separated)
          schema:
            type: string
            example: '1'
        - name: filter[assigned_to_me]
          in: query
          description: Only show tickets assigned to the authenticated user
          schema:
            type: boolean
        - name: filter[unassigned]
          in: query
          description: Only show unassigned tickets
          schema:
            type: boolean
        - name: q
          in: query
          description: Semantic search query. Uses AI embeddings to find tickets by meaning across subjects, summaries, and messages. Results are ordered by relevance.
          schema:
            type: string
        - name: filter[search]
          in: query
          description: Full-text keyword search across ticket subjects, messages, and contact info
          schema:
            type: string
        - name: filter[assigned_user_id]
          in: query
          description: Filter by specific assignee user ID
          schema:
            type: integer
        - name: filter[created_after]
          in: query
          description: Only tickets created on or after this date (ISO 8601)
          schema:
            type: string
            format: date
            example: '2026-01-01'
        - name: filter[created_before]
          in: query
          description: Only tickets created on or before this date (ISO 8601)
          schema:
            type: string
            format: date
            example: '2026-03-01'
        - name: sort
          in: query
          description: 'Sort field. Prefix with - for descending. Allowed: created_at, updated_at, last_activity_at'
          schema:
            type: string
            example: '-updated_at'
        - name: per_page
          in: query
          description: Items per page (max 100, default 25)
          schema:
            type: integer
            default: 25
        - name: page
          in: query
          description: Page number
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: Paginated list of tickets
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Ticket'
                  - $ref: '#/components/schemas/PaginationMeta'
        '401':
          $ref: '#/components/responses/Unauthenticated'

  /tickets/{ticket}:
    get:
      operationId: showTicket
      summary: Get ticket details
      tags: [Tickets]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
      responses:
        '200':
          description: Ticket details with messages and activities
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket:
                    $ref: '#/components/schemas/TicketDetail'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'

  /tickets/{ticket}/status:
    put:
      operationId: updateTicketStatus
      summary: Change ticket status
      tags: [Tickets]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status:
                  type: string
                  enum: [open, closed, spam]
      responses:
        '200':
          description: Ticket with updated status
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket:
                    $ref: '#/components/schemas/Ticket'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /tickets/{ticket}/assignee:
    put:
      operationId: updateTicketAssignee
      summary: Assign or unassign a user
      tags: [Tickets]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                assignee_id:
                  type: integer
                  nullable: true
                  description: User ID to assign, or null to unassign
      responses:
        '200':
          description: Ticket with updated assignee
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket:
                    $ref: '#/components/schemas/Ticket'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /tickets/{ticket}/team:
    put:
      operationId: updateTicketTeam
      summary: Assign or unassign a team
      tags: [Tickets]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                team_id:
                  type: integer
                  nullable: true
                  description: Team ID to assign, or null to unassign
      responses:
        '200':
          description: Ticket with updated team
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket:
                    $ref: '#/components/schemas/Ticket'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /tickets/{ticket}/reply:
    post:
      operationId: replyToTicket
      summary: Send a reply
      tags: [Messages]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body:
                  type: string
                  description: HTML body of the reply
                to_recipients:
                  type: array
                  items:
                    type: string
                    format: email
                cc_recipients:
                  type: array
                  items:
                    type: string
                    format: email
                bcc_recipients:
                  type: array
                  items:
                    type: string
                    format: email
      responses:
        '201':
          description: Message created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    $ref: '#/components/schemas/Message'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /tickets/{ticket}/forward:
    post:
      operationId: forwardTicket
      summary: Forward a ticket
      tags: [Messages]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body, to_recipients]
              properties:
                body:
                  type: string
                  description: HTML body of the forward
                to_recipients:
                  type: array
                  minItems: 1
                  items:
                    type: string
                    format: email
                cc_recipients:
                  type: array
                  items:
                    type: string
                    format: email
                bcc_recipients:
                  type: array
                  items:
                    type: string
                    format: email
                forwarded_from_message_id:
                  type: integer
                  nullable: true
                  description: ID of the original message being forwarded
      responses:
        '201':
          description: Forward message created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    $ref: '#/components/schemas/Message'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /tickets/{ticket}/note:
    post:
      operationId: addNoteToTicket
      summary: Add an internal note
      tags: [Messages]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [body]
              properties:
                body:
                  type: string
                  description: HTML body of the note
      responses:
        '201':
          description: Note created
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    $ref: '#/components/schemas/Message'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /tickets/{ticket}/tags/{tag}:
    post:
      operationId: addTagToTicket
      summary: Add a tag to a ticket
      tags: [Tickets]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
        - name: tag
          in: path
          required: true
          description: Tag ULID
          schema:
            type: string
      responses:
        '200':
          description: Ticket with updated tags
          content:
            application/json:
              schema:
                type: object
                properties:
                  ticket:
                    $ref: '#/components/schemas/Ticket'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: removeTagFromTicket
      summary: Remove a tag from a ticket
      tags: [Tickets]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
        - name: tag
          in: path
          required: true
          description: Tag ULID
          schema:
            type: string
      responses:
        '204':
          description: Tag removed
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'

  /tickets/{ticket}/activities:
    get:
      operationId: listTicketActivities
      summary: List ticket activities
      tags: [Tickets]
      parameters:
        - name: ticket
          in: path
          required: true
          description: Ticket ULID
          schema:
            type: string
        - name: page
          in: query
          description: Page number
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: Paginated list of activities
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Activity'
                  - $ref: '#/components/schemas/PaginationMeta'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'

  /tags:
    get:
      operationId: listTags
      summary: List workspace tags
      tags: [Tags]
      responses:
        '200':
          description: List of tags
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Tag'
        '401':
          $ref: '#/components/responses/Unauthenticated'

  /channels:
    get:
      operationId: listChannels
      summary: List workspace channels
      tags: [Channels]
      responses:
        '200':
          description: List of channels accessible to the user
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Channel'
        '401':
          $ref: '#/components/responses/Unauthenticated'

  /members:
    get:
      operationId: listMembers
      summary: List workspace members
      tags: [Members]
      responses:
        '200':
          description: List of workspace members
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
        '401':
          $ref: '#/components/responses/Unauthenticated'

  /contacts:
    get:
      operationId: listContacts
      summary: List contacts
      tags: [Contacts]
      parameters:
        - name: filter[name]
          in: query
          description: Filter by name (partial match)
          schema:
            type: string
        - name: filter[email]
          in: query
          description: Filter by email (partial match)
          schema:
            type: string
        - name: filter[search]
          in: query
          description: Search by name or email
          schema:
            type: string
        - name: sort
          in: query
          description: 'Sort field. Prefix with - for descending. Allowed: name, created_at, last_activity'
          schema:
            type: string
            default: name
        - name: per_page
          in: query
          description: Items per page (max 100, default 25)
          schema:
            type: integer
            default: 25
        - name: page
          in: query
          description: Page number
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: Paginated list of contacts
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Contact'
                  - $ref: '#/components/schemas/PaginationMeta'
        '401':
          $ref: '#/components/responses/Unauthenticated'

  /contacts/{contact}:
    get:
      operationId: showContact
      summary: Get contact details
      tags: [Contacts]
      parameters:
        - name: contact
          in: path
          required: true
          description: Contact ULID
          schema:
            type: string
      responses:
        '200':
          description: Contact details
          content:
            application/json:
              schema:
                type: object
                properties:
                  contact:
                    $ref: '#/components/schemas/Contact'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '404':
          $ref: '#/components/responses/NotFound'
