openapi: 3.1.0
info:
  title: ChatX Business API
  version: 1.0.0
  description: Business messaging, follower consent, webhooks, and QR web sign-in. Console endpoints use a ChatX user JWT; `/v1` messaging endpoints use an opaque developer-app token.
servers:
  - url: https://api.chatx.example
    description: Replace with your ChatX API base URL
tags:
  - name: QR sign-in
  - name: Business
  - name: Developer apps
  - name: Webhooks
paths:
  /businesses/{businessAccountId}/follow:
    parameters:
      - name: businessAccountId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Business]
      operationId: getBusinessFollow
      summary: Read the current user's follower relationship
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Active or unfollowed relationship
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/BaseEnvelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/BusinessFollow' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Business]
      operationId: followBusiness
      summary: Follow a live business
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                marketing_opt_in: { type: boolean, default: false }
      responses:
        '200': { description: Active follower relationship }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Business]
      operationId: updateBusinessFollow
      summary: Update marketing consent for an active follow
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [marketing_opt_in]
              properties:
                marketing_opt_in: { type: boolean }
      responses:
        '200': { description: Updated follower relationship }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Business]
      operationId: unfollowBusiness
      summary: Unfollow a business
      description: Idempotent; repeated requests leave the relationship unfollowed.
      security: [{ bearerAuth: [] }]
      responses:
        '200': { $ref: '#/components/responses/EmptySuccess' }
  /auth/business/otp/request:
    post:
      tags: [Business]
      operationId: requestBusinessLoginOTP
      summary: Send a login code to an existing account inside ChatX
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_number]
              properties:
                phone_number: { type: string }
      responses:
        '200': { $ref: '#/components/responses/EmptySuccess' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { description: ChatX delivery unavailable }
  /auth/business/otp/verify:
    post:
      tags: [Business]
      operationId: verifyBusinessLoginOTP
      summary: Verify a Business-purpose login code and issue a session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_number, code]
              properties:
                phone_number: { type: string }
                code: { type: string, pattern: '^[0-9]{6}$' }
      responses:
        '200': { description: Access and rotating refresh tokens issued }
        '400': { description: Invalid or expired code }
        '403': { description: Account is banned or suspended }
        '429': { $ref: '#/components/responses/RateLimited' }
  /auth/qr/sessions:
    post:
      tags: [QR sign-in]
      operationId: createQRSession
      summary: Create a two-minute QR sign-in session
      responses:
        '201':
          description: Session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QRSessionEnvelope'
        '429': { $ref: '#/components/responses/RateLimited' }
  /auth/qr/sessions/{id}/preview:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    get:
      tags: [QR sign-in]
      operationId: previewQRSession
      summary: Read the requesting browser before approval
      responses:
        '200':
          description: Session preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QRPreviewEnvelope'
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /auth/qr/sessions/{id}:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    get:
      tags: [QR sign-in]
      operationId: getQRSessionStatus
      summary: Poll QR session status
      parameters:
        - $ref: '#/components/parameters/QRSecret'
      responses:
        '200':
          description: Current status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QRStatusEnvelope'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /auth/qr/sessions/{id}/approve:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    post:
      tags: [QR sign-in]
      operationId: approveQRSession
      summary: Approve a browser login from an authenticated mobile app
      security:
        - bearerAuth: []
      responses:
        '200': { $ref: '#/components/responses/EmptySuccess' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /auth/qr/sessions/{id}/exchange:
    parameters:
      - $ref: '#/components/parameters/SessionId'
    post:
      tags: [QR sign-in]
      operationId: exchangeQRSession
      summary: Exchange an approved session once
      parameters:
        - $ref: '#/components/parameters/QRSecret'
      responses:
        '200':
          description: Access token issued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenEnvelope'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /business/me:
    get:
      tags: [Business]
      operationId: getBusinessAccount
      summary: Read the active Business account
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Business account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BusinessAccountEnvelope'
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    patch:
      tags: [Business]
      operationId: updateBusinessAccount
      summary: Update the owner-managed Business profile
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [business_name]
              properties:
                business_name: { type: string, maxLength: 100 }
                category: { type: [string, 'null'] }
                website_url: { type: [string, 'null'], format: uri }
      responses:
        '200': { description: Business profile updated }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /business/sandbox:
    post:
      tags: [Business]
      operationId: createBusinessSandbox
      summary: Create a private sandbox workspace
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [business_name]
              properties:
                business_name: { type: string, maxLength: 100 }
                category: { type: [string, 'null'] }
                website_url: { type: [string, 'null'], format: uri }
      responses:
        '201': { description: Sandbox created }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
  /business/usage:
    get:
      tags: [Business]
      operationId: getBusinessUsage
      summary: Read message and webhook totals
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: Usage totals }
        '403': { $ref: '#/components/responses/Forbidden' }
  /business/messages:
    get:
      tags: [Business]
      operationId: listBusinessMessages
      summary: List recent business message sends
      security: [{ bearerAuth: [] }]
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        '200': { description: Message send history }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Business]
      operationId: sendBusinessMessage
      summary: Send a category-scoped message without template approval
      description: Utility requires an active follower. Marketing also requires opt-in. Authentication is reserved for account security messages.
      security: [{ bearerAuth: [] }]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BusinessMessageInput' }
      responses:
        '200': { $ref: '#/components/responses/MessageSendSuccess' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /business/team:
    get:
      tags: [Business]
      operationId: listBusinessTeam
      summary: List console team members
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: Team members }
    post:
      tags: [Business]
      operationId: addBusinessTeamMember
      summary: Add or update a team member
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [phone_number, role]
              properties:
                phone_number: { type: string, description: Existing ChatX account phone number }
                role: { type: string, enum: [admin, member] }
      responses:
        '201': { description: Team member added }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /business/team/{userId}:
    delete:
      tags: [Business]
      operationId: removeBusinessTeamMember
      summary: Remove a team member
      security: [{ bearerAuth: [] }]
      parameters:
        - name: userId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { $ref: '#/components/responses/EmptySuccess' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /business/apps:
    get:
      tags: [Developer apps]
      operationId: listDeveloperApps
      summary: Owner-only list of developer apps; tokens are never returned
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: Developer apps }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Developer apps]
      operationId: createDeveloperApp
      summary: Create an app and return its API token once
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, scopes]
              properties:
                name: { type: string, maxLength: 100 }
                scopes:
                  type: array
                  minItems: 1
                  uniqueItems: true
                  items: { type: string, enum: [authentication, utility, marketing, service] }
      responses:
        '201': { description: App metadata and one-time API token }
        '403': { $ref: '#/components/responses/Forbidden' }
  /business/apps/{appId}/rotate:
    post:
      tags: [Developer apps]
      operationId: rotateDeveloperAppToken
      summary: Owner-only token rotation; immediately invalidates the old token
      security: [{ bearerAuth: [] }]
      parameters:
        - name: appId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { description: App metadata and new one-time API token }
        '403': { $ref: '#/components/responses/Forbidden' }
  /business/apps/{appId}:
    delete:
      tags: [Developer apps]
      operationId: revokeDeveloperApp
      summary: Owner-only revocation of the app and its token
      security: [{ bearerAuth: [] }]
      parameters:
        - name: appId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200': { $ref: '#/components/responses/EmptySuccess' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /v1/{businessAccountId}/messages:
    post:
      tags: [Developer apps]
      operationId: sendMessageWithAppToken
      summary: Send a category-scoped message with the matching app scope
      security: [{ appToken: [] }]
      parameters:
        - name: businessAccountId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BusinessMessageInput" }
      responses:
        "200": { $ref: "#/components/responses/MessageSendSuccess" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /v1/service/{phoneNumber}/messages:
    post:
      tags: [Developer apps]
      operationId: sendServiceMessageWithAppToken
      summary: Send free-form service text to a follower
      security: [{ appToken: [] }]
      parameters:
        - name: phoneNumber
          in: path
          required: true
          description: The business account's own phone number
          schema: { type: string }
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to, body]
              properties:
                to: { type: string, description: Recipient phone number }
                body: { type: string }
      responses:
        '200': { $ref: '#/components/responses/MessageSendSuccess' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
  /business/webhooks:
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: List webhook endpoints
      security: [{ bearerAuth: [] }]
      responses:
        '200':
          description: Webhooks
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/BaseEnvelope'
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: '#/components/schemas/Webhook' }
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Register a webhook endpoint
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookInput' }
      responses:
        '201':
          description: Webhook created; signing secret is returned once
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/BaseEnvelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Webhook' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '409': { $ref: '#/components/responses/Conflict' }
  /business/webhooks/{id}:
    parameters:
      - $ref: '#/components/parameters/WebhookId'
    patch:
      tags: [Webhooks]
      operationId: updateWebhook
      summary: Update a webhook
      security: [{ bearerAuth: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { $ref: '#/components/schemas/WebhookEvent' }
                active: { type: boolean }
      responses:
        '200':
          description: Webhook updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/BaseEnvelope'
                  - type: object
                    properties:
                      data: { $ref: '#/components/schemas/Webhook' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Delete a webhook
      security: [{ bearerAuth: [] }]
      responses:
        '200': { $ref: '#/components/responses/EmptySuccess' }
        '404': { $ref: '#/components/responses/NotFound' }
  /business/webhooks/{id}/test:
    parameters:
      - $ref: '#/components/parameters/WebhookId'
    post:
      tags: [Webhooks]
      operationId: testWebhook
      summary: Queue a signed test event
      security: [{ bearerAuth: [] }]
      responses:
        '200': { $ref: '#/components/responses/EmptySuccess' }
        '404': { $ref: '#/components/responses/NotFound' }
  /business/webhook-deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: List recent webhook attempts and retry status
      security: [{ bearerAuth: [] }]
      responses:
        '200': { description: Webhook delivery history }
        '403': { $ref: '#/components/responses/Forbidden' }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: ChatX user access token used by the console and mobile follow APIs.
    appToken:
      type: http
      scheme: bearer
      description: Opaque developer-app token returned once when an app is created or rotated.
  parameters:
    SessionId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    WebhookId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    QRSecret:
      name: X-ChatX-QR-Secret
      in: header
      required: true
      schema: { type: string }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: A caller-generated key, up to 255 characters. Retrying the same request with the same key returns the original operation ID without sending again. Reusing it for different content returns 409.
      schema: { type: string, maxLength: 255 }
  responses:
    EmptySuccess:
      description: Operation completed
      content:
        application/json:
          schema: { $ref: '#/components/schemas/BaseEnvelope' }
    MessageSendSuccess:
      description: Message sent, or the completed result of an idempotent retry
      content:
        application/json:
          schema: { $ref: '#/components/schemas/MessageSendEnvelope' }
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Unauthorized:
      description: Authentication or QR secret failed
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Forbidden:
      description: Active Business access or resource ownership is required
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    NotFound:
      description: Resource was not found or expired
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    Conflict:
      description: Resource state conflicts with the operation
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
    RateLimited:
      description: Request limit reached
      headers:
        Retry-After:
          schema: { type: integer }
          description: Seconds until another request should be attempted
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorEnvelope' }
  schemas:
    BaseEnvelope:
      type: object
      required: [success, message, data, timestamp]
      properties:
        success: { type: boolean }
        message: { type: string }
        data: {}
        timestamp: { type: string, format: date-time }
    ErrorEnvelope:
      allOf:
        - $ref: '#/components/schemas/BaseEnvelope'
        - type: object
          properties:
            success: { const: false }
            data: { type: 'null' }
    MessageSendEnvelope:
      allOf:
        - $ref: '#/components/schemas/BaseEnvelope'
        - type: object
          properties:
            success: { const: true }
            message: { const: message_sent }
            data:
              type: object
              required: [id, status]
              properties:
                id: { type: string, format: uuid }
                status: { const: sent }
    QRSessionEnvelope:
      allOf:
        - $ref: '#/components/schemas/BaseEnvelope'
        - type: object
          properties:
            data:
              type: object
              required: [id, qr_payload, exchange_secret, expires_at]
              properties:
                id: { type: string, format: uuid }
                qr_payload: { type: string, example: 'chatx://business-login/00000000-0000-0000-0000-000000000000' }
                exchange_secret: { type: string }
                expires_at: { type: string, format: date-time }
    QRPreviewEnvelope:
      allOf:
        - $ref: '#/components/schemas/BaseEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                client_name: { type: string, example: Chrome on macOS }
                expires_at: { type: string, format: date-time }
    QRStatusEnvelope:
      allOf:
        - $ref: '#/components/schemas/BaseEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                status: { type: string, enum: [pending, approved] }
    TokenEnvelope:
      allOf:
        - $ref: '#/components/schemas/BaseEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                token: { type: string }
    BusinessAccountEnvelope:
      allOf:
        - $ref: '#/components/schemas/BaseEnvelope'
        - type: object
          properties:
            data:
              type: object
              properties:
                user_id: { type: string, format: uuid }
                business_name: { type: string }
                category: { type: [string, 'null'] }
                website_url: { type: [string, 'null'], format: uri }
                status: { type: string, enum: [active, suspended] }
                mode: { type: string, enum: [sandbox, live] }
    BusinessFollow:
      type: object
      required: [business_account_id, user_id, status, marketing_opt_in]
      properties:
        business_account_id: { type: string, format: uuid }
        user_id: { type: string, format: uuid }
        status: { type: string, enum: [active, unfollowed] }
        marketing_opt_in: { type: boolean }
        followed_at: { type: [string, 'null'], format: date-time }
        updated_at: { type: [string, 'null'], format: date-time }
    BusinessMessageInput:
      type: object
      required: [to, category]
      properties:
        to: { type: string, description: Recipient phone number }
        category: { type: string, enum: [authentication, utility, marketing] }
        body: { type: string, description: Plain text for utility or marketing }
        message_content: { type: object, description: Structured authentication or rich marketing content }
    WebhookEvent:
      type: string
      enum: [message.sent, message.failed, grant.revoked]
    WebhookInput:
      type: object
      required: [url, events]
      properties:
        url: { type: string, format: uri }
        events:
          type: array
          minItems: 1
          uniqueItems: true
          items: { $ref: '#/components/schemas/WebhookEvent' }
    Webhook:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEvent' }
        active: { type: boolean }
        secret: { type: string, description: Returned only when the webhook is created }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
