openapi: 3.1.0
info:
  title: Anvil API
  version: "1.0.0"
  description: |
    Anvil is an AI-powered lead-generation platform built by YUE YUAN COMPANY LIMITED

    All endpoints return a standard envelope:

      { "success": true, "data": ..., "error": null, "meta": { ... } }

    On error, "success" is false, "data" is null, and "error" carries a code +
    human-readable message.

    Authenticate with a Bearer JWT (interactive web/mobile clients) OR a
    programmatic X-Api-Key header. API-key management endpoints accept ONLY
    JWT — keys cannot mint or revoke other keys.

    Tenant scoping: most endpoints require an active tenant. Pass it via the
    X-Tenant-ID header (the JWT also encodes the user's last-active tenant
    as a fallback).
  contact:
    name: YUE YUAN COMPANY LIMITED
    email: 738888@proton.me
    url: https://www.anvilhk.com
  license:
    name: MIT
servers:
  - url: https://api.anvilhk.com
    description: Production
  - url: http://localhost:8080
    description: Local
security:
  - BearerAuth: []
  - ApiKeyAuth: []
tags:
  - name: Auth
    description: Registration, login (incl. OAuth + 2FA), token lifecycle, password reset.
  - name: Me
    description: Authenticated user profile, sessions, 2FA, usage, GDPR export/delete.
  - name: Team
    description: Tenant member directory and invitations (OWNER/ADMIN only).
  - name: Contacts
    description: People in your CRM, with AI memory + AI-derived profile.
  - name: Leads
    description: Sales-cycle records on top of contacts.
  - name: Pipelines
    description: Funnels and their ordered stages.
  - name: Activities
    description: Timeline events on contacts and leads.
  - name: Tags
    description: Tenant-scoped colour-coded labels.
  - name: Chatbots
    description: Conversational AI bots and live sessions.
  - name: Knowledge Bases
    description: RAG corpora that back chatbots and agents.
  - name: Voice
    description: Cloned voices, scripts, outbound campaigns, call sessions.
  - name: Agents
    description: Goal-driven autonomous agents with plan / approval / log surfaces.
  - name: Research
    description: Keyword discovery, BSR, blue-ocean, competitor reverse, niche scoring.
  - name: Influencers
    description: KOL discovery, analysis, outreach, and campaigns.
  - name: Crawl
    description: Spider tasks, results, and supported platforms.
  - name: AI
    description: Generic AI passthrough (chat / analyze / content) shared across features.
  - name: Affiliate
    description: "Referral program: apply, dashboard, commissions, payouts."
  - name: Trade Documents
    description: Quotation, PI, packing list, commercial invoice, sales contract.
  - name: Webhooks
    description: Outgoing webhooks (you receive) + incoming provider receivers (we receive).
  - name: Billing
    description: Plans, subscription, usage metrics, invoices.
  - name: Branding
    description: White-label customisation (theme, domain, email templates, manifest).
paths:
  # ----------------------------------------------------------------- AUTH
  /api/v1/auth/register:
    post:
      tags: [Auth]
      summary: Register a new user with email + password
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password, name]
              properties:
                email: { type: string, format: email }
                password: { type: string, minLength: 8 }
                name: { type: string }
      responses:
        "200":
          description: User created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthTokensEnvelope" }
              example:
                success: true
                data:
                  accessToken: "eyJhbGciOi..."
                  expiresIn: 3600
                  user: { id: "u_01H...", email: "alice@acme.com", name: "Alice" }
                  tenant: { id: "t_01H...", name: "Acme Inc.", plan: "FREE" }
  /api/v1/auth/login:
    post:
      tags: [Auth]
      summary: Login with email/identifier + password
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                identifier: { type: string, description: "Email or username" }
                email: { type: string, format: email }
                password: { type: string }
                rememberMe: { type: boolean, default: false }
      responses:
        "200":
          description: AuthTokens, OR an MFA challenge if 2FA is enrolled.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/AuthTokensEnvelope"
                  - $ref: "#/components/schemas/MfaChallengeEnvelope"
              example:
                success: true
                data:
                  accessToken: "eyJ..."
                  expiresIn: 3600
                  user: { id: "u_01H...", email: "alice@acme.com", name: "Alice" }
                  tenant: { id: "t_01H...", name: "Acme Inc.", plan: "PRO" }
  /api/v1/auth/oauth/google:
    post:
      tags: [Auth]
      summary: Google ID-token sign-in
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [idToken]
              properties:
                idToken: { type: string }
                nonce: { type: string, nullable: true }
                rememberMe: { type: boolean }
      responses:
        "200":
          description: Authenticated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthTokensEnvelope" }
              example:
                success: true
                data:
                  accessToken: "eyJ..."
                  expiresIn: 3600
                  user: { id: "u_01H...", email: "alice@gmail.com", name: "Alice" }
                  tenant: null
  /api/v1/auth/oauth/apple:
    post:
      tags: [Auth]
      summary: Apple ID-token sign-in
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [idToken]
              properties:
                idToken: { type: string }
                code: { type: string, nullable: true }
                user:
                  type: object
                  nullable: true
                  properties:
                    name:
                      type: object
                      properties:
                        firstName: { type: string }
                        lastName: { type: string }
                nonce: { type: string, nullable: true }
                rememberMe: { type: boolean }
      responses:
        "200":
          description: Authenticated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthTokensEnvelope" }
              example:
                success: true
                data:
                  accessToken: "eyJ..."
                  expiresIn: 3600
                  user: { id: "u_01H...", email: "alice@privaterelay.appleid.com", name: "Alice" }
                  tenant: null
  /api/v1/auth/refresh:
    post:
      tags: [Auth]
      summary: Refresh the access token (refresh-token cookie required)
      security: []
      responses:
        "200":
          description: Refreshed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuthTokensEnvelope" }
              example:
                success: true
                data:
                  accessToken: "eyJ..."
                  expiresIn: 3600
                  user: { id: "u_01H..." }
                  tenant: { id: "t_01H..." }
  /api/v1/auth/logout:
    post:
      tags: [Auth]
      summary: Revoke the current session
      responses:
        "200":
          description: Logged out
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/auth/logout/all:
    post:
      tags: [Auth]
      summary: Revoke every session for the current user
      responses:
        "200":
          description: All sessions revoked
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/auth/verify-email:
    post:
      tags: [Auth]
      summary: Confirm an email-verification token from the welcome email
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        "200":
          description: Verified
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/auth/forgot-password:
    post:
      tags: [Auth]
      summary: Trigger a password-reset email
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        "200":
          description: Reset email queued (always returns success to avoid user enumeration)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/auth/reset-password:
    post:
      tags: [Auth]
      summary: Set a new password using a reset token
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, password]
              properties:
                token: { type: string }
                password: { type: string, minLength: 8 }
      responses:
        "200":
          description: Password updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  # ----------------------------------------------------------------- ME
  /api/v1/auth/me:
    get:
      tags: [Me]
      summary: Get the current user, their tenants, and active tenant
      responses:
        "200":
          description: Current user
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/MeResponse" }
              example:
                success: true
                data:
                  user: { id: "u_01H...", email: "alice@acme.com", name: "Alice", role: "OWNER" }
                  tenants: [{ id: "t_01H...", name: "Acme Inc.", plan: "PRO" }]
                  currentTenant: { id: "t_01H...", name: "Acme Inc.", plan: "PRO" }
    put:
      tags: [Me]
      summary: Update profile (name / avatar / locale)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                avatar: { type: string, nullable: true }
                locale: { type: string, nullable: true }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/User" }
              example:
                success: true
                data: { id: "u_01H...", email: "alice@acme.com", name: "Alice Doe", role: "OWNER" }
  /api/v1/auth/me/password:
    put:
      tags: [Me]
      summary: Change password (requires current password)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [currentPassword, newPassword]
              properties:
                currentPassword: { type: string }
                newPassword: { type: string, minLength: 8 }
      responses:
        "200":
          description: Changed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/auth/sessions:
    get:
      tags: [Me]
      summary: List active sessions for the current user
      responses:
        "200":
          description: Sessions
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Session" }
              example:
                success: true
                data:
                  - id: "sess_01H..."
                    device: "Chrome on macOS"
                    ip: "203.0.113.7"
                    location: "Hong Kong"
                    lastUsedAt: "2026-05-04T08:21:00Z"
                    isCurrent: true
  /api/v1/auth/sessions/{id}:
    delete:
      tags: [Me]
      summary: Revoke a session
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Revoked
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/me/usage:
    get:
      tags: [Me]
      summary: Current period plan-quota usage for the active tenant
      responses:
        "200":
          description: Usage snapshot
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/UsageMetrics" }
              example:
                success: true
                data:
                  crawlTasks: { used: 42, limit: 100 }
                  leads: { used: 1843, limit: 5000 }
                  contacts: { used: 5221, limit: 10000 }
                  aiTokens: { used: 281000, limit: 1000000 }
                  seats: { used: 4, limit: 10 }
  /api/v1/me/export:
    get:
      tags: [Me]
      summary: GDPR Article 20 portability export (streamed JSON)
      responses:
        "200":
          description: User-data export
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data: { format: "anvil-export-v1", generatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/me/delete:
    post:
      tags: [Me]
      summary: Begin GDPR Article 17 erasure (sends confirmation email)
      responses:
        "200":
          description: Confirmation email queued
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  # ----------------------------------------------------------------- TEAM
  /api/v1/team/members:
    get:
      tags: [Team]
      summary: List members of the active tenant
      responses:
        "200":
          description: Members
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/TeamMember" }
              example:
                success: true
                data:
                  - id: "tm_01H..."
                    userId: "u_01H..."
                    name: "Alice"
                    email: "alice@acme.com"
                    avatar: null
                    role: "OWNER"
                    status: "ACTIVE"
                    joinedAt: "2025-09-01T00:00:00Z"
                    lastActiveAt: "2026-05-06T07:00:00Z"
  /api/v1/team/invite:
    post:
      tags: [Team]
      summary: Invite a new member by email
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, role]
              properties:
                email: { type: string, format: email }
                role: { type: string, enum: [OWNER, ADMIN, MANAGER, MEMBER, VIEWER] }
      responses:
        "200":
          description: Invitation sent
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          invited: { type: string }
                          email:
                            type: object
                            properties:
                              sent: { type: boolean }
                              provider: { type: string }
              example:
                success: true
                data:
                  invited: "bob@acme.com"
                  email: { sent: true, provider: "resend" }
  /api/v1/team/invite/accept:
    post:
      tags: [Team]
      summary: Accept an invitation token (public — invitee may not yet belong to the tenant)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string }
      responses:
        "200":
          description: Accepted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data: { tenantId: "t_01H...", role: "MEMBER" }
  /api/v1/team/invites:
    get:
      tags: [Team]
      summary: List pending invitations
      responses:
        "200":
          description: Pending invites
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data:
                  - id: "inv_01H..."
                    email: "bob@acme.com"
                    role: "MEMBER"
                    invitedByName: "Alice"
                    createdAt: "2026-05-01T00:00:00Z"
                    expiresAt: "2026-05-08T00:00:00Z"
                    resendCount: 0
  /api/v1/team/invites/{id}/resend:
    post:
      tags: [Team]
      summary: Resend an invitation email
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Resent
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data: { id: "inv_01H...", resendCount: 1, expiresAt: "2026-05-13T00:00:00Z", email: { sent: true, provider: "resend" } }
  /api/v1/team/invites/{id}:
    delete:
      tags: [Team]
      summary: Revoke a pending invitation
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Revoked
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/team/members/{id}/role:
    put:
      tags: [Team]
      summary: Change a member's role
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [role]
              properties:
                role: { type: string, enum: [OWNER, ADMIN, MANAGER, MEMBER, VIEWER] }
      responses:
        "200":
          description: Role updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/TeamMember" }
              example:
                success: true
                data: { id: "tm_01H...", userId: "u_01H...", name: "Bob", role: "ADMIN", status: "ACTIVE", joinedAt: "2025-12-01T00:00:00Z" }
  /api/v1/team/members/{id}:
    delete:
      tags: [Team]
      summary: Remove a member from the tenant
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  # ----------------------------------------------------------------- CONTACTS
  /api/v1/contacts:
    get:
      tags: [Contacts]
      summary: List contacts (cursor-paginated)
      parameters:
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { name: search, in: query, schema: { type: string } }
        - { name: source, in: query, schema: { type: string } }
        - { name: stage, in: query, schema: { type: string } }
        - { name: platform, in: query, schema: { type: string } }
        - { name: country, in: query, schema: { type: string } }
        - { name: hasEmail, in: query, schema: { type: boolean } }
        - { name: owner, in: query, schema: { type: string } }
        - { name: minScore, in: query, schema: { type: number } }
        - { name: maxScore, in: query, schema: { type: number } }
      responses:
        "200":
          description: Page of contacts
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Contact" }
                      meta: { $ref: "#/components/schemas/PageMeta" }
              example:
                success: true
                data:
                  - id: "ct_01H..."
                    tenantId: "t_01H..."
                    name: "Bob Smith"
                    email: "bob@example.com"
                    phone: "+852 1234 5678"
                    company: "Example Co."
                    source: "CRAWLER"
                    platform: "LINKEDIN"
                    country: "HK"
                    tags: []
                    createdAt: "2026-04-01T00:00:00Z"
                    updatedAt: "2026-05-04T00:00:00Z"
                meta: { cursor: "eyJsYXN0SWQiOi...", hasMore: true, total: 5221 }
    post:
      tags: [Contacts]
      summary: Create a contact
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactCreate" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Contact" }
              example:
                success: true
                data: { id: "ct_01H...", name: "Charlie", email: "charlie@example.com", tenantId: "t_01H...", tags: [], source: "MANUAL", createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/contacts/{id}:
    get:
      tags: [Contacts]
      summary: Get a contact by id
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Contact
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Contact" }
              example:
                success: true
                data: { id: "ct_01H...", name: "Bob Smith", email: "bob@example.com", tenantId: "t_01H...", tags: [], source: "CRAWLER", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" }
    patch:
      tags: [Contacts]
      summary: Update a contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactCreate" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Contact" }
              example:
                success: true
                data: { id: "ct_01H...", name: "Bob S.", email: "bob@example.com", tenantId: "t_01H...", tags: [], createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
    delete:
      tags: [Contacts]
      summary: Delete a contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/contacts/import:
    post:
      tags: [Contacts]
      summary: Bulk-import contacts from CSV/Excel
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
      responses:
        "200":
          description: Import result
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { imported: 312, failed: 4 } }
  /api/v1/contacts/export:
    post:
      tags: [Contacts]
      summary: Export contacts to CSV (filtered)
      requestBody:
        required: false
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "200":
          description: CSV blob
          content:
            text/csv:
              schema: { type: string, format: binary }
  /api/v1/contacts/merge:
    post:
      tags: [Contacts]
      summary: Merge duplicate contacts into one canonical record
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [primaryId, duplicateIds]
              properties:
                primaryId: { type: string }
                duplicateIds:
                  type: array
                  items: { type: string }
      responses:
        "200":
          description: Merged
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Contact" }
              example:
                success: true
                data: { id: "ct_01H...", name: "Bob Smith", email: "bob@example.com", tenantId: "t_01H...", tags: [], createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/contacts/{id}/activities:
    get:
      tags: [Contacts]
      summary: List activities for a contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Activities
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Activity" }
              example:
                success: true
                data: []
  /api/v1/contacts/{id}/memory:
    get:
      tags: [Contacts]
      summary: Get AI memory notes for a contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Memory notes
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data:
                  - id: "mem_01H..."
                    contactId: "ct_01H..."
                    source: "manual"
                    content: "Prefers email, hates phone calls."
                    language: "en"
                    createdAt: "2026-05-04T00:00:00Z"
    post:
      tags: [Contacts]
      summary: Append an AI memory note
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content: { type: string }
                source: { type: string }
                language: { type: string }
      responses:
        "200":
          description: Note added
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data: { id: "mem_01H...", contactId: "ct_01H...", source: "manual", content: "Prefers email.", language: "en", createdAt: "2026-05-06T00:00:00Z" }
  /api/v1/contacts/{id}/profile:
    get:
      tags: [Contacts]
      summary: Get the AI-derived profile for a contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: AI profile
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data:
                  industry: "SaaS"
                  communicationTone: "Direct, technical"
                  painPoints: ["Manual lead enrichment"]
                  buyingSignals: ["Visited pricing page 3x"]
                  preferences: { contactChannel: "email" }
                  lastAiSummary: "VP Engineering at a 50-person SaaS in HK..."
                  lastUpdatedAt: "2026-05-04T00:00:00Z"
  /api/v1/contacts/{id}/profile/refresh:
    post:
      tags: [Contacts]
      summary: Force a refresh of the AI profile (re-runs DeepSeek pro)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Refreshed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data:
                  industry: "SaaS"
                  communicationTone: "Direct, technical"
                  painPoints: []
                  buyingSignals: []
                  preferences: {}
                  lastUpdatedAt: "2026-05-06T00:00:00Z"
  # ----------------------------------------------------------------- LEADS
  /api/v1/leads:
    get:
      tags: [Leads]
      summary: List leads
      parameters:
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer, default: 50 } }
        - { name: status, in: query, schema: { type: string, enum: [OPEN, WON, LOST, ARCHIVED] } }
        - { name: pipelineId, in: query, schema: { type: string } }
        - { name: assignedToId, in: query, schema: { type: string } }
      responses:
        "200":
          description: Leads page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Lead" }
                      meta: { $ref: "#/components/schemas/PageMeta" }
              example:
                success: true
                data:
                  - id: "ld_01H..."
                    tenantId: "t_01H..."
                    contactId: "ct_01H..."
                    pipelineId: "pl_01H..."
                    stageId: "ps_01H..."
                    status: "OPEN"
                    source: "CRAWLER"
                    score: 78
                    value: 12000
                    currency: "USD"
                    assignedToId: "u_01H..."
                    createdAt: "2026-04-01T00:00:00Z"
                    updatedAt: "2026-05-06T00:00:00Z"
                meta: { hasMore: false, total: 12 }
    post:
      tags: [Leads]
      summary: Create a lead
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                contactId: { type: string }
                pipelineId: { type: string }
                stageId: { type: string }
                source: { type: string }
                value: { type: number }
                currency: { type: string }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Lead" }
              example:
                success: true
                data: { id: "ld_01H...", tenantId: "t_01H...", contactId: "ct_01H...", pipelineId: null, stageId: null, status: "OPEN", source: "MANUAL", score: 0, value: null, currency: null, assignedToId: null, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/leads/{id}:
    get:
      tags: [Leads]
      summary: Get a lead
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Lead
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Lead" }
              example:
                success: true
                data: { id: "ld_01H...", tenantId: "t_01H...", contactId: "ct_01H...", pipelineId: "pl_01H...", stageId: "ps_01H...", status: "OPEN", source: "CRAWLER", score: 78, value: 12000, currency: "USD", assignedToId: "u_01H...", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
    patch:
      tags: [Leads]
      summary: Update a lead
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Lead" }
              example:
                success: true
                data: { id: "ld_01H...", tenantId: "t_01H...", contactId: "ct_01H...", pipelineId: "pl_01H...", stageId: "ps_01H...", status: "WON", source: "CRAWLER", score: 92, value: 12000, currency: "USD", assignedToId: "u_01H...", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
    delete:
      tags: [Leads]
      summary: Delete a lead
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/leads/{id}/stage:
    patch:
      tags: [Leads]
      summary: Move a lead to a different pipeline stage
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [stageId]
              properties:
                stageId: { type: string }
      responses:
        "200":
          description: Stage updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Lead" }
              example:
                success: true
                data: { id: "ld_01H...", tenantId: "t_01H...", contactId: "ct_01H...", pipelineId: "pl_01H...", stageId: "ps_02H...", status: "OPEN", source: "CRAWLER", score: 78, value: 12000, currency: "USD", assignedToId: "u_01H...", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/leads/{id}/assign:
    patch:
      tags: [Leads]
      summary: Assign a lead to a user
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId]
              properties:
                userId: { type: string }
      responses:
        "200":
          description: Assigned
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Lead" }
              example:
                success: true
                data: { id: "ld_01H...", tenantId: "t_01H...", contactId: "ct_01H...", pipelineId: "pl_01H...", stageId: "ps_01H...", status: "OPEN", source: "CRAWLER", score: 78, value: 12000, currency: "USD", assignedToId: "u_02H...", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/leads/auto-assign:
    post:
      tags: [Leads]
      summary: Auto-assign all unassigned leads using the round-robin rule
      responses:
        "200":
          description: Auto-assigned
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { assigned: 17 } }
  # ----------------------------------------------------------------- PIPELINES
  /api/v1/pipelines:
    get:
      tags: [Pipelines]
      summary: List pipelines for the active tenant
      responses:
        "200":
          description: Pipelines
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Pipeline" }
              example:
                success: true
                data:
                  - id: "pl_01H..."
                    tenantId: "t_01H..."
                    name: "Default sales pipeline"
                    isDefault: true
                    stages:
                      - { id: "ps_01H...", pipelineId: "pl_01H...", name: "New", order: 0, probability: 0.1, color: "#6366F1" }
                      - { id: "ps_02H...", pipelineId: "pl_01H...", name: "Qualified", order: 1, probability: 0.3, color: "#06B6D4" }
                    createdAt: "2025-09-01T00:00:00Z"
                    updatedAt: "2026-05-04T00:00:00Z"
    post:
      tags: [Pipelines]
      summary: Create a pipeline
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                isDefault: { type: boolean }
                stages:
                  type: array
                  items: { $ref: "#/components/schemas/PipelineStage" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Pipeline" }
              example:
                success: true
                data: { id: "pl_02H...", tenantId: "t_01H...", name: "Enterprise pipeline", isDefault: false, stages: [], createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/pipelines/{id}:
    get:
      tags: [Pipelines]
      summary: Get a pipeline
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Pipeline
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Pipeline" }
              example:
                success: true
                data: { id: "pl_01H...", tenantId: "t_01H...", name: "Default", isDefault: true, stages: [], createdAt: "2025-09-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" }
    patch:
      tags: [Pipelines]
      summary: Update a pipeline
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { type: object, additionalProperties: true }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Pipeline" }
              example:
                success: true
                data: { id: "pl_01H...", tenantId: "t_01H...", name: "Default v2", isDefault: true, stages: [], createdAt: "2025-09-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/pipelines/{id}/stats:
    get:
      tags: [Pipelines]
      summary: Funnel stats for a pipeline
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Stats
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data:
                  totalLeads: 142
                  byStage:
                    - { stageId: "ps_01H...", stageName: "New", count: 60, value: 480000 }
                    - { stageId: "ps_02H...", stageName: "Qualified", count: 50, value: 612000 }
                  winRate: 0.18
                  avgCycleDays: 21
  /api/v1/pipelines/{id}/stages:
    get:
      tags: [Pipelines]
      summary: List stages with per-stage lead counts (Kanban view)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Stages
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/PipelineStage" }
              example:
                success: true
                data: [{ id: "ps_01H...", pipelineId: "pl_01H...", name: "New", order: 0, probability: 0.1, color: "#6366F1" }]
    post:
      tags: [Pipelines]
      summary: Add a stage to a pipeline
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PipelineStage" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/PipelineStage" }
              example:
                success: true
                data: { id: "ps_03H...", pipelineId: "pl_01H...", name: "Negotiation", order: 2, probability: 0.6, color: "#F59E0B" }
  /api/v1/pipelines/{id}/stages/{stageId}:
    patch:
      tags: [Pipelines]
      summary: Update a stage
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: stageId, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PipelineStage" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/PipelineStage" }
              example:
                success: true
                data: { id: "ps_01H...", pipelineId: "pl_01H...", name: "New leads", order: 0, probability: 0.1, color: "#6366F1" }
    delete:
      tags: [Pipelines]
      summary: Delete a stage
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: stageId, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  # ----------------------------------------------------------------- ACTIVITIES
  /api/v1/activities:
    get:
      tags: [Activities]
      summary: List activities (timeline)
      parameters:
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: contactId, in: query, schema: { type: string } }
        - { name: leadId, in: query, schema: { type: string } }
        - { name: type, in: query, schema: { type: string } }
      responses:
        "200":
          description: Page of activities
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Activity" }
                      meta: { $ref: "#/components/schemas/PageMeta" }
              example:
                success: true
                data:
                  - id: "act_01H..."
                    tenantId: "t_01H..."
                    userId: "u_01H..."
                    contactId: "ct_01H..."
                    leadId: null
                    type: "EMAIL_SENT"
                    title: "Initial outreach"
                    content: "Hi Bob, ..."
                    metadata: {}
                    createdAt: "2026-05-04T00:00:00Z"
    post:
      tags: [Activities]
      summary: Log a manual activity
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, title]
              properties:
                type: { type: string }
                title: { type: string }
                content: { type: string }
                contactId: { type: string }
                leadId: { type: string }
                metadata:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Activity" }
              example:
                success: true
                data: { id: "act_02H...", tenantId: "t_01H...", userId: "u_01H...", contactId: "ct_01H...", leadId: null, type: "NOTE", title: "Follow up next week", content: null, metadata: {}, createdAt: "2026-05-06T00:00:00Z" }
  # ----------------------------------------------------------------- TAGS
  /api/v1/tags:
    get:
      tags: [Tags]
      summary: List tags
      responses:
        "200":
          description: Tags
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Tag" }
              example:
                success: true
                data: [{ id: "tag_01H...", tenantId: "t_01H...", name: "Hot", color: "#EF4444", usageCount: 23, createdAt: "2025-09-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" }]
    post:
      tags: [Tags]
      summary: Create a tag
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, color]
              properties:
                name: { type: string }
                color: { type: string }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Tag" }
              example:
                success: true
                data: { id: "tag_02H...", tenantId: "t_01H...", name: "VIP", color: "#10B981", usageCount: 0, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/tags/{id}:
    patch:
      tags: [Tags]
      summary: Update a tag
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                color: { type: string }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Tag" }
              example:
                success: true
                data: { id: "tag_01H...", tenantId: "t_01H...", name: "Hot lead", color: "#EF4444", usageCount: 23, createdAt: "2025-09-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
    delete:
      tags: [Tags]
      summary: Delete a tag
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  # ----------------------------------------------------------------- CHATBOTS
  /api/v1/chatbots:
    get:
      tags: [Chatbots]
      summary: List chatbots
      parameters:
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: enabled, in: query, schema: { type: boolean } }
      responses:
        "200":
          description: Bots
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Chatbot" }
                      meta: { $ref: "#/components/schemas/PageMeta" }
              example:
                success: true
                data: [{ id: "bot_01H...", tenantId: "t_01H...", name: "Support EN", platforms: ["WEBSITE"], knowledgeBaseIds: ["kb_01H..."], persona: "Friendly", model: "deepseek-v4-flash", temperature: 0.6, enabled: true, avatar: null, description: null, createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" }]
    post:
      tags: [Chatbots]
      summary: Create a chatbot
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Chatbot" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Chatbot" }
              example:
                success: true
                data: { id: "bot_02H...", tenantId: "t_01H...", name: "Sales bot", platforms: [], knowledgeBaseIds: [], persona: null, model: "deepseek-v4-flash", temperature: 0.7, enabled: false, avatar: null, description: null, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/chatbots/{id}:
    get:
      tags: [Chatbots]
      summary: Get a chatbot
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Bot
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Chatbot" }
              example:
                success: true
                data: { id: "bot_01H...", tenantId: "t_01H...", name: "Support EN", platforms: ["WEBSITE"], knowledgeBaseIds: [], persona: null, model: "deepseek-v4-flash", temperature: 0.6, enabled: true, avatar: null, description: null, createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" }
    patch:
      tags: [Chatbots]
      summary: Update a chatbot
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/Chatbot" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Chatbot" }
              example:
                success: true
                data: { id: "bot_01H...", tenantId: "t_01H...", name: "Support EN v2", platforms: ["WEBSITE"], knowledgeBaseIds: [], persona: null, model: "deepseek-v4-flash", temperature: 0.5, enabled: true, avatar: null, description: null, createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
    delete:
      tags: [Chatbots]
      summary: Delete a chatbot
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/chatbots/{id}/sessions:
    get:
      tags: [Chatbots]
      summary: List sessions for a chatbot
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Sessions
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [] }
  /api/v1/chatbots/{id}/test:
    post:
      tags: [Chatbots]
      summary: One-shot test message against a bot (no session persistence)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message]
              properties:
                message: { type: string }
      responses:
        "200":
          description: Bot reply
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { reply: "Hi! How can I help?", tokens: 124, latencyMs: 482 } }
  /api/v1/chatbots/{id}/stats:
    get:
      tags: [Chatbots]
      summary: Aggregate stats for a chatbot
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Stats
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { sessions: 1240, messages: 5310, avgRating: 4.5, resolutionRate: 0.71, takeoverRate: 0.08 } }
  # ----------------------------------------------------------------- KNOWLEDGE BASES
  /api/v1/knowledge-bases:
    get:
      tags: [Knowledge Bases]
      summary: List knowledge bases
      responses:
        "200":
          description: KBs
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/KnowledgeBase" }
              example:
                success: true
                data: [{ id: "kb_01H...", tenantId: "t_01H...", name: "Product docs", type: "DOCS", description: null, documentsCount: 42, lastTrainedAt: "2026-05-01T00:00:00Z", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" }]
    post:
      tags: [Knowledge Bases]
      summary: Create a knowledge base
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/KnowledgeBase" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/KnowledgeBase" }
              example:
                success: true
                data: { id: "kb_02H...", tenantId: "t_01H...", name: "Sales playbook", type: "DOCS", description: null, documentsCount: 0, lastTrainedAt: null, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
  /api/v1/knowledge-bases/{id}:
    get:
      tags: [Knowledge Bases]
      summary: Get a knowledge base
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: KB
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/KnowledgeBase" }
              example:
                success: true
                data: { id: "kb_01H...", tenantId: "t_01H...", name: "Product docs", type: "DOCS", description: null, documentsCount: 42, lastTrainedAt: "2026-05-01T00:00:00Z", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" }
    patch:
      tags: [Knowledge Bases]
      summary: Update a knowledge base
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/KnowledgeBase" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/KnowledgeBase" }
              example:
                success: true
                data: { id: "kb_01H...", tenantId: "t_01H...", name: "Product docs v2", type: "DOCS", description: null, documentsCount: 42, lastTrainedAt: "2026-05-01T00:00:00Z", createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" }
    delete:
      tags: [Knowledge Bases]
      summary: Delete a knowledge base
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/knowledge-bases/{id}/upload:
    post:
      tags: [Knowledge Bases]
      summary: Upload a source document (PDF / DOCX / TXT)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
      responses:
        "200":
          description: Indexed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { documentId: "doc_01H...", chunks: 132 } }
  /api/v1/knowledge-bases/{id}/test:
    post:
      tags: [Knowledge Bases]
      summary: Run a RAG query against a knowledge base
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [question]
              properties:
                question: { type: string }
      responses:
        "200":
          description: Answer
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { answer: "The default plan is FREE.", sources: [{ title: "Pricing FAQ", score: 0.92 }] } }
  /api/v1/knowledge-bases/{id}/stats:
    get:
      tags: [Knowledge Bases]
      summary: KB index statistics
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Stats
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { documents: 42, chunks: 5301, tokens: 1843200, sizeMb: 38.4 } }
  # ----------------------------------------------------------------- VOICE
  /api/v1/voice/voices:
    get:
      tags: [Voice]
      summary: List cloned voices
      responses:
        "200":
          description: Voices
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Voice" }
              example:
                success: true
                data: [{ id: "vc_01H...", elevenlabsVoiceId: "abc123", name: "Sales-EN-female", labels: { language: "en", gender: "female" }, sampleFileUrls: [], status: "READY", isPlatformDefault: false, createdAt: "2026-04-01T00:00:00Z" }]
  /api/v1/voice/voices/clone:
    post:
      tags: [Voice]
      summary: Clone a new voice from one or more audio samples
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
                gender: { type: string }
                age: { type: string }
                language: { type: string }
                files:
                  type: array
                  items: { type: string, format: binary }
      responses:
        "200":
          description: Cloned
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Voice" }
              example:
                success: true
                data: { id: "vc_02H...", elevenlabsVoiceId: "xyz", name: "Sales-EN-female-2", labels: { language: "en" }, sampleFileUrls: [], status: "PROCESSING", isPlatformDefault: false, createdAt: "2026-05-06T00:00:00Z" }
  /api/v1/voice/voices/{id}:
    delete:
      tags: [Voice]
      summary: Delete a cloned voice
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/voice/voices/{id}/preview:
    post:
      tags: [Voice]
      summary: Render a preview audio clip for a voice
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string }
      responses:
        "200":
          description: MP3 audio blob
          content:
            audio/mpeg:
              schema: { type: string, format: binary }
  /api/v1/voice/scripts:
    get:
      tags: [Voice]
      summary: List call scripts
      responses:
        "200":
          description: Scripts
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/VoiceScript" }
              example:
                success: true
                data: [{ id: "vs_01H...", name: "Cold opener", language: "en", openingLine: "Hi, this is...", objectionResponses: [], maxTurns: 8, maxDurationSeconds: 300, createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" }]
    post:
      tags: [Voice]
      summary: Create a call script
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VoiceScript" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/VoiceScript" }
              example: { success: true, data: { id: "vs_02H...", name: "Re-engage", language: "en", openingLine: "Hey, just checking in.", objectionResponses: [], maxTurns: 6, maxDurationSeconds: 240, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" } }
  /api/v1/voice/scripts/{id}:
    patch:
      tags: [Voice]
      summary: Update a script
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VoiceScript" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/VoiceScript" }
              example: { success: true, data: { id: "vs_01H...", name: "Cold opener v2", language: "en", openingLine: "Hi, ...", objectionResponses: [], maxTurns: 8, maxDurationSeconds: 300, createdAt: "2026-04-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" } }
    delete:
      tags: [Voice]
      summary: Delete a script
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/voice/campaigns:
    get:
      tags: [Voice]
      summary: List call campaigns
      responses:
        "200":
          description: Campaigns
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/VoiceCampaign" }
              example: { success: true, data: [] }
    post:
      tags: [Voice]
      summary: Create a campaign
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/VoiceCampaign" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/VoiceCampaign" }
              example: { success: true, data: { id: "vcamp_01H...", name: "May campaign", scriptId: "vs_01H...", voiceId: "vc_01H...", fromNumber: "+18005551212", status: "DRAFT", callWindow: { start: "09:00", end: "17:00" }, timezone: "Asia/Hong_Kong", maxConcurrent: 5, maxRetries: 2, stats: { totalContacts: 0, callsAttempted: 0, callsCompleted: 0, callsConverted: 0, conversionRate: 0, avgDurationSeconds: 0, costCents: 0 }, createdAt: "2026-05-06T00:00:00Z" } }
  /api/v1/voice/campaigns/{id}:
    get:
      tags: [Voice]
      summary: Get a campaign
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Campaign
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/VoiceCampaign" }
              example: { success: true, data: { id: "vcamp_01H...", name: "May campaign", scriptId: "vs_01H...", voiceId: "vc_01H...", fromNumber: "+18005551212", status: "RUNNING", callWindow: { start: "09:00", end: "17:00" }, timezone: "Asia/Hong_Kong", maxConcurrent: 5, maxRetries: 2, stats: { totalContacts: 200, callsAttempted: 50, callsCompleted: 30, callsConverted: 4, conversionRate: 0.13, avgDurationSeconds: 184, costCents: 1230 }, createdAt: "2026-05-06T00:00:00Z" } }
  /api/v1/voice/campaigns/{id}/start:
    post:
      tags: [Voice]
      summary: Start a campaign
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Started
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/VoiceCampaign" }
              example: { success: true, data: { id: "vcamp_01H...", name: "May", scriptId: "vs_01H...", voiceId: "vc_01H...", fromNumber: "+1...", status: "RUNNING", callWindow: { start: "09:00", end: "17:00" }, timezone: "Asia/Hong_Kong", maxConcurrent: 5, maxRetries: 2, stats: { totalContacts: 0, callsAttempted: 0, callsCompleted: 0, callsConverted: 0, conversionRate: 0, avgDurationSeconds: 0, costCents: 0 }, createdAt: "2026-05-06T00:00:00Z", startedAt: "2026-05-06T01:00:00Z" } }
  /api/v1/voice/campaigns/{id}/pause:
    post:
      tags: [Voice]
      summary: Pause a campaign
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Paused
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "vcamp_01H...", status: "PAUSED" } }
  /api/v1/voice/campaigns/{id}/stop:
    post:
      tags: [Voice]
      summary: Stop a campaign
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Stopped
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "vcamp_01H...", status: "STOPPED" } }
  /api/v1/voice/sessions:
    get:
      tags: [Voice]
      summary: List call sessions
      parameters:
        - { name: status, in: query, schema: { type: string } }
        - { name: outcome, in: query, schema: { type: string } }
        - { name: campaignId, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
      responses:
        "200":
          description: Sessions
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/VoiceSession" }
                      meta: { $ref: "#/components/schemas/PageMeta" }
              example: { success: true, data: [], meta: { total: 0, hasMore: false } }
  /api/v1/voice/sessions/{id}:
    get:
      tags: [Voice]
      summary: Get one call session
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Session
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/VoiceSession" }
              example: { success: true, data: { id: "vsess_01H...", phoneNumber: "+85291234567", direction: "OUTBOUND", status: "COMPLETED", startedAt: "2026-05-06T00:00:00Z", transcript: [], capturedContactInfo: { phones: [], wechats: [], emails: [] } } }
  /api/v1/voice/calls/test:
    post:
      tags: [Voice]
      summary: Place a one-shot test call
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [scriptId, voiceId, phoneNumber]
              properties:
                scriptId: { type: string }
                voiceId: { type: string }
                phoneNumber: { type: string }
                fromNumber: { type: string }
      responses:
        "200":
          description: Call placed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { sessionId: "vsess_02H...", twilioCallSid: "CAabcd1234" } }
  # ----------------------------------------------------------------- AGENTS
  /api/v1/agents:
    get:
      tags: [Agents]
      summary: List autonomous agents
      responses:
        "200":
          description: Agents
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Agent" }
              example:
                success: true
                data: [{ id: "ag_01H...", tenantId: "t_01H...", name: "Lead-finder", goal: "Find 50 SaaS leads in HK", platforms: ["LINKEDIN"], budgetCredits: 500, creditsUsed: 87, deadline: null, constraints: {}, autoApprove: false, status: "RUNNING", currentPlan: null, metrics: { leadsFound: 12, leadsContacted: 4, leadsResponded: 1, leadsConverted: 0, chatbotSessions: 0, contentGenerated: 0, avgIntentScore: 0.62, creditsUsed: 87 }, config: {}, createdAt: "2026-05-04T00:00:00Z", startedAt: "2026-05-04T00:00:00Z", completedAt: null }]
    post:
      tags: [Agents]
      summary: Create an agent (DRAFT)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, goal]
              properties:
                name: { type: string }
                goal: { type: string }
                platforms:
                  type: array
                  items: { type: string }
                budgetCredits: { type: number }
                deadline: { type: string, format: date-time }
                constraints: { type: object, additionalProperties: true }
                autoApprove: { type: boolean }
                templateId: { type: string }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Agent" }
              example:
                success: true
                data: { id: "ag_02H...", tenantId: "t_01H...", name: "New agent", goal: "Find leads", platforms: [], budgetCredits: 100, creditsUsed: 0, deadline: null, constraints: {}, autoApprove: false, status: "DRAFT", currentPlan: null, metrics: { leadsFound: 0, leadsContacted: 0, leadsResponded: 0, leadsConverted: 0, chatbotSessions: 0, contentGenerated: 0, avgIntentScore: 0, creditsUsed: 0 }, config: {}, createdAt: "2026-05-06T00:00:00Z", startedAt: null, completedAt: null }
  /api/v1/agents/{id}:
    get:
      tags: [Agents]
      summary: Get an agent (with logs and pending approvals)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Agent detail
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Agent" }
              example:
                success: true
                data: { id: "ag_01H...", tenantId: "t_01H...", name: "Lead-finder", goal: "Find leads", platforms: [], budgetCredits: 500, creditsUsed: 87, deadline: null, constraints: {}, autoApprove: false, status: "RUNNING", currentPlan: null, metrics: { leadsFound: 12, leadsContacted: 4, leadsResponded: 1, leadsConverted: 0, chatbotSessions: 0, contentGenerated: 0, avgIntentScore: 0.62, creditsUsed: 87 }, config: {}, createdAt: "2026-05-04T00:00:00Z", startedAt: "2026-05-04T00:00:00Z", completedAt: null }
    delete:
      tags: [Agents]
      summary: Delete an agent
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { deleted: true } }
  /api/v1/agents/{id}/start:
    post:
      tags: [Agents]
      summary: Start an agent
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Started
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Agent" }
              example: { success: true, data: { id: "ag_01H...", status: "RUNNING" } }
  /api/v1/agents/{id}/pause:
    post:
      tags: [Agents]
      summary: Pause an agent
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Paused
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "ag_01H...", status: "PAUSED" } }
  /api/v1/agents/{id}/stop:
    post:
      tags: [Agents]
      summary: Stop an agent
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Stopped
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "ag_01H...", status: "COMPLETED" } }
  /api/v1/agents/{id}/log:
    get:
      tags: [Agents]
      summary: List the agent's execution log
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200":
          description: Log entries
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [], meta: { total: 0 } }
  /api/v1/agents/{id}/approvals:
    get:
      tags: [Agents]
      summary: List approvals (pending or all)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: status, in: query, schema: { type: string, enum: [PENDING, APPROVED, REJECTED] } }
      responses:
        "200":
          description: Approvals
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [] }
  /api/v1/agents/{id}/approvals/{approvalId}/approve:
    post:
      tags: [Agents]
      summary: Approve a pending action
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: approvalId, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Approved
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "appr_01H...", status: "APPROVED", decidedAt: "2026-05-06T00:00:00Z" } }
  /api/v1/agents/{id}/approvals/{approvalId}/reject:
    post:
      tags: [Agents]
      summary: Reject a pending action
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
        - { name: approvalId, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Rejected
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "appr_01H...", status: "REJECTED", decidedAt: "2026-05-06T00:00:00Z" } }
  # ----------------------------------------------------------------- RESEARCH
  /api/v1/research/keywords:
    post:
      tags: [Research]
      summary: Discover keywords from a seed
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [seed]
              properties:
                seed: { type: string }
                platforms:
                  type: array
                  items: { type: string }
                lang: { type: string }
                limit: { type: integer }
      responses:
        "200":
          description: Keywords
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data:
                  - { keyword: "wireless earbuds", search_volume: 90400, competition: "high", competition_score: 0.81, trend: "up", trend_data: [], cpc_cny: 4.2, difficulty: 73, platforms: ["AMAZON"] }
  /api/v1/research/keywords/trending:
    get:
      tags: [Research]
      summary: Get currently-trending keywords across platforms
      responses:
        "200":
          description: Trending
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [] }
  /api/v1/research/bsr:
    post:
      tags: [Research]
      summary: Best-seller-rank analysis
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [category]
              properties:
                category: { type: string }
                platform: { type: string }
                limit: { type: integer }
      responses:
        "200":
          description: BSR products
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [] }
  /api/v1/research/blueocean:
    post:
      tags: [Research]
      summary: Find low-competition / high-margin niches
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [niche]
              properties:
                niche: { type: string }
                min_demand: { type: number }
                max_competition: { type: number }
                min_margin: { type: number }
                min_trend: { type: number }
      responses:
        "200":
          description: Opportunities
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [] }
  /api/v1/research/niche-score:
    post:
      tags: [Research]
      summary: Score a niche on demand / competition / margin / trend / barrier
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [niche]
              properties:
                niche: { type: string }
      responses:
        "200":
          description: Score
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { niche: "smart home", total_score: 78, dimensions: { demand: 84, competition: 60, margin: 72, trend: 88, barrier: 65 }, summary: "Mid-high opportunity", recommendation: "Launch within 60 days" } }
  /api/v1/research/competitor-reverse:
    post:
      tags: [Research]
      summary: Reverse-engineer a competitor store from a URL
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string }
      responses:
        "200":
          description: Competitor profile
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { store_name: "ExampleCo", platform: "AMAZON", total_products: 240, avg_price: 32.5, avg_rating: 4.3, estimated_monthly_revenue: 218000, top_products: [], top_keywords: [], ad_spend_estimate: 12000, pricing_strategy: "premium", strengths: [], weaknesses: [] } }
  # ----------------------------------------------------------------- INFLUENCERS
  /api/v1/influencers:
    get:
      tags: [Influencers]
      summary: Search influencers
      parameters:
        - { name: platform, in: query, schema: { type: string } }
        - { name: category, in: query, schema: { type: string } }
        - { name: minFollowers, in: query, schema: { type: integer } }
        - { name: maxFollowers, in: query, schema: { type: integer } }
        - { name: location, in: query, schema: { type: string } }
        - { name: language, in: query, schema: { type: string } }
        - { name: hasEmail, in: query, schema: { type: boolean } }
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
      responses:
        "200":
          description: KOL list
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [] }
  /api/v1/influencers/{id}:
    get:
      tags: [Influencers]
      summary: Get a KOL profile
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: KOL
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "kol_01H...", username: "creator", real_name: "Jane", platform: "INSTAGRAM", followers: 250000, engagement_rate: 0.043, has_email: true, email: "jane@example.com", verified: true } }
  /api/v1/influencers/discover:
    post:
      tags: [Influencers]
      summary: AI-driven KOL discovery from a niche brief
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [niche]
              properties:
                niche: { type: string }
                product_description: { type: string }
                budget: { type: number }
                target_platforms:
                  type: array
                  items: { type: string }
                limit: { type: integer }
      responses:
        "200":
          description: Recommendations
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { niche: "fitness", target_categories: ["health"], results: [], total_candidates: 0, search_timestamp: "2026-05-06T00:00:00Z" } }
  /api/v1/influencers/analyze:
    post:
      tags: [Influencers]
      summary: Deep-analyse a KOL by URL
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string }
      responses:
        "200":
          description: Full KOL audit
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { url: "https://...", platform: "INSTAGRAM", analysis_id: "kana_01H...", status: "COMPLETED", ai_recommendation: "Strong fit." } }
  /api/v1/influencers/{id}/outreach:
    post:
      tags: [Influencers]
      summary: Send outreach to a KOL
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [channel, message]
              properties:
                channel: { type: string, enum: [email, dm, whatsapp, line] }
                subject: { type: string }
                message: { type: string }
                campaign_id: { type: string }
                template_id: { type: string }
      responses:
        "200":
          description: Outreach queued
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "out_01H...", kol_id: "kol_01H...", kol_username: "creator", channel: "email", subject: "Partnership", message_preview: "Hi Jane...", status: "QUEUED", sent_at: "2026-05-06T00:00:00Z", campaign_id: null, delivery_status: "PENDING", opened: false, replied: false } }
  # ----------------------------------------------------------------- CRAWL
  /api/v1/crawl-tasks:
    get:
      tags: [Crawl]
      summary: List spider tasks
      parameters:
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: status, in: query, schema: { type: string } }
        - { name: platform, in: query, schema: { type: string } }
      responses:
        "200":
          description: Tasks
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/CrawlTask" }
                      meta: { $ref: "#/components/schemas/PageMeta" }
              example: { success: true, data: [], meta: { hasMore: false } }
    post:
      tags: [Crawl]
      summary: Create a spider task
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CrawlTask" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/CrawlTask" }
              example: { success: true, data: { id: "ct_03H...", tenantId: "t_01H...", name: "LI HK SaaS", platform: "LINKEDIN", keywords: ["saas"], status: "PENDING", progress: 0, config: {}, schedule: null, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z", startedAt: null, completedAt: null } }
  /api/v1/crawl-tasks/{id}:
    get:
      tags: [Crawl]
      summary: Get a task
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Task
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/CrawlTask" }
              example: { success: true, data: { id: "ct_03H...", tenantId: "t_01H...", name: "LI HK SaaS", platform: "LINKEDIN", keywords: ["saas"], status: "RUNNING", progress: 0.42, config: {}, schedule: null, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z", startedAt: "2026-05-06T00:01:00Z", completedAt: null } }
    patch:
      tags: [Crawl]
      summary: Update a task
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CrawlTask" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/CrawlTask" }
              example: { success: true, data: { id: "ct_03H...", tenantId: "t_01H...", name: "LI HK SaaS v2", platform: "LINKEDIN", keywords: ["saas", "fintech"], status: "RUNNING", progress: 0.42, config: {}, schedule: null, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:05:00Z", startedAt: "2026-05-06T00:01:00Z", completedAt: null } }
    delete:
      tags: [Crawl]
      summary: Delete a task
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/crawl-tasks/{id}/start:
    post:
      tags: [Crawl]
      summary: Start a task
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Started
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "ct_03H...", status: "RUNNING" } }
  /api/v1/crawl-tasks/{id}/pause:
    post:
      tags: [Crawl]
      summary: Pause a task
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Paused
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "ct_03H...", status: "PAUSED" } }
  /api/v1/crawl-tasks/{id}/stop:
    post:
      tags: [Crawl]
      summary: Stop a task
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Stopped
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "ct_03H...", status: "COMPLETED" } }
  /api/v1/crawl-results:
    get:
      tags: [Crawl]
      summary: List crawl results
      parameters:
        - { name: taskId, in: query, schema: { type: string } }
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
      responses:
        "200":
          description: Results
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [], meta: { hasMore: false } }
  /api/v1/platforms:
    get:
      tags: [Crawl]
      summary: List supported crawl platforms
      responses:
        "200":
          description: Platforms
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [{ key: "XIAOHONGSHU", label: "Xiaohongshu", spiders: 1 }, { key: "LINKEDIN", label: "LinkedIn", spiders: 2 }] }
  # ----------------------------------------------------------------- AI
  /api/v1/ai/chat:
    post:
      tags: [AI]
      summary: Generic chat-completion passthrough (DeepSeek)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messages]
              properties:
                messages:
                  type: array
                  items:
                    type: object
                    properties:
                      role: { type: string, enum: [USER, ASSISTANT, SYSTEM] }
                      content: { type: string }
                model: { type: string, enum: [deepseek-v4-pro, deepseek-v4-flash] }
                temperature: { type: number }
                stream: { type: boolean }
      responses:
        "200":
          description: Reply
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { reply: "Hello!", tokens: 12 } }
  /api/v1/ai/analyze:
    post:
      tags: [AI]
      summary: Generic analyze (intent / sentiment / classify / translate)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [task, input]
              properties:
                task: { type: string, enum: [intent, sentiment, classify, translate, keyword] }
                input: { type: string }
                params:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Result
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { task: "sentiment", score: 0.82, label: "positive" } }
  /api/v1/ai/content/generate:
    post:
      tags: [AI]
      summary: Generate marketing content (post / email / ad copy)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, brief]
              properties:
                type: { type: string, enum: [post, email, ad_copy, video_script] }
                brief: { type: string }
                tone: { type: string }
                language: { type: string }
                platform: { type: string }
      responses:
        "200":
          description: Generated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { content: "Stop wasting hours on lead enrichment...", variantCount: 1 } }
  # ----------------------------------------------------------------- AFFILIATE
  /api/v1/affiliate/apply:
    post:
      tags: [Affiliate]
      summary: Apply to the affiliate program
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payoutEmail, promotionChannels]
              properties:
                payoutEmail: { type: string, format: email }
                websiteUrl: { type: string }
                promotionChannels:
                  type: array
                  items: { type: string }
                description: { type: string }
                estimatedMau: { type: integer }
                parentCode: { type: string }
      responses:
        "200":
          description: Applied
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Affiliate" }
              example: { success: true, data: { id: "aff_01H...", code: "ALICE2026", status: "PENDING", tier: "STANDARD", commissionRate: 0.2, recurringMonths: 12, payoutEmail: "alice@acme.com", payoutMethod: null, websiteUrl: null, promotionChannels: ["youtube"], rejectionReason: null, totalClicks: 0, totalSignups: 0, totalConversions: 0, totalEarnings: 0, pendingEarnings: 0, paidEarnings: 0, createdAt: "2026-05-06T00:00:00Z" } }
  /api/v1/affiliate/me:
    get:
      tags: [Affiliate]
      summary: Get the current user's affiliate record
      responses:
        "200":
          description: Affiliate record
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { exists: true, affiliate: { id: "aff_01H...", code: "ALICE2026", status: "APPROVED", tier: "GOLD", commissionRate: 0.3, recurringMonths: 12, payoutEmail: "alice@acme.com", payoutMethod: "stripe", websiteUrl: null, promotionChannels: [], rejectionReason: null, totalClicks: 1284, totalSignups: 92, totalConversions: 14, totalEarnings: 3120, pendingEarnings: 220, paidEarnings: 2900, createdAt: "2025-12-01T00:00:00Z" } } }
  /api/v1/affiliate/stats:
    get:
      tags: [Affiliate]
      summary: Affiliate dashboard stats
      parameters:
        - { name: period, in: query, schema: { type: integer, default: 30 } }
      responses:
        "200":
          description: Stats
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { clicks: 1284, uniqueVisitors: 980, signups: 92, conversions: 14, mrr: 412, totalEarnings: 3120, pendingEarnings: 220, paidEarnings: 2900, conversionRate: 0.152, avgTicket: 222, dailyEarnings: [], sourceBreakdown: [] } }
  /api/v1/affiliate/referrals:
    get:
      tags: [Affiliate]
      summary: List referred tenants
      parameters:
        - { name: cursor, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: status, in: query, schema: { type: string, enum: [SIGNED_UP, TRIAL, PAYING, CANCELED] } }
      responses:
        "200":
          description: Referrals
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [], meta: { hasMore: false } }
  /api/v1/affiliate/commissions:
    get:
      tags: [Affiliate]
      summary: List earned commissions
      parameters:
        - { name: cursor, in: query, schema: { type: string } }
        - { name: status, in: query, schema: { type: string, enum: [PENDING, APPROVED, PAID, VOIDED] } }
      responses:
        "200":
          description: Commissions
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Commission" }
              example: { success: true, data: [] }
  /api/v1/affiliate/payout-request:
    post:
      tags: [Affiliate]
      summary: Request a payout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [amount, method, details]
              properties:
                amount: { type: number }
                method: { type: string, enum: [crypto, stripe, bank] }
                details:
                  type: object
                  additionalProperties: true
      responses:
        "200":
          description: Payout requested
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Payout" }
              example: { success: true, data: { id: "po_01H...", amount: 220, currency: "USD", method: "stripe", status: "PENDING", providerTxId: null, rejectionReason: null, approvedAt: null, paidAt: null, createdAt: "2026-05-06T00:00:00Z" } }
  /api/v1/affiliate/payouts:
    get:
      tags: [Affiliate]
      summary: List payouts
      responses:
        "200":
          description: Payouts
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Payout" }
              example: { success: true, data: [] }
  /api/v1/affiliate/track:
    post:
      tags: [Affiliate]
      summary: Public click-tracking ping (called from the landing page)
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code: { type: string }
                referrer: { type: string }
                ua: { type: string }
                landing: { type: string }
      responses:
        "200":
          description: Tracked
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { tracked: true } }
  # ----------------------------------------------------------------- TRADE DOCUMENTS
  /api/v1/trade-documents:
    get:
      tags: [Trade Documents]
      summary: List trade documents
      parameters:
        - { name: type, in: query, schema: { type: string, enum: [QUOTATION, PROFORMA, PACKING_LIST, COMMERCIAL_INVOICE, SALES_CONTRACT, BOL, AWB] } }
        - { name: status, in: query, schema: { type: string } }
      responses:
        "200":
          description: Docs
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/TradeDocument" }
              example: { success: true, data: [] }
    post:
      tags: [Trade Documents]
      summary: Create a trade document
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TradeDocument" }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/TradeDocument" }
              example: { success: true, data: { id: "td_01H...", type: "QUOTATION", number: "Q-2026-0001", status: "DRAFT", currency: "USD", total: 12000, lines: [], buyer: { name: "Buyer Co.", country: "US" }, seller: { name: "Seller Co.", country: "HK" }, terms: { incoterm: "FOB", paymentTerms: "T/T 30%" }, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" } }
  /api/v1/trade-documents/{id}:
    get:
      tags: [Trade Documents]
      summary: Get a document
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Doc
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/TradeDocument" }
              example: { success: true, data: { id: "td_01H...", type: "QUOTATION", number: "Q-2026-0001", status: "SENT", currency: "USD", total: 12000, lines: [], buyer: { name: "Buyer Co.", country: "US" }, seller: { name: "Seller Co.", country: "HK" }, terms: {}, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:01:00Z" } }
    patch:
      tags: [Trade Documents]
      summary: Update a document
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TradeDocument" }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/TradeDocument" }
              example: { success: true, data: { id: "td_01H...", type: "QUOTATION", number: "Q-2026-0001", status: "DRAFT", currency: "USD", total: 13000, lines: [], buyer: { name: "Buyer Co.", country: "US" }, seller: { name: "Seller Co.", country: "HK" }, terms: {}, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:05:00Z" } }
    delete:
      tags: [Trade Documents]
      summary: Delete a document
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/trade-documents/ai-draft-quote:
    post:
      tags: [Trade Documents]
      summary: AI-draft a quotation from a product list (stateless)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [products]
              properties:
                products:
                  type: array
                  items:
                    type: object
                    properties:
                      sku: { type: string }
                      name: { type: string }
                      unitPrice: { type: number }
                      qty: { type: integer }
                      currency: { type: string }
                      spec: { type: string }
                buyerHistory:
                  type: object
                  additionalProperties: true
                language: { type: string }
                instructions: { type: string }
      responses:
        "200":
          description: Draft
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { intro: "Dear Buyer,...", lines: [], terms: { incoterm: "FOB", paymentTerms: "T/T 30%", validityDays: 30 }, closing: "Best regards" } }
  # ----------------------------------------------------------------- WEBHOOKS
  /api/v1/webhooks:
    get:
      tags: [Webhooks]
      summary: List outgoing webhooks
      responses:
        "200":
          description: Webhooks
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Webhook" }
              example: { success: true, data: [] }
    post:
      tags: [Webhooks]
      summary: Register an outgoing webhook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events:
                  type: array
                  items: { type: string }
                secret: { type: string }
                enabled: { type: boolean }
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Webhook" }
              example: { success: true, data: { id: "wh_01H...", tenantId: "t_01H...", url: "https://example.com/hook", events: ["lead.created"], secret: "whsec_...", enabled: true, lastTriggeredAt: null, failureCount: 0, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" } }
  /api/v1/webhooks/{id}:
    patch:
      tags: [Webhooks]
      summary: Update a webhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/Webhook" }
              example: { success: true, data: { id: "wh_01H...", tenantId: "t_01H...", url: "https://example.com/hook", events: ["lead.created", "lead.won"], secret: "whsec_...", enabled: true, lastTriggeredAt: null, failureCount: 0, createdAt: "2026-05-06T00:00:00Z", updatedAt: "2026-05-06T01:00:00Z" } }
    delete:
      tags: [Webhooks]
      summary: Delete a webhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/webhooks/{id}/test:
    post:
      tags: [Webhooks]
      summary: Fire a test event at the webhook
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: Result
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { statusCode: 200, latencyMs: 142, success: true } }
  /webhooks/esign/{provider}:
    post:
      tags: [Webhooks]
      summary: Provider-signed e-signature webhook receiver
      security: []
      parameters:
        - name: provider
          in: path
          required: true
          schema: { type: string, enum: [docusign, hellosign, fadada, e_qianbao] }
      responses:
        "200":
          description: Ack
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { received: true } }
  /webhooks/wecom:
    post:
      tags: [Webhooks]
      summary: WeCom (WeChat Work) signed webhook
      security: []
      responses:
        "200":
          description: Ack
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { received: true } }
  /webhooks/sms/{provider}:
    post:
      tags: [Webhooks]
      summary: SMS DLR webhook (Twilio / Vonage / MessageBird)
      security: []
      parameters:
        - name: provider
          in: path
          required: true
          schema: { type: string, enum: [twilio, vonage, messagebird] }
      responses:
        "200":
          description: Ack
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { received: true } }
  /webhooks/zoom:
    post:
      tags: [Webhooks]
      summary: Zoom webhook
      security: []
      responses:
        "200":
          description: Ack
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { received: true } }
  /webhooks/google-meet:
    post:
      tags: [Webhooks]
      summary: Google Meet webhook
      security: []
      responses:
        "200":
          description: Ack
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { received: true } }
  /webhooks/ms-teams:
    post:
      tags: [Webhooks]
      summary: Microsoft Teams webhook
      security: []
      responses:
        "200":
          description: Ack
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { received: true } }
  # ----------------------------------------------------------------- BILLING
  /api/v1/billing/plans:
    get:
      tags: [Billing]
      summary: List available plans
      responses:
        "200":
          description: Plans
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example:
                success: true
                data:
                  - id: "plan_free"
                    name: "Free"
                    key: "FREE"
                    description: "Get started"
                    pricing: { monthly: 0, yearly: 0, currency: "USD" }
                    limits: { leads: 100, contacts: 500, seats: 1, aiTokens: 50000 }
                    features: ["basic crawler", "1 chatbot"]
                  - id: "plan_pro"
                    name: "Pro"
                    key: "PRO"
                    pricing: { monthly: 99, yearly: 990, currency: "USD" }
                    limits: { leads: 5000, contacts: 10000, seats: 10, aiTokens: 1000000 }
                    features: ["all crawlers", "10 chatbots", "voice campaigns"]
                    popular: true
  /api/v1/billing/subscription:
    get:
      tags: [Billing]
      summary: Get current subscription
      responses:
        "200":
          description: Subscription
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "sub_01H...", tenantId: "t_01H...", plan: "PRO", status: "active", interval: "monthly", currentPeriodStart: "2026-05-01T00:00:00Z", currentPeriodEnd: "2026-06-01T00:00:00Z", cancelAtPeriodEnd: false, canceledAt: null } }
  /api/v1/billing/checkout:
    post:
      tags: [Billing]
      summary: Open a Stripe / Alipay / Crypto checkout session
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [plan, interval]
              properties:
                plan: { type: string, enum: [FREE, STARTER, PRO, ENTERPRISE] }
                interval: { type: string, enum: [monthly, yearly] }
      responses:
        "200":
          description: Checkout URL
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { url: "https://checkout.stripe.com/...", sessionId: "cs_test_..." } }
  /api/v1/billing/cancel:
    post:
      tags: [Billing]
      summary: Cancel subscription at period end
      responses:
        "200":
          description: Cancelled
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "sub_01H...", tenantId: "t_01H...", plan: "PRO", status: "active", interval: "monthly", currentPeriodStart: "2026-05-01T00:00:00Z", currentPeriodEnd: "2026-06-01T00:00:00Z", cancelAtPeriodEnd: true, canceledAt: "2026-05-06T00:00:00Z" } }
  /api/v1/billing/usage:
    get:
      tags: [Billing]
      summary: Current period usage metrics
      responses:
        "200":
          description: Usage
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: { $ref: "#/components/schemas/UsageMetrics" }
              example:
                success: true
                data:
                  crawlTasks: { used: 42, limit: 100 }
                  leads: { used: 1843, limit: 5000 }
                  contacts: { used: 5221, limit: 10000 }
                  aiTokens: { used: 281000, limit: 1000000 }
                  seats: { used: 4, limit: 10 }
  /api/v1/billing/invoices:
    get:
      tags: [Billing]
      summary: List invoices
      responses:
        "200":
          description: Invoices
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items: { $ref: "#/components/schemas/Invoice" }
              example:
                success: true
                data:
                  - { id: "inv_01H...", tenantId: "t_01H...", number: "INV-2026-0001", amount: 99, currency: "USD", status: "paid", issuedAt: "2026-05-01T00:00:00Z", paidAt: "2026-05-01T00:00:01Z", downloadUrl: "https://..." }
  # ----------------------------------------------------------------- BRANDING
  /api/v1/branding/css:
    get:
      tags: [Branding]
      summary: Public per-tenant theme CSS (resolved by host header)
      security: []
      responses:
        "200":
          description: CSS
          content:
            text/css:
              schema: { type: string }
  /api/v1/branding/manifest:
    get:
      tags: [Branding]
      summary: Public branding manifest (logo, colours, social links)
      security: []
      parameters:
        - { name: X-Branding-Host, in: header, required: false, schema: { type: string } }
      responses:
        "200":
          description: Manifest
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { branded: true, brandName: "Acme", primaryColor: "#6366F1", accentColor: "#06B6D4", logoLight: "https://...", favicon: "https://..." } }
  /api/v1/branding:
    get:
      tags: [Branding]
      summary: Get the tenant branding profile
      responses:
        "200":
          description: Profile
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "br_01H...", tenantId: "t_01H...", brandName: "Acme", legalName: "Acme Inc.", primaryColor: "#6366F1", accentColor: "#06B6D4", customDomain: null, customDomainStatus: "NOT_SET", emailFromName: "Acme", emailFromAddress: "noreply@acme.com", hidePoweredBy: false, hideSupportChat: false, hideUpgradePrompts: false, socialLinks: {}, enabled: true, createdAt: "2025-09-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" } }
    put:
      tags: [Branding]
      summary: Update the branding profile
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "br_01H...", tenantId: "t_01H...", brandName: "Acme v2", primaryColor: "#0EA5E9", enabled: true, updatedAt: "2026-05-06T00:00:00Z" } }
  /api/v1/branding/publish:
    post:
      tags: [Branding]
      summary: Publish the branding profile
      responses:
        "200":
          description: Published
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { enabled: true, issues: [] } }
  /api/v1/branding/unpublish:
    post:
      tags: [Branding]
      summary: Unpublish the branding profile (revert to platform default)
      responses:
        "200":
          description: Unpublished
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { enabled: false } }
  /api/v1/branding/domain:
    post:
      tags: [Branding]
      summary: Configure a custom domain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain: { type: string }
      responses:
        "200":
          description: DNS instructions
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { domain: "app.acme.com", dnsTarget: "edge.anvilhk.com", verifyToken: "abc...", cname: { host: "app.acme.com", value: "edge.anvilhk.com" }, txt: { host: "_anvil.app.acme.com", value: "anvil-verify=abc..." }, status: "PENDING" } }
    delete:
      tags: [Branding]
      summary: Remove the custom domain
      responses:
        "200":
          description: Removed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SuccessEnvelope" }
              example: { success: true, data: { success: true } }
  /api/v1/branding/email-templates:
    get:
      tags: [Branding]
      summary: List branded email templates
      responses:
        "200":
          description: Templates
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: [] }
  /api/v1/branding/email-templates/{key}:
    get:
      tags: [Branding]
      summary: Get one email template
      parameters:
        - name: key
          in: path
          required: true
          schema: { type: string, enum: [welcome, invite, password_reset, verify_email, weekly_digest, lead_alert] }
      responses:
        "200":
          description: Template
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "et_01H...", profileId: "br_01H...", templateKey: "welcome", subject: "Welcome to Acme!", bodyHtml: "<h1>Welcome</h1>", bodyText: "Welcome", enabled: true, variables: ["{{name}}"], createdAt: "2025-09-01T00:00:00Z", updatedAt: "2026-05-04T00:00:00Z" } }
    put:
      tags: [Branding]
      summary: Update an email template
      parameters:
        - name: key
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                subject: { type: string }
                bodyHtml: { type: string }
                bodyText: { type: string }
                enabled: { type: boolean }
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiEnvelope" }
              example: { success: true, data: { id: "et_01H...", profileId: "br_01H...", templateKey: "welcome", subject: "Welcome!", bodyHtml: "<h1>Hi</h1>", bodyText: "Hi", enabled: true, variables: [], createdAt: "2025-09-01T00:00:00Z", updatedAt: "2026-05-06T00:00:00Z" } }
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
  schemas:
    ApiEnvelope:
      type: object
      properties:
        success: { type: boolean }
        data: { description: "Endpoint-specific payload" }
        error:
          oneOf:
            - { $ref: "#/components/schemas/ApiError" }
            - { type: "null" }
        meta:
          oneOf:
            - { $ref: "#/components/schemas/PageMeta" }
            - { type: object, additionalProperties: true }
            - { type: "null" }
      required: [success]
    ApiError:
      type: object
      properties:
        code:
          type: string
          description: Machine-readable error code (e.g. VALIDATION_ERROR, FORBIDDEN, RATE_LIMITED).
        message:
          type: string
          description: Human-readable message.
        details:
          type: object
          additionalProperties: true
          nullable: true
      required: [code, message]
    SuccessEnvelope:
      type: object
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          properties:
            success: { type: boolean, enum: [true] }
      required: [success, data]
    AuthTokensEnvelope:
      type: object
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          required: [accessToken, expiresIn, user]
          properties:
            accessToken: { type: string }
            expiresIn: { type: integer }
            user: { $ref: "#/components/schemas/User" }
            tenant:
              oneOf:
                - { $ref: "#/components/schemas/Tenant" }
                - { type: "null" }
      required: [success, data]
    MfaChallengeEnvelope:
      type: object
      properties:
        success: { type: boolean, enum: [true] }
        data:
          type: object
          required: [requires2fa, mfaToken, expiresIn]
          properties:
            requires2fa: { type: boolean, enum: [true] }
            mfaToken: { type: string }
            expiresIn: { type: integer }
      required: [success, data]
    PageMeta:
      type: object
      properties:
        cursor:
          type: string
          nullable: true
        hasMore: { type: boolean }
        total:
          type: integer
          nullable: true
    User:
      type: object
      properties:
        id: { type: string }
        email: { type: string, format: email, nullable: true }
        name: { type: string, nullable: true }
        avatar: { type: string, nullable: true }
        role:
          type: string
          enum: [OWNER, ADMIN, MANAGER, MEMBER, VIEWER]
          nullable: true
        locale: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
      required: [id]
    Tenant:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        slug: { type: string, nullable: true }
        plan:
          type: string
          enum: [FREE, STARTER, PRO, ENTERPRISE]
        createdAt: { type: string, format: date-time }
      required: [id, name, plan]
    TeamMember:
      type: object
      properties:
        id: { type: string }
        userId: { type: string }
        name: { type: string }
        email: { type: string, nullable: true }
        avatar: { type: string, nullable: true }
        role:
          type: string
          enum: [OWNER, ADMIN, MANAGER, MEMBER, VIEWER]
        status:
          type: string
          enum: [ACTIVE, PENDING, SUSPENDED]
        joinedAt: { type: string, format: date-time }
        lastActiveAt: { type: string, format: date-time, nullable: true }
      required: [id, userId, name, role, status, joinedAt]
    Session:
      type: object
      properties:
        id: { type: string }
        device: { type: string, nullable: true }
        ip: { type: string, nullable: true }
        location: { type: string, nullable: true }
        lastUsedAt: { type: string, format: date-time }
        isCurrent: { type: boolean }
      required: [id, lastUsedAt, isCurrent]
    Contact:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        name: { type: string }
        email: { type: string, format: email, nullable: true }
        phone: { type: string, nullable: true }
        company: { type: string, nullable: true }
        title: { type: string, nullable: true }
        avatar: { type: string, nullable: true }
        source:
          type: string
          enum: [CRAWLER, AI_CHAT, MANUAL, IMPORT, API, WEBSITE, REFERRAL]
          nullable: true
        platform:
          type: string
          nullable: true
        country: { type: string, nullable: true }
        city: { type: string, nullable: true }
        stage: { type: string, nullable: true }
        score:
          oneOf:
            - { type: number }
            - { type: "null" }
        tags:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              color: { type: string, nullable: true }
        owner:
          type: object
          nullable: true
          properties:
            id: { type: string }
            name: { type: string }
            avatar: { type: string, nullable: true }
        leadCount: { type: integer, nullable: true }
        activityCount: { type: integer, nullable: true }
        lastActivityAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, tenantId, name, createdAt, updatedAt]
    ContactCreate:
      type: object
      properties:
        name: { type: string }
        email: { type: string, format: email, nullable: true }
        phone: { type: string, nullable: true }
        company: { type: string, nullable: true }
        title: { type: string, nullable: true }
        source: { type: string, nullable: true }
        platform: { type: string, nullable: true }
        country: { type: string, nullable: true }
        city: { type: string, nullable: true }
        tagIds:
          type: array
          items: { type: string }
    Lead:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        contactId: { type: string }
        pipelineId: { type: string, nullable: true }
        stageId: { type: string, nullable: true }
        status:
          type: string
          enum: [OPEN, WON, LOST, ARCHIVED]
        source:
          type: string
          enum: [CRAWLER, AI_CHAT, MANUAL, IMPORT, API, WEBSITE, REFERRAL]
        score: { type: number }
        value: { type: number, nullable: true }
        currency: { type: string, nullable: true }
        assignedToId: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, tenantId, contactId, status, source, score, createdAt, updatedAt]
    Pipeline:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        name: { type: string }
        isDefault: { type: boolean }
        stages:
          type: array
          items: { $ref: "#/components/schemas/PipelineStage" }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, tenantId, name, isDefault, stages]
    PipelineStage:
      type: object
      properties:
        id: { type: string }
        pipelineId: { type: string }
        name: { type: string }
        order: { type: integer }
        probability: { type: number }
        color: { type: string, nullable: true }
      required: [id, pipelineId, name, order, probability]
    Activity:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        userId: { type: string, nullable: true }
        contactId: { type: string, nullable: true }
        leadId: { type: string, nullable: true }
        type: { type: string }
        title: { type: string }
        content: { type: string, nullable: true }
        metadata:
          type: object
          additionalProperties: true
        createdAt: { type: string, format: date-time }
      required: [id, tenantId, type, title, createdAt]
    Tag:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        name: { type: string }
        color: { type: string }
        usageCount: { type: integer }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, tenantId, name, color]
    Chatbot:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        name: { type: string }
        avatar: { type: string, nullable: true }
        description: { type: string, nullable: true }
        platforms:
          type: array
          items: { type: string }
        knowledgeBaseIds:
          type: array
          items: { type: string }
        persona: { type: string, nullable: true }
        model:
          type: string
          enum: [deepseek-v4-pro, deepseek-v4-flash]
        temperature: { type: number }
        enabled: { type: boolean }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, tenantId, name, model]
    KnowledgeBase:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        name: { type: string }
        type: { type: string }
        description: { type: string, nullable: true }
        documentsCount: { type: integer }
        lastTrainedAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, tenantId, name, type]
    ChatSession:
      type: object
      properties:
        id: { type: string }
        botId: { type: string }
        contactId: { type: string, nullable: true }
        platform: { type: string }
        status:
          type: string
          enum: [ACTIVE, WAITING, HUMAN_TAKEOVER, CLOSED, ARCHIVED]
        messageCount: { type: integer }
        lastMessageAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
      required: [id, botId, status]
    Voice:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string, nullable: true }
        elevenlabsVoiceId: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        labels:
          type: object
          additionalProperties: { type: string }
        sampleFileUrls:
          type: array
          items: { type: string }
        status:
          type: string
          enum: [READY, PROCESSING, FAILED]
        isPlatformDefault: { type: boolean }
        previewUrl: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
      required: [id, name, status]
    VoiceScript:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string, nullable: true }
        name: { type: string }
        language: { type: string }
        objective: { type: string, nullable: true }
        openingLine: { type: string }
        closingLine: { type: string, nullable: true }
        systemPrompt: { type: string, nullable: true }
        objectionResponses:
          type: array
          items:
            type: object
            properties:
              trigger: { type: string }
              response: { type: string }
        maxTurns: { type: integer }
        maxDurationSeconds: { type: integer }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, name, language, openingLine]
    VoiceCampaign:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string, nullable: true }
        name: { type: string }
        scriptId: { type: string }
        voiceId: { type: string }
        fromNumber: { type: string }
        status:
          type: string
          enum: [DRAFT, SCHEDULED, RUNNING, PAUSED, COMPLETED, STOPPED]
        scheduleCron: { type: string, nullable: true }
        callWindow:
          type: object
          properties:
            start: { type: string }
            end: { type: string }
        timezone: { type: string }
        maxConcurrent: { type: integer }
        maxRetries: { type: integer }
        stats:
          type: object
          properties:
            totalContacts: { type: integer }
            callsAttempted: { type: integer }
            callsCompleted: { type: integer }
            callsConverted: { type: integer }
            conversionRate: { type: number }
            avgDurationSeconds: { type: number }
            costCents: { type: integer }
        createdAt: { type: string, format: date-time }
        startedAt: { type: string, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
      required: [id, name, scriptId, voiceId, fromNumber, status]
    VoiceSession:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string, nullable: true }
        campaignId: { type: string, nullable: true }
        contactId: { type: string, nullable: true }
        twilioCallSid: { type: string, nullable: true }
        phoneNumber: { type: string }
        direction:
          type: string
          enum: [OUTBOUND, INBOUND]
        status:
          type: string
          enum: [INITIATED, RINGING, IN_PROGRESS, COMPLETED, FAILED, BUSY, NO_ANSWER, VOICEMAIL, CANCELED]
        outcome:
          type: string
          enum: [CONVERTED, INTERESTED, CALLBACK, NOT_INTERESTED, DO_NOT_CALL, WRONG_NUMBER, VOICEMAIL, NO_ANSWER, FAILED]
          nullable: true
        startedAt: { type: string, format: date-time }
        connectedAt: { type: string, format: date-time, nullable: true }
        endedAt: { type: string, format: date-time, nullable: true }
        durationSeconds: { type: integer, nullable: true }
        recordingUrl: { type: string, nullable: true }
        transcript:
          type: array
          items:
            type: object
            properties:
              speaker:
                type: string
                enum: [AI, LEAD, SYSTEM]
              content: { type: string }
              timestamp: { type: string, format: date-time }
              offsetSeconds: { type: number }
              confidence: { type: number, nullable: true }
        summary: { type: string, nullable: true }
        intentScore: { type: number, nullable: true }
        sentiment: { type: number, nullable: true }
        capturedContactInfo:
          type: object
          properties:
            phones:
              type: array
              items: { type: string }
            wechats:
              type: array
              items: { type: string }
            emails:
              type: array
              items: { type: string }
            company: { type: string, nullable: true }
            role: { type: string, nullable: true }
        costCents: { type: integer, nullable: true }
        errorCode: { type: string, nullable: true }
        errorMessage: { type: string, nullable: true }
      required: [id, phoneNumber, direction, status, startedAt]
    Agent:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        name: { type: string }
        goal: { type: string }
        platforms:
          type: array
          items: { type: string }
        budgetCredits: { type: number }
        creditsUsed: { type: number }
        deadline: { type: string, format: date-time, nullable: true }
        constraints:
          type: object
          additionalProperties: true
        autoApprove: { type: boolean }
        status:
          type: string
          enum: [DRAFT, PLANNING, RUNNING, PAUSED, COMPLETED, FAILED]
        currentPlan:
          type: object
          nullable: true
          additionalProperties: true
        metrics:
          type: object
          properties:
            leadsFound: { type: integer }
            leadsContacted: { type: integer }
            leadsResponded: { type: integer }
            leadsConverted: { type: integer }
            chatbotSessions: { type: integer }
            contentGenerated: { type: integer }
            avgIntentScore: { type: number }
            creditsUsed: { type: number }
        config:
          type: object
          additionalProperties: true
        createdAt: { type: string, format: date-time }
        startedAt: { type: string, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
      required: [id, tenantId, name, goal, status]
    Affiliate:
      type: object
      properties:
        id: { type: string }
        code: { type: string }
        status:
          type: string
          enum: [PENDING, APPROVED, REJECTED, SUSPENDED]
        tier:
          type: string
          enum: [STANDARD, GOLD, PLATINUM]
        commissionRate: { type: number }
        recurringMonths: { type: integer }
        payoutEmail: { type: string, format: email, nullable: true }
        payoutMethod: { type: string, nullable: true }
        websiteUrl: { type: string, nullable: true }
        promotionChannels:
          type: array
          items: { type: string }
        rejectionReason: { type: string, nullable: true }
        totalClicks: { type: integer }
        totalSignups: { type: integer }
        totalConversions: { type: integer }
        totalEarnings: { type: number }
        pendingEarnings: { type: number }
        paidEarnings: { type: number }
        createdAt: { type: string, format: date-time }
      required: [id, code, status, tier]
    Commission:
      type: object
      properties:
        id: { type: string }
        amount: { type: number }
        currency: { type: string }
        rate: { type: number }
        type:
          type: string
          enum: [FIRST_SALE, RECURRING, SECOND_TIER]
        status:
          type: string
          enum: [PENDING, APPROVED, PAID, VOIDED]
        periodMonth: { type: string }
        approvedAt: { type: string, format: date-time, nullable: true }
        paidAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
        tenantName: { type: string }
        plan: { type: string, nullable: true }
      required: [id, amount, currency, status, type]
    Payout:
      type: object
      properties:
        id: { type: string }
        amount: { type: number }
        currency: { type: string }
        method: { type: string }
        status:
          type: string
          enum: [PENDING, APPROVED, PROCESSING, PAID, REJECTED, FAILED]
        providerTxId: { type: string, nullable: true }
        rejectionReason: { type: string, nullable: true }
        approvedAt: { type: string, format: date-time, nullable: true }
        paidAt: { type: string, format: date-time, nullable: true }
        createdAt: { type: string, format: date-time }
      required: [id, amount, currency, method, status]
    TradeDocument:
      type: object
      properties:
        id: { type: string }
        type:
          type: string
          enum: [QUOTATION, PROFORMA, PACKING_LIST, COMMERCIAL_INVOICE, SALES_CONTRACT, BOL, AWB]
        number: { type: string }
        status:
          type: string
          enum: [DRAFT, SENT, ACCEPTED, DECLINED, VOIDED]
        currency: { type: string }
        total: { type: number }
        lines:
          type: array
          items:
            type: object
            additionalProperties: true
        buyer:
          type: object
          additionalProperties: true
        seller:
          type: object
          additionalProperties: true
        terms:
          type: object
          additionalProperties: true
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, type, status]
    Webhook:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        url: { type: string, format: uri }
        events:
          type: array
          items: { type: string }
        secret: { type: string, nullable: true }
        enabled: { type: boolean }
        lastTriggeredAt: { type: string, format: date-time, nullable: true }
        failureCount: { type: integer }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
      required: [id, tenantId, url, events, enabled]
    Invoice:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        number: { type: string }
        amount: { type: number }
        currency: { type: string }
        status: { type: string }
        issuedAt: { type: string, format: date-time }
        paidAt: { type: string, format: date-time, nullable: true }
        downloadUrl: { type: string, nullable: true }
      required: [id, tenantId, number, amount, currency, status, issuedAt]
    UsageMetrics:
      type: object
      properties:
        crawlTasks:
          type: object
          properties:
            used: { type: integer }
            limit: { type: integer }
        leads:
          type: object
          properties:
            used: { type: integer }
            limit: { type: integer }
        contacts:
          type: object
          properties:
            used: { type: integer }
            limit: { type: integer }
        aiTokens:
          type: object
          properties:
            used: { type: integer }
            limit: { type: integer }
        seats:
          type: object
          properties:
            used: { type: integer }
            limit: { type: integer }
    MeResponse:
      type: object
      properties:
        user: { $ref: "#/components/schemas/User" }
        tenants:
          type: array
          items: { $ref: "#/components/schemas/Tenant" }
        currentTenant:
          oneOf:
            - { $ref: "#/components/schemas/Tenant" }
            - { type: "null" }
      required: [user, tenants]
    CrawlTask:
      type: object
      properties:
        id: { type: string }
        tenantId: { type: string }
        name: { type: string }
        platform:
          type: string
          enum:
            - XIAOHONGSHU
            - DOUYIN
            - WEIBO
            - BILIBILI
            - KUAISHOU
            - ZHIHU
            - TAOBAO
            - JD
            - PINDUODUO
            - WECHAT_MP
            - INSTAGRAM
            - TIKTOK
            - TWITTER
            - FACEBOOK
            - YOUTUBE
            - LINKEDIN
            - REDDIT
            - PINTEREST
            - AMAZON
            - SHOPIFY
        keywords:
          type: array
          items: { type: string }
        status:
          type: string
          enum: [PENDING, RUNNING, PAUSED, COMPLETED, FAILED, SCHEDULED]
        progress: { type: number }
        config:
          type: object
          additionalProperties: true
        schedule: { type: string, nullable: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        startedAt: { type: string, format: date-time, nullable: true }
        completedAt: { type: string, format: date-time, nullable: true }
      required: [id, tenantId, name, platform, status]
