openapi: 3.0.3
info:
  title: Open4X Support Group Extension
  version: 2026.09.21-hierarchy.1
  description: Deployed to production on 2026-09-16 with migration 0055 and matching Gateway/Widget. Supplements the main OpenAPI. Routing does not grant customer access. Merchant acceptance and other environments must be verified separately.
servers:
  - url: https://api.open4x.com
security:
  - Bearer: []
paths:
  /v1/conversations/{id}/widget-session:
    parameters:
      - {$ref: '#/components/parameters/ConversationId'}
    post:
      operationId: issueWidgetSessionWithSupportGroup
      description: Extends the existing widget-session request. Server conversation:write required; support_group additionally requires conversation:supervise. Validate customer login and order ownership before issuing. This issues a single-conversation token; use customer-session for a directory token.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [external_user_id]
              properties:
                external_user_id: {type: string, minLength: 1, maxLength: 200}
                user_name: {type: string, minLength: 1, maxLength: 200}
                participant_id: {type: string, description: Active customer participant required when support_group is present}
                recipient_participant_ids:
                  type: array
                  items: {type: string}
                  description: Explicit active recipients required for order/dispute conversations
                chat_id: {type: string, maxLength: 240}
                ttl_seconds: {type: integer, minimum: 900, maximum: 3600, default: 1800}
                support_group: {$ref: '#/components/schemas/WidgetSupportGroup'}
      responses:
        '201':
          description: Single-conversation credential. Never log or expose outside the authorized user's session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      conversation_id: {type: string}
                      channel_id: {type: string}
                      chat_id: {type: string}
                      participant_id: {type: string, nullable: true}
                      recipient_participant_ids: {type: array, items: {type: string}}
                      session_token: {type: string}
                      expires_in: {type: integer}
        default: {$ref: '#/components/responses/Error'}
  /v1/conversations/customer-groups/{externalUserId}:
    parameters:
      - in: path
        name: externalUserId
        required: true
        schema: {type: string, minLength: 1, maxLength: 200}
    put:
      operationId: setCustomerSupportGroup
      description: Server API Key with conversation:supervise. Sets the authoritative customer group for personal and order support chats. Null explicitly ungroups. Enrolls eligible single-customer support conversations. Access is checked on subsequent requests; old order tags no longer override customer groups.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [group_tag]
              properties:
                group_tag: {$ref: '#/components/schemas/GroupTag'}
      responses:
        '200':
          description: Group configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      external_user_id: {type: string}
                      group_tag: {$ref: '#/components/schemas/GroupTag'}
        default: {$ref: '#/components/responses/Error'}
  /v1/conversations/{id}/support-routing:
    parameters:
      - {$ref: '#/components/parameters/ConversationId'}
    put:
      operationId: setConversationSupportRouting
      description: Server conversation:supervise. Supports general_support, content_support or order_support only. Owner must match an active customer participant and cannot replace an existing different owner. Null/omitted tag inherits. A non-null tag initializes an unconfigured customer group, never an order override. Conflicting customer groups or ownership return 409; change groups with customer-groups. Never grants customer identity access.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [external_user_id]
              properties:
                external_user_id: {type: string, minLength: 1, maxLength: 200}
                group_tag: {$ref: '#/components/schemas/GroupTag'}
      responses:
        '200':
          description: Routing configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      conversation_id: {type: string}
                      external_user_id: {type: string}
                      group_tag: {$ref: '#/components/schemas/GroupTag'}
        default: {$ref: '#/components/responses/Error'}
    delete:
      operationId: removeConversationSupportRouting
      description: Server conversation:supervise. Removes routing only, not history or customer permissions.
      responses:
        '200':
          description: Routing removed (also succeeds if absent)
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      removed: {type: boolean, enum: [true]}
        default: {$ref: '#/components/responses/Error'}
  /v1/conversations/workbench-sessions:
    post:
      operationId: issueScopedWorkbenchSession
      description: Server read/write scopes required. Dynamic groups/tenant additionally require conversation:supervise. Merchant authorizes staff roles before issuance. GET /inbox and all individual routes recheck tenant and grants; removing issuing scopes revokes access. Revoke using DELETE /workbench-sessions/{sessionId} from the existing API.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/WorkbenchGrant'}
      responses:
        '201':
          description: Short-lived opaque session. Cache-Control no-store. Treat token as a secret.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    required: [id, session_token, expires_in, expires_at]
                    properties:
                      id: {type: string}
                      session_token: {type: string}
                      expires_in: {type: integer}
                      expires_at: {type: string, format: date-time}
        default: {$ref: '#/components/responses/Error'}
components:
  securitySchemes:
    Bearer: {type: http, scheme: bearer}
  parameters:
    ConversationId:
      in: path
      name: id
      required: true
      schema: {type: string, minLength: 1, maxLength: 120}
  responses:
    Error:
      description: 400 invalid input; 401 invalid credentials; 403 scope/grant denied; 404 conversation absent; 409 private conversation cannot join support queue; 429 rate or quota exceeded; 5xx server error. Use the platform request ID header for diagnostics.
      content:
        application/json:
          schema:
            type: object
            required: [error]
            properties:
              error: {type: string}
              detail: {type: string}
  schemas:
    GroupTag:
      type: string
      nullable: true
      minLength: 1
      maxLength: 100
      pattern: '^[a-zA-Z0-9_-]+$'
    WidgetSupportGroup:
      type: object
      description: Requires server conversation:supervise and a matching active customer participant. Empty object or null tag inherits. A non-null tag initializes only an unconfigured customer group; an existing different group returns 409. Omission leaves routing unchanged. This is not a per-order override.
      properties:
        group_tag: {$ref: '#/components/schemas/GroupTag'}
    WorkbenchGrant:
      type: object
      required: [external_user_id, origin]
      properties:
        external_user_id: {type: string, minLength: 1, maxLength: 200}
        origin: {type: string, format: uri, description: Exact Widget origin, HTTPS or localhost HTTP}
        scope: {type: string, enum: [conversations, groups, tenant], default: conversations}
        conversation_ids:
          type: array
          maxItems: 1000
          description: Required nonempty for conversations scope. Must be absent or empty for dynamic scopes.
          items: {type: string, pattern: '^[a-zA-Z0-9_-]{1,120}$'}
        group_tags:
          type: array
          minItems: 1
          maxItems: 50
          description: Required only for groups scope; forbidden for other scopes.
          items: {type: string, pattern: '^[a-zA-Z0-9_-]{1,100}$'}
        mode: {type: string, enum: [agent, read_only, notes], default: agent}
        manage: {type: boolean, default: false, description: True only with agent mode}
        supervise: {type: boolean, default: false, description: Explicit merchant administrator capability. Requires tenant scope and agent mode. Observe without auto-joining; join before writing. Enables supervision and audit routes.}
        ttl_seconds: {type: integer, minimum: 60, maximum: 3600, default: 900}
        constraints:
          type: object
          additionalProperties: false
          properties:
            agent_id: {type: string, minLength: 1, maxLength: 200}
            business_line: {type: string, minLength: 1, maxLength: 200}
            merchant_id: {type: string, minLength: 1, maxLength: 200}
            creator_id: {type: string, minLength: 1, maxLength: 200}
