openapi: 3.1.0
info:
  title: Winnr API
  description: |
    The Winnr API provides programmatic access to manage email domains, email users,
    and inbox operations. Use this API to integrate Winnr's email infrastructure
    into your applications.

    ## Authentication

    All API requests require authentication using an API token.

    API tokens start with `wnr_` and can be created in your account settings
    or via the `POST /v1/auth/tokens` endpoint.

    Include your token in the `Authorization` header:
    ```
    Authorization: Bearer wnr_xxx...
    ```

    ## Rate Limiting

    API requests are rate limited based on your account tier:

    | Plan | Requests/min | Burst |
    |------|--------------|-------|
    | Startup | 300 | 500 |
    | Enterprise | 500 | 800 |

    Rate limit headers are included in all responses:
    - `X-RateLimit-Limit` - Your per-minute limit
    - `X-RateLimit-Remaining` - Requests remaining in current window
    - `X-RateLimit-Reset` - Unix timestamp when the window resets

    ## Pagination

    List endpoints support cursor-based pagination:
    - `limit` - Number of items per page (default: 25, max: 100)
    - `cursor` - Cursor for next page (from previous response)

  version: 1.0.0
  contact:
    name: Winnr Support
    email: support@winnr.app
  license:
    name: Proprietary
    url: https://winnr.app/terms

servers:
  - url: https://api.winnr.app
    description: Production API
  - url: https://c8qc9z97f5.execute-api.us-east-1.amazonaws.com
    description: Production API (direct)
  - url: http://localhost:3000
    description: Local development

security:
  - bearerAuth: []

tags:
  - name: Account
    description: Account management operations
  - name: Team Users
    description: Team user management (account members with dashboard access)
  - name: Tokens
    description: API token management
  - name: Domains
    description: Email domain operations
  - name: Email Users
    description: Email user (mailbox) operations
  - name: Email User Templates
    description: Reusable email user persona templates (name + username combos)
  - name: Inbox
    description: Email inbox operations
  - name: Jobs
    description: Async job status
  - name: Export
    description: Data export operations (rate limited to 1 per 5 seconds)

paths:
  /v1/account:
    get:
      tags: [Account]
      summary: Get account details
      description: Returns the authenticated account's details including limits and configuration.
      operationId: getAccount
      responses:
        '200':
          description: Account details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/account/usage:
    get:
      tags: [Account]
      summary: Get usage statistics
      description: Returns current usage statistics and limits for the account.
      operationId: getAccountUsage
      responses:
        '200':
          description: Usage statistics
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/account/users:
    get:
      tags: [Team Users]
      summary: List team users
      description: |
        Returns all team users (dashboard members) for the authenticated account.
        Users are sorted by role (owner first, then admins, then members) then by email.
      operationId: listTeamUsers
      responses:
        '200':
          description: List of team users
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      $ref: '#/components/schemas/TeamUser'
                  total:
                    type: integer
                    description: Total number of team users
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/account/users/invite:
    post:
      tags: [Team Users]
      summary: Create/invite team user
      description: |
        Creates a new team user with access to the dashboard.
        
        If a password is provided, sets it directly. Otherwise, generates a temporary
        password and returns a password reset link that can be sent to the user.
        
        Creates the user account and user document.
      operationId: inviteTeamUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
                  description: User's email address
                password:
                  type: string
                  minLength: 6
                  description: Password (optional - if not provided, a reset link is generated)
                first_name:
                  type: string
                  maxLength: 100
                  description: First name
                last_name:
                  type: string
                  maxLength: 100
                  description: Last name
                role:
                  type: string
                  enum: [admin, member]
                  default: member
                  description: User role (admin or member)
      responses:
        '200':
          description: User created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: string
                    description: User ID
                  email:
                    type: string
                  role:
                    type: string
                  first_name:
                    type: string
                  last_name:
                    type: string
                  reset_link:
                    type: string
                    description: Password reset link (only if password not provided)
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: User limit reached or email already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /v1/account/users/{user_id}:
    patch:
      tags: [Team Users]
      summary: Update team user
      description: |
        Updates a team user's details including email, password, name, and role.
        
        Cannot modify the account owner (superadmin).
      operationId: updateTeamUser
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: string
          description: User ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                  description: New email address
                password:
                  type: string
                  minLength: 6
                  description: New password
                first_name:
                  type: string
                  maxLength: 100
                  description: First name
                last_name:
                  type: string
                  maxLength: 100
                  description: Last name
                role:
                  type: string
                  enum: [admin, member]
                  description: User role (admin or member)
      responses:
        '200':
          description: User updated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: string
                  email:
                    type: string
                  display_name:
                    type: string
                  first_name:
                    type: string
                  last_name:
                    type: string
                  role:
                    type: string
                  is_admin:
                    type: boolean
                  is_superadmin:
                    type: boolean
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: User does not belong to this account or is account owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Team Users]
      summary: Delete team user
      description: |
        Removes a team user from the account.
        Cannot delete the account owner (superadmin).
      operationId: deleteTeamUser
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: string
          description: User ID
      responses:
        '200':
          description: User deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  id:
                    type: string
                  deleted:
                    type: boolean
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: User does not belong to this account or is account owner
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/auth/tokens:
    get:
      tags: [Tokens]
      summary: List API tokens
      description: List all API tokens for the account.
      operationId: listTokens
      responses:
        '200':
          description: List of tokens
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Tokens]
      summary: Create API token
      description: |
        Create a new API token. The full token is only returned once at creation time.
        Store it securely as it cannot be retrieved again.
      operationId: createToken
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTokenRequest'
      responses:
        '201':
          description: Token created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenCreatedResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/auth/tokens/{token_id}:
    delete:
      tags: [Tokens]
      summary: Revoke API token
      description: Revoke an API token. This action is irreversible.
      operationId: revokeToken
      parameters:
        - $ref: '#/components/parameters/TokenId'
      responses:
        '200':
          description: Token revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Token revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains:
    get:
      tags: [Domains]
      summary: List domains
      description: List all domains for the account.
      operationId: listDomains
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: List of domains
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/domains/search:
    get:
      tags: [Domains]
      summary: Search domain availability
      description: Check if a domain is available for purchase and get pricing.
      operationId: searchDomains
      parameters:
        - name: q
          in: query
          required: true
          schema:
            type: string
          description: Domain name to search
      responses:
        '200':
          description: Domain availability result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainSearchResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/domains/search-bulk:
    post:
      tags: [Domains]
      summary: Bulk search domain availability
      description: |
        Check availability and pricing for multiple domains at once.
        Maximum 100 domains per request.
      operationId: searchDomainsBulk
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              properties:
                domains:
                  type: array
                  items:
                    type: string
                  maxItems: 100
                  description: List of domain names to search
      responses:
        '200':
          description: Bulk domain availability results
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        domain:
                          type: string
                        available:
                          type: boolean
                          nullable: true
                        price:
                          type: number
                          nullable: true
                        error:
                          type: string
                          nullable: true
                  count:
                    type: integer
                    description: Total domains searched
                  available_count:
                    type: integer
                    description: Number of available domains
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/domains/suggest:
    get:
      tags: [Domains]
      summary: Get domain suggestions
      description: Get domain name suggestions based on keywords.
      operationId: suggestDomains
      parameters:
        - name: keywords
          in: query
          required: true
          schema:
            type: string
          description: Keywords for suggestions
      responses:
        '200':
          description: Domain suggestions
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainSuggestResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/domains/connect:
    post:
      tags: [Domains]
      summary: Connect external domains
      description: |
        Connect one or more domains you already own to your Winnr account.

        **Standard flow:** Creates DNS zones and returns nameservers that you must
        configure at your domain registrar. After updating nameservers, use
        `POST /v1/domains/check-ns` to verify and trigger provisioning.

        **Cloudflare flow:** If your domains are already on Cloudflare, you can pass
        a `cloudflare_api_token` with Zone:DNS:Edit and Zone:Zone:Edit permissions.
        We'll create the necessary DNS records (MX, SPF, DKIM) directly in your
        Cloudflare account — no nameserver changes needed. Provisioning starts
        immediately.
      operationId: connectDomains
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              properties:
                domains:
                  type: array
                  items:
                    type: string
                  maxItems: 100
                  description: List of domain names to connect
                  example: ["example.com", "anotherdomain.com"]
                cloudflare_api_token:
                  type: string
                  description: |
                    Optional Cloudflare API token. When provided, skips CloudDNS zone
                    creation and nameserver changes. Instead, DNS records are created
                    directly in your Cloudflare account and provisioning starts immediately.
                    The token needs Zone:DNS:Edit and Zone:Zone:Edit permissions.
      responses:
        '200':
          description: Domains connected
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items:
                      type: object
                      properties:
                        domain:
                          type: string
                        domain_id:
                          type: string
                          nullable: true
                        status:
                          type: string
                          enum: [created, already_exists, error]
                        dns_provider:
                          type: string
                        nameservers:
                          type: array
                          items:
                            type: string
                        error:
                          type: string
                          nullable: true
                  nameservers:
                    type: array
                    items:
                      type: string
                    description: Nameservers to set at your registrar (empty when using Cloudflare token)
                  domains_used:
                    type: integer
                  domain_limit:
                    type: integer
                  cloudflare_mode:
                    type: boolean
                    description: Whether Cloudflare fast-path was used
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Domain limit exceeded

  /v1/domains/check-provider:
    post:
      tags: [Domains]
      summary: Check domain registrar
      description: |
        Check if a domain is registered at Cloudflare Registrar using RDAP
        (Registration Data Access Protocol). Domains registered at Cloudflare
        cannot change their nameservers, so they must use the Cloudflare API
        token flow when connecting.
      operationId: checkDnsProvider
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: Domain name to check
                  example: example.com
      responses:
        '200':
          description: Registrar detected
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    type: string
                  registrar:
                    type: string
                    description: Registrar name from RDAP (e.g. "Cloudflare, Inc.")
                  nameservers:
                    type: array
                    items:
                      type: string
                    description: Current nameservers (for reference)
                  is_cloudflare:
                    type: boolean
                    description: Whether the domain is registered at Cloudflare Registrar
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/domains/check-ns:
    post:
      tags: [Domains]
      summary: Verify domain nameservers
      description: |
        Check whether domains have their nameservers correctly pointed to Winnr.
        When verified, automatically queues the domain for provisioning
        (DNS records, email setup, etc.).
      operationId: checkDomainNs
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domains]
              properties:
                domains:
                  type: array
                  items:
                    type: string
                  description: List of domain names to check
      responses:
        '200':
          description: NS check results
          content:
            application/json:
              schema:
                type: object
                properties:
                  domains:
                    type: array
                    items:
                      type: object
                      properties:
                        domain:
                          type: string
                        domain_id:
                          type: string
                        status:
                          type: string
                          enum: [verified, failed, not_found, already_complete, error]
                        message:
                          type: string
                        verified:
                          type: boolean
                        nameservers:
                          type: array
                          items:
                            type: string
                        current_ns:
                          type: array
                          items:
                            type: string
                  all_verified:
                    type: boolean
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/domains/setup:
    post:
      tags: [Domains]
      summary: Setup a new domain
      description: |
        Queue a domain for setup. This is an async operation that registers
        the domain, configures DNS, and sets up email infrastructure.
      operationId: setupDomain
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SetupDomainRequest'
      responses:
        '202':
          description: Domain setup queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/domains/{domain_id}:
    get:
      tags: [Domains]
      summary: Get domain
      description: Get details for a specific domain.
      operationId: getDomain
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '200':
          description: Domain details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags: [Domains]
      summary: Update domain
      description: Update domain settings.
      operationId: updateDomain
      parameters:
        - $ref: '#/components/parameters/DomainId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDomainRequest'
      responses:
        '200':
          description: Updated domain
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DomainResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Domains]
      summary: Delete domain
      description: Queue a domain for deletion. This is an async operation.
      operationId: deleteDomain
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '202':
          description: Domain deletion queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains/{domain_id}/dns-status:
    get:
      tags: [Domains]
      summary: Get DNS status
      description: Check DNS propagation status for a domain.
      operationId: getDnsStatus
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '200':
          description: DNS status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DnsStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains/{domain_id}/dns-records:
    get:
      tags: [Domains]
      summary: Get manual DNS records
      description: |
        Get the expected DNS records for a manual-setup domain.
        Only available for domains with dns_provider="manual".
        Returns "pending" status if Mailcow provisioning is still in progress.
      operationId: getManualDnsRecords
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '200':
          description: DNS records
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [pending, ready]
                  domain:
                    type: string
                  records:
                    type: array
                    nullable: true
                    items:
                      type: object
                      properties:
                        type:
                          type: string
                          description: Record type (A, MX, TXT)
                        host:
                          type: string
                          description: Record hostname
                        value:
                          type: string
                          description: Record value
                        ttl:
                          type: integer
                        purpose:
                          type: string
                          description: Purpose (SPF, DKIM, DMARC)
        '400':
          description: Domain is not a manual DNS domain
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains/{domain_id}/verify-dns:
    post:
      tags: [Domains]
      summary: Verify manual DNS records
      description: |
        Verify DNS records for a manual-setup domain via live DNS lookups.
        Only available for domains with dns_provider="manual".
        When all critical records (A, MX, SPF, DKIM) are verified,
        promotes domain to "complete" status.
      operationId: verifyManualDns
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '200':
          description: Verification results
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    type: string
                  all_verified:
                    type: boolean
                  critical_verified:
                    type: boolean
                  records:
                    type: array
                    items:
                      type: object
                      properties:
                        type:
                          type: string
                        host:
                          type: string
                        purpose:
                          type: string
                        verified:
                          type: boolean
                        expected:
                          type: string
                        actual:
                          type: string
                          nullable: true
        '400':
          description: Domain is not a manual DNS domain
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: DNS records not yet computed (Mailcow provisioning in progress)

  /v1/domains/{domain_id}/custom-dns-records:
    get:
      tags: [Domains]
      summary: List custom DNS records
      description: |
        List user-added DNS records for the domain. Records are stored in
        Firestore alongside the domain doc; this endpoint does not make
        provider calls. Returns 503 if the domain's `dns_provider` is
        `manual` or `external` (no API write access).
      operationId: listCustomDnsRecords
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '200':
          description: Custom DNS records
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain: {type: string}
                  dns_provider: {type: string}
                  records:
                    type: array
                    items: {$ref: '#/components/schemas/CustomDnsRecord'}
                  dmarc_override:
                    nullable: true
                    type: object
                    properties:
                      value: {type: string}
                      updated_at: {type: string, format: date-time}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '503': {description: DNS provider does not support API record management}
    post:
      tags: [Domains]
      summary: Create a custom DNS record
      description: |
        Create a TXT, CNAME, A, or AAAA record on the domain's DNS zone.
        The request is validated against Winnr-managed records — attempts
        to create records that collide with MX, inbound A, DKIM, SPF, or
        tracking CNAMEs return 409 `managed_record_collision`. To change
        DMARC, use PUT /v1/domains/{id}/dmarc.
      operationId: createCustomDnsRecord
      parameters:
        - $ref: '#/components/parameters/DomainId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, type, value]
              properties:
                name: {type: string, description: '"@" for apex, otherwise short label (e.g. "www") or FQDN'}
                type: {type: string, enum: [TXT, CNAME, A, AAAA]}
                value: {type: string}
                ttl: {type: integer, default: 300, minimum: 60, maximum: 86400}
      responses:
        '200':
          description: Record created
          content:
            application/json:
              schema:
                type: object
                properties:
                  record: {$ref: '#/components/schemas/CustomDnsRecord'}
        '400': {description: Invalid input (apex CNAME, invalid type, etc.)}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {description: Record collides with a Winnr-managed record or a duplicate user record}
        '503': {description: DNS provider does not support API record management}

  /v1/domains/{domain_id}/custom-dns-records/{record_id}:
    patch:
      tags: [Domains]
      summary: Update a custom DNS record
      operationId: updateCustomDnsRecord
      parameters:
        - $ref: '#/components/parameters/DomainId'
        - in: path
          name: record_id
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: {type: string}
                type: {type: string, enum: [TXT, CNAME, A, AAAA]}
                value: {type: string}
                ttl: {type: integer, minimum: 60, maximum: 86400}
      responses:
        '200':
          description: Record updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  record: {$ref: '#/components/schemas/CustomDnsRecord'}
        '400': {description: Invalid input}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {description: Record collides with a Winnr-managed record}
        '503': {description: DNS provider does not support API record management}
    delete:
      tags: [Domains]
      summary: Delete a custom DNS record
      operationId: deleteCustomDnsRecord
      parameters:
        - $ref: '#/components/parameters/DomainId'
        - in: path
          name: record_id
          required: true
          schema: {type: string}
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: {type: string}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '503': {description: DNS provider does not support API record management}

  /v1/domains/{domain_id}/dmarc:
    put:
      tags: [Domains]
      summary: Set DMARC override
      description: |
        Override the default DMARC TXT record at `_dmarc.{domain}`. The
        override is stored on the domain doc and is reapplied automatically
        on any future domain re-setup.
      operationId: setDmarcOverride
      parameters:
        - $ref: '#/components/parameters/DomainId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value]
              properties:
                value:
                  type: string
                  description: 'DMARC record value (must start with "v=DMARC1")'
                  example: 'v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com'
      responses:
        '200':
          description: DMARC override applied
          content:
            application/json:
              schema:
                type: object
                properties:
                  dmarc_override:
                    type: object
                    properties:
                      value: {type: string}
                      updated_at: {type: string, format: date-time}
        '400': {description: 'Invalid DMARC value (does not start with v=DMARC1)'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '503': {description: DNS provider does not support API record management}
    delete:
      tags: [Domains]
      summary: Reset DMARC to Winnr default
      description: Clear `dmarc_override` and reapply the default `p=reject` policy at the provider.
      operationId: resetDmarcOverride
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '200':
          description: DMARC reset
          content:
            application/json:
              schema:
                type: object
                properties:
                  reset: {type: boolean}
                  applied_default: {type: string}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '503': {description: DNS provider does not support API record management}

  /v1/domains/{domain_id}/redirect:
    post:
      tags: [Domains]
      summary: Setup redirect
      description: |
        Configure a redirect for the domain. Updates Firestore immediately
        and queues an SQS message for the worker to configure the actual
        CDN/DNS redirect infrastructure (Caddy, CloudDNS WR, Route53, Cloudflare).
      operationId: setupRedirect
      parameters:
        - $ref: '#/components/parameters/DomainId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RedirectRequest'
      responses:
        '200':
          description: Redirect configured and queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    type: string
                    description: The domain name
                  redirect_url:
                    type: string
                    description: The configured redirect URL
                  queued:
                    type: boolean
                    description: Whether the SQS message was queued successfully
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Domains]
      summary: Remove redirect
      description: |
        Remove the redirect configuration from a domain.
        Clears the redirect URL in Firestore and queues an SQS message
        for the worker to tear down the CDN/DNS redirect infrastructure.
      operationId: removeRedirect
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '200':
          description: Redirect removed and teardown queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  queued:
                    type: boolean
                    description: Whether the SQS teardown message was queued successfully
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains/{domain_id}/forward:
    post:
      tags: [Domains]
      summary: Setup email forward
      description: |
        Configure email forwarding (Mailcow BCC map) for the domain.
        Configures email forwarding for the domain.
      operationId: setupForward
      parameters:
        - $ref: '#/components/parameters/DomainId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForwardRequest'
      responses:
        '200':
          description: Forward configured
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    type: string
                  forward_address:
                    type: string
                  queued:
                    type: boolean
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Domains]
      summary: Remove email forward
      description: |
        Remove email forwarding from a domain.
        Removes email forwarding from the domain.
      operationId: removeForward
      parameters:
        - $ref: '#/components/parameters/DomainId'
      responses:
        '200':
          description: Forward removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  queued:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains/{domain_id}/email-users:
    get:
      tags: [Domains]
      summary: List email users for domain
      description: Get all email users associated with a specific domain.
      operationId: getDomainEmailUsers
      parameters:
        - $ref: '#/components/parameters/DomainId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: List of email users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailUserListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/email-users:
    get:
      tags: [Email Users]
      summary: List email users
      description: List all email users for the account.
      operationId: listEmailUsers
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: domain
          in: query
          schema:
            type: string
          description: Filter by domain name
      responses:
        '200':
          description: List of email users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailUserListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Email Users]
      summary: Create email user
      description: Create a new email user (mailbox).
      operationId: createEmailUser
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateEmailUserRequest'
      responses:
        '201':
          description: Email user created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailUserResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'

  /v1/email-users/generate-names:
    post:
      tags: [Email Users]
      summary: Generate username suggestions
      description: Generate AI-powered username suggestions.
      operationId: generateNames
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateNamesRequest'
      responses:
        '200':
          description: Generated names
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateNamesResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/email-users/bulk:
    post:
      tags: [Email Users]
      summary: Bulk create email users
      description: Create multiple email users at once.
      operationId: bulkCreateEmailUsers
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkCreateRequest'
      responses:
        '200':
          description: Bulk creation results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkCreateResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

    delete:
      tags: [Email Users]
      summary: Bulk delete email users
      description: Delete multiple email users at once.
      operationId: bulkDeleteEmailUsers
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkDeleteRequest'
      responses:
        '200':
          description: Bulk deletion results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkDeleteResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/email-users/{user_id}:
    get:
      tags: [Email Users]
      summary: Get email user
      description: Get details for a specific email user.
      operationId: getEmailUser
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Email user details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailUserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags: [Email Users]
      summary: Update email user
      description: Update email user settings.
      operationId: updateEmailUser
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateEmailUserRequest'
      responses:
        '200':
          description: Updated email user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailUserResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Email Users]
      summary: Delete email user
      description: Queue an email user for deletion.
      operationId: deleteEmailUser
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Deletion queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Email User Templates ─────────────────────────────────────────
  /v1/email-user-templates:
    get:
      tags: [Email User Templates]
      summary: List templates
      description: Returns all email user templates for the account, ordered by created_at DESC.
      operationId: listTemplates
      responses:
        '200':
          description: Template list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/EmailUserTemplate'
                  count:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Email User Templates]
      summary: Create template
      description: Create a new email user template.
      operationId: createTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTemplateRequest'
      responses:
        '200':
          description: Created template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailUserTemplate'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/email-user-templates/{template_id}:
    parameters:
      - name: template_id
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Email User Templates]
      summary: Get template
      operationId: getTemplate
      responses:
        '200':
          description: Template details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailUserTemplate'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Email User Templates]
      summary: Update template
      description: Partial update — at least one of name or usernames required.
      operationId: updateTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTemplateRequest'
      responses:
        '200':
          description: Updated template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailUserTemplate'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Email User Templates]
      summary: Delete template
      operationId: deleteTemplate
      responses:
        '200':
          description: Template deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  template_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/email-users/{user_id}/inbox:
    get:
      tags: [Inbox]
      summary: List inbox messages
      description: Get emails from the user's inbox.
      operationId: listInbox
      parameters:
        - $ref: '#/components/parameters/UserId'
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: List of emails
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/email-users/{user_id}/inbox/send:
    post:
      tags: [Inbox]
      summary: Send email
      description: Send an email from this user's account.
      operationId: sendEmail
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendEmailRequest'
      responses:
        '200':
          description: Email sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendEmailResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/email-users/{user_id}/inbox/refresh:
    post:
      tags: [Inbox]
      summary: Refresh inbox
      description: Trigger an inbox sync to fetch new emails.
      operationId: refreshInbox
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Refresh queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  email:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/email-users/{user_id}/inbox/{message_id}:
    delete:
      tags: [Inbox]
      summary: Delete email
      description: Delete an email from the inbox.
      operationId: deleteEmail
      parameters:
        - $ref: '#/components/parameters/UserId'
        - $ref: '#/components/parameters/MessageId'
      responses:
        '200':
          description: Email deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  message_id:
                    type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Inbox]
      summary: Mark email as read/unread
      description: Update the read status of an email in the inbox.
      operationId: markEmailRead
      parameters:
        - $ref: '#/components/parameters/UserId'
        - $ref: '#/components/parameters/MessageId'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarkReadRequest'
      responses:
        '200':
          description: Read status updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarkReadResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/inbox:
    get:
      tags: [Inbox]
      summary: List all inbox messages for the account
      description: |
        Returns emails across every mailbox on the account, sorted by received
        time descending, served from the DynamoDB cache. Supports cursor-based
        pagination plus optional date-range, single-mailbox, and warmup-exclusion
        filters applied server-side.
      operationId: listAccountInbox
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
            maximum: 200
        - name: cursor
          in: query
          schema:
            type: string
          description: Opaque cursor returned by the previous page.
        - name: date_from
          in: query
          schema:
            type: string
            format: date
          description: Inclusive start of the date range (YYYY-MM-DD).
        - name: date_to
          in: query
          schema:
            type: string
            format: date
          description: Inclusive end of the date range (YYYY-MM-DD), through end of day.
        - name: mailbox
          in: query
          schema:
            type: string
          description: Filter to a single mailbox email address.
        - name: exclude_warmup
          in: query
          schema:
            type: boolean
          description: If true, exclude emails matching the account's warmup phrases.
        - name: has_attachments
          in: query
          schema:
            type: boolean
          description: If true, only return emails flagged as having attachments.
      responses:
        '200':
          description: Page of inbox messages
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboxListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/inbox/refresh:
    post:
      tags: [Inbox]
      summary: Trigger account-wide inbox sync
      description: |
        Kicks off the legacy IMAP sync Lambda for every mailbox on the
        account, refilling the DynamoDB cache with new messages. Returns
        immediately; the actual sync runs asynchronously.
      operationId: refreshAccountInbox
      responses:
        '200':
          description: Refresh queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/inbox/bulk-delete:
    post:
      tags: [Inbox]
      summary: Bulk delete inbox messages
      description: |
        Delete up to 200 messages per call. Messages are grouped by mailbox
        so each mailbox uses a single IMAP connection. DynamoDB cache rows
        are removed only for UIDs that the IMAP delete confirmed.
      operationId: bulkDeleteInboxMessages
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messages]
              properties:
                messages:
                  type: array
                  maxItems: 200
                  items:
                    type: object
                    required: [uid, mailbox]
                    properties:
                      uid:
                        type: string
                        description: IMAP message UID.
                      mailbox:
                        type: string
                        format: email
                        description: Mailbox the message belongs to.
      responses:
        '200':
          description: Bulk delete result
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: integer
                  failed:
                    type: integer
                  errors:
                    type: array
                    items:
                      type: object
                      properties:
                        uid:
                          type: string
                        mailbox:
                          type: string
                        error:
                          type: string
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/inbox/{message_id}:
    delete:
      tags: [Inbox]
      summary: Delete an inbox message (account-level)
      description: |
        Delete a single message from the mailbox via IMAP and from the
        DynamoDB cache. Requires the `mailbox` query parameter so the
        API can locate the right Mailcow credentials.
      operationId: deleteInboxMessage
      parameters:
        - name: message_id
          in: path
          required: true
          schema:
            type: string
          description: IMAP message UID.
        - name: mailbox
          in: query
          required: true
          schema:
            type: string
            format: email
          description: Mailbox the message belongs to.
      responses:
        '200':
          description: Message deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  message_id:
                    type: string
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/inbox/{uid}:
    patch:
      tags: [Inbox]
      summary: Mark email as read/unread (account-level)
      description: |
        Update the read status of an email. Uses the mailbox query parameter
        to identify the correct email user for the IMAP operation.
      operationId: markEmailReadAccountLevel
      parameters:
        - name: uid
          in: path
          required: true
          schema:
            type: string
          description: IMAP message UID
        - name: mailbox
          in: query
          required: true
          schema:
            type: string
          description: Email address the message belongs to
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MarkReadRequest'
      responses:
        '200':
          description: Read status updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MarkReadResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/inbox/{uid}/body:
    get:
      tags: [Inbox]
      summary: Get full message body
      description: |
        Fetch the full email body from DynamoDB for a specific message.
        Firestore only stores a 200-character body_preview; use this
        endpoint to load the complete body on demand (e.g., when opening
        a thread).
      operationId: getMessageBody
      parameters:
        - name: uid
          in: path
          required: true
          schema:
            type: string
          description: IMAP message UID
        - name: mailbox
          in: query
          required: true
          schema:
            type: string
          description: Email address the message belongs to
      responses:
        '200':
          description: Full message body
          content:
            application/json:
              schema:
                type: object
                properties:
                  uid:
                    type: string
                  mailbox:
                    type: string
                  body:
                    type: string
                    description: Full email body text
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/inbox/{uid}/attachments:
    get:
      tags: [Inbox]
      summary: List message attachments
      description: |
        Fetch attachment metadata for an inbox message. Connects to the
        mailbox's Mailcow IMAP server and walks the MIME structure.
      operationId: listAttachments
      parameters:
        - name: uid
          in: path
          required: true
          schema:
            type: string
          description: IMAP message UID
        - name: mailbox
          in: query
          required: true
          schema:
            type: string
          description: Email address the message belongs to
      responses:
        '200':
          description: Attachment metadata list
          content:
            application/json:
              schema:
                type: object
                properties:
                  attachments:
                    type: array
                    items:
                      $ref: '#/components/schemas/AttachmentMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/inbox/{uid}/attachments/{part_index}:
    get:
      tags: [Inbox]
      summary: Download attachment
      description: |
        Get a presigned download URL for an inbox attachment. Checks S3
        cache first; if not cached, fetches from IMAP and caches to S3.
        The presigned URL is valid for 5 minutes.
      operationId: downloadAttachment
      parameters:
        - name: uid
          in: path
          required: true
          schema:
            type: string
          description: IMAP message UID
        - name: part_index
          in: path
          required: true
          schema:
            type: integer
          description: MIME part index of the attachment
        - name: mailbox
          in: query
          required: true
          schema:
            type: string
          description: Email address the message belongs to
      responses:
        '200':
          description: Presigned download URL and metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachmentDownload'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/jobs:
    get:
      tags: [Jobs]
      summary: List jobs
      description: |
        List recent async jobs for the account, ordered by `created_at` descending.

        Jobs are an audit trail of async operations and are retained after the
        underlying resource (e.g. a domain) is deleted. To see only active work,
        filter with `filter[status]=in_progress`.

        For live resource state (e.g. whether a domain currently exists), query
        the corresponding resource endpoint (e.g. `GET /v1/domains/{domain_id}`).
      operationId: listJobs
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: filter[status]
          in: query
          required: false
          schema:
            type: string
            enum: [pending, in_progress, completed, failed]
          description: Filter by job status.
        - name: filter[type]
          in: query
          required: false
          schema:
            type: string
            enum: [domain_setup, domain_delete, email_user_delete, warming_enable, warming_disable]
          description: Filter by job type.
      responses:
        '200':
          description: List of jobs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/jobs/{job_id}:
    get:
      tags: [Jobs]
      summary: Get job status
      description: Get the status of an async job.
      operationId: getJob
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Job details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/export:
    post:
      tags: [Export]
      summary: Export email users to CSV
      description: |
        Export email users to a CSV file in various formats compatible with popular
        email outreach tools. The CSV is uploaded to S3 and a presigned download URL
        is returned.

        **Rate Limit:** This endpoint is heavily rate limited to 1 request per 5 seconds
        per account to prevent abuse, as export operations are resource-intensive.

        **Supported Formats:**
        - `smartlead` - SmartLead.ai format
        - `instantly` - Instantly.ai format
        - `saleshandy` - SalesHandy format
        - `lemlist` - Lemlist format
        - `woodpecker` - Woodpecker format
        - `snov` - Snov.io format
        - `reply` - Reply.io format
        - `mailshake` - Mailshake format
        - `gmass` - GMass format
        - `outreach` - Outreach.io format
        - `salesloft` - SalesLoft format
        - `mixmax` - MixMax format
        - `apollo` - Apollo format
        - `hunter` - Hunter.io format
        - `close` - Close.io format
        - `yesware` - Yesware format
        - `mailchimp` - Mailchimp format
        - `hubspot` - HubSpot format
      operationId: exportEmailUsers
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExportRequest'
      responses:
        '200':
          description: Export successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportResponse'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          description: Export rate limit exceeded (1 per 5 seconds)
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until next export is allowed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                error:
                  code: rate_limited
                  message: Export rate limit exceeded. Max 1 export per 5 seconds.

  /v1/export/formats:
    get:
      tags: [Export]
      summary: List supported export formats
      description: Returns a list of all supported export formats.
      operationId: getExportFormats
      responses:
        '200':
          description: List of supported formats
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportFormatsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/onboarding/unsubscribe:
    get:
      tags: [Onboarding]
      summary: Unsubscribe from onboarding emails
      description: |
        Public endpoint (no authentication required). Validates the HMAC token
        from the unsubscribe link in onboarding drip emails and sets
        `onboarding_unsubscribed: true` on the account. Returns an HTML
        confirmation page. Does not affect transactional emails.
      operationId: unsubscribeOnboarding
      security: []
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: Base64-encoded HMAC token containing the account ID
      responses:
        '200':
          description: Successfully unsubscribed (HTML page)
          content:
            text/html:
              schema:
                type: string
        '400':
          description: Invalid or missing token (HTML page)
          content:
            text/html:
              schema:
                type: string

  /v1/warming:
    get:
      tags: [Warming]
      summary: List warming mailboxes
      description: |
        Returns all email users with warming enabled, including cached stats.
        Uses offset-based pagination via `page` and `per_page` query parameters.
      operationId: listWarmingMailboxes
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
            minimum: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 100
            maximum: 500
        - name: sort_by
          in: query
          schema:
            type: string
          description: |
            Field to sort by. Defaults to status-priority ordering. Supports
            common WarmingMailbox fields (e.g. `full_address`, `domain`,
            `status`).
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
      responses:
        '200':
          description: List of warming mailboxes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WarmingMailboxListResponse'

  /v1/warming/overview:
    get:
      tags: [Warming]
      summary: Get warming overview
      description: Returns account-level aggregate warming stats.
      operationId: getWarmingOverview
      responses:
        '200':
          description: Warming overview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WarmingOverview'

  /v1/warming/enable:
    post:
      tags: [Warming]
      summary: Enable warming
      description: Enable warming for one or more email users. Creates campaigns and starts warmup.
      operationId: enableWarming
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_ids]
              properties:
                user_ids:
                  type: array
                  items:
                    type: string
                settings:
                  type: object
                  properties:
                    emails_per_day:
                      type: integer
                      default: 20
                    response_rate:
                      type: integer
                      default: 30
                    rampup_enabled:
                      type: boolean
                      default: true
                    rampup_speed:
                      type: string
                      enum: [slow, normal, fast]
                      default: normal
      responses:
        '200':
          description: Warming enabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  enabled:
                    type: array
                    items:
                      $ref: '#/components/schemas/WarmingMailbox'

  /v1/warming/enable-async:
    post:
      tags: [Warming]
      summary: Enable warming (async)
      description: |
        Asynchronously enable warming for email users. Creates a background job
        that processes mailboxes via SQS. Each mailbox is enabled independently
        with automatic retries on failure. Track progress via the returned job_id
        using GET /v1/jobs/{job_id} or a Firestore listener on the job document.
        Billing must be handled separately before calling this endpoint.
      operationId: enableWarmingAsync
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_ids]
              properties:
                user_ids:
                  type: array
                  items:
                    type: string
                  maxItems: 500
                settings:
                  type: object
                  properties:
                    emails_per_day:
                      type: integer
                      minimum: 1
                      maximum: 20
                      default: 20
                    rampup_speed:
                      type: string
                      enum: [slow, normal, fast]
                      default: normal
      responses:
        '200':
          description: Job created and mailboxes queued for processing
          content:
            application/json:
              schema:
                type: object
                properties:
                  job_id:
                    type: string
                    nullable: true
                    description: Job ID for progress tracking, null if no eligible mailboxes
                  status:
                    type: string
                    enum: [processing, completed]
                  total:
                    type: integer
                    description: Number of eligible mailboxes queued
                  queued:
                    type: integer
                    description: Number of SQS messages successfully sent
                  skipped:
                    type: integer
                    description: Number of mailboxes skipped (already warming, inactive, etc.)

  /v1/warming/disable:
    post:
      tags: [Warming]
      summary: Disable warming
      description: Disable warming for one or more email users. Deletes campaigns.
      operationId: disableWarming
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user_ids]
              properties:
                user_ids:
                  type: array
                  items:
                    type: string
      responses:
        '200':
          description: Warming disabled

  /v1/warming/{user_id}/pause:
    post:
      tags: [Warming]
      summary: Pause warming
      operationId: pauseWarming
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Warming paused

  /v1/warming/{user_id}/resume:
    post:
      tags: [Warming]
      summary: Resume warming
      operationId: resumeWarming
      parameters:
        - $ref: '#/components/parameters/UserId'
      responses:
        '200':
          description: Warming resumed

  /v1/warming/{user_id}/settings:
    patch:
      tags: [Warming]
      summary: Update warming settings
      operationId: updateWarmingSettings
      parameters:
        - $ref: '#/components/parameters/UserId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                emails_per_day:
                  type: integer
                response_rate:
                  type: integer
                rampup_enabled:
                  type: boolean
                rampup_speed:
                  type: string
                  enum: [slow, normal, fast]
      responses:
        '200':
          description: Settings updated

  /v1/warming/{user_id}/metrics:
    get:
      tags: [Warming]
      summary: Get warming metrics
      operationId: getWarmingMetrics
      parameters:
        - $ref: '#/components/parameters/UserId'
        - name: days
          in: query
          schema:
            type: integer
            default: 30
      responses:
        '200':
          description: Daily warming metrics
          content:
            application/json:
              schema:
                type: object
                properties:
                  metrics:
                    type: array
                    items:
                      $ref: '#/components/schemas/WarmingDailyMetric'

  /v1/warming/{user_id}/activity:
    get:
      tags: [Warming]
      summary: Get warming activity
      operationId: getWarmingActivity
      parameters:
        - $ref: '#/components/parameters/UserId'
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: Activity logs
          content:
            application/json:
              schema:
                type: object
                properties:
                  activities:
                    type: array
                    items:
                      $ref: '#/components/schemas/WarmingActivity'

  /v1/warming/refresh:
    post:
      tags: [Warming]
      summary: Refresh warming metrics
      description: Triggers an on-demand sync of warming metrics for the account.
      operationId: refreshWarming
      responses:
        '200':
          description: Refresh triggered

  /v1/warming/import:
    post:
      tags: [Warming]
      summary: Import external accounts for warming
      description: |
        Import email accounts from external providers (Google Workspace, Microsoft 365, custom SMTP)
        and immediately enable warming on them. Accounts are created in Firestore with `type: "external"`
        and warming campaigns are set up in the provider.

        Billing must be handled by the frontend BEFORE calling this endpoint. The frontend charges via
        Stripe first, then sends batches of up to 5 accounts per request to avoid API Gateway timeout.

        **Provider defaults** (auto-filled when provider is `google` or `microsoft`):
        - Google: smtp.gmail.com:465, imap.gmail.com:993
        - Microsoft: smtp.office365.com:587, outlook.office365.com:993
        - SMTP: all host/port fields required from the request
      operationId: importExternalAccounts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [accounts]
              properties:
                accounts:
                  type: array
                  maxItems: 200
                  items:
                    type: object
                    required: [email, password, provider]
                    properties:
                      email:
                        type: string
                        format: email
                      name:
                        type: string
                        description: Display name (auto-derived from email username if omitted)
                      password:
                        type: string
                        description: App password (Google/Microsoft) or SMTP password
                      provider:
                        type: string
                        enum: [google, microsoft, smtp]
                      smtp_host:
                        type: string
                        description: Required for smtp provider
                      smtp_port:
                        type: integer
                        description: Required for smtp provider (e.g. 465 or 587)
                      imap_host:
                        type: string
                        description: Required for smtp provider
                      imap_port:
                        type: integer
                        description: Required for smtp provider (e.g. 993)
                warming_settings:
                  type: object
                  properties:
                    emails_per_day:
                      type: integer
                      minimum: 1
                      maximum: 20
                      default: 20
                    rampup_speed:
                      type: string
                      enum: [slow, normal, fast]
                      default: normal
      responses:
        '200':
          description: Import results
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        email:
                          type: string
                        status:
                          type: string
                          enum: [imported, duplicate, error]
                        user_id:
                          type: string
                        message:
                          type: string
                  imported:
                    type: integer
                  duplicates:
                    type: integer
                  errors:
                    type: integer
                  total:
                    type: integer

  /v1/warming/import/{user_id}:
    delete:
      tags: [Warming]
      summary: Delete an external warming account
      description: |
        Delete an external account imported via POST /v1/warming/import.
        If warming is active, the provider campaign is deleted first, then the Firestore doc is removed.
        Only works for accounts with `type: "external"`.
      operationId: deleteExternalAccount
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Account deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  user_id:
                    type: string
        '400':
          description: Not an external account
        '404':
          description: User not found

  # ── Reseller ──────────────────────────────────────────────────────────────

  /v1/reseller/profile:
    get:
      tags: [Reseller]
      summary: Get reseller profile
      description: Returns the reseller profile for the authenticated account. Requires account_type = "reseller".
      operationId: getResellerProfile
      responses:
        '200':
          description: Reseller profile
        '403':
          description: Not a reseller account
    patch:
      tags: [Reseller]
      summary: Update reseller profile
      description: Update branding, settings, and defaults on the reseller profile.
      operationId: updateResellerProfile
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                company_name:
                  type: string
                support_email:
                  type: string
                default_domain_limit:
                  type: integer
                default_email_user_limit:
                  type: integer
                self_signup_enabled:
                  type: boolean
      responses:
        '200':
          description: Updated profile

  /v1/reseller/accounts:
    get:
      tags: [Reseller]
      summary: List sub-accounts
      description: List all sub-accounts owned by this reseller. Supports search and pagination.
      operationId: listResellerAccounts
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: search
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            type: string
      responses:
        '200':
          description: List of sub-accounts
    post:
      tags: [Reseller]
      summary: Create sub-account
      description: Create a new sub-account with a Firebase Auth user and Firestore account doc.
      operationId: createResellerAccount
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, first_name]
              properties:
                email:
                  type: string
                first_name:
                  type: string
                last_name:
                  type: string
                company_name:
                  type: string
                password:
                  type: string
                domain_limit:
                  type: integer
                email_user_limit:
                  type: integer
      responses:
        '200':
          description: Created sub-account with user details and optional reset link

  /v1/reseller/accounts/{id}/usage:
    get:
      tags: [Reseller]
      summary: Get sub-account usage
      description: Get detailed usage metrics for a single sub-account.
      operationId: getResellerAccountUsage
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Sub-account usage metrics

  /v1/reseller/usage:
    get:
      tags: [Reseller]
      summary: Get aggregate usage
      description: Get aggregate usage across all sub-accounts (total domains, email users, warming, etc.).
      operationId: getResellerUsage
      responses:
        '200':
          description: Aggregate reseller usage

  /v1/reseller/audit-log:
    get:
      tags: [Reseller]
      summary: List audit log
      description: List audit log entries for this reseller's actions.
      operationId: listResellerAuditLog
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Audit log entries

  /v1/reseller/impersonate:
    post:
      tags: [Reseller]
      summary: Impersonate sub-account
      description: |
        Generate a Firebase custom token to impersonate a sub-account's admin user.
        Opens in a new tab with a 55-minute session cap.
      operationId: impersonateSubAccount
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [account_id]
              properties:
                account_id:
                  type: string
                duration_minutes:
                  type: integer
                  default: 55
                  maximum: 55
      responses:
        '200':
          description: Impersonation token generated
        '400':
          description: Account suspended/deleted or no admin user
        '403':
          description: Not a reseller or doesn't own account

  /v1/reseller/accounts/{id}/suspend:
    post:
      tags: [Reseller]
      summary: Suspend sub-account
      description: Suspend a sub-account and disable all its Firebase Auth users.
      operationId: suspendSubAccount
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Account suspended
        '400':
          description: Already suspended or deleted

  /v1/reseller/accounts/{id}/reactivate:
    post:
      tags: [Reseller]
      summary: Reactivate sub-account
      description: Reactivate a suspended sub-account and re-enable its Firebase Auth users.
      operationId: reactivateSubAccount
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Account reactivated
        '400':
          description: Not currently suspended

  /v1/reseller/accounts/{id}:
    get:
      tags: [Reseller]
      summary: Get sub-account detail
      operationId: getResellerAccount
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Sub-account detail with admin user and domain breakdown
    patch:
      tags: [Reseller]
      summary: Update sub-account
      operationId: updateResellerAccount
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                domain_limit:
                  type: integer
                email_user_limit:
                  type: integer
      responses:
        '200':
          description: Updated sub-account
    delete:
      tags: [Reseller]
      summary: Delete sub-account
      description: |
        Soft-delete a sub-account. Sets status to 'deleted', disables all Firebase Auth users,
        and decrements the reseller's sub_account_count. 90-day retention.
      operationId: deleteSubAccount
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Account deleted
        '400':
          description: Already deleted

  # ── Brand / Whitelabel ─────────────────────────────────────────────────────

  /v1/brand:
    get:
      tags: [Brand]
      summary: Brand detection
      description: |
        Brand detection endpoint. Returns brand config for a given hostname.
        Public — no auth required.
      operationId: getBrand
      security: []
      parameters:
        - name: host
          in: query
          required: true
          schema:
            type: string
          description: Hostname to look up brand config for (e.g. "app.example.com")
      responses:
        '200':
          description: Brand config (or null if no custom brand)
          content:
            application/json:
              schema:
                type: object
                properties:
                  brand:
                    nullable: true
                    type: object
                    properties:
                      company_name:
                        type: string
                      logo_light_url:
                        type: string
                      logo_dark_url:
                        type: string
                      favicon_url:
                        type: string
                      primary_color:
                        type: string
                      primary_color_dark:
                        type: string
                      support_email:
                        type: string
                      marketing_url:
                        type: string
                      terms_url:
                        type: string
                      privacy_url:
                        type: string
                      self_signup_enabled:
                        type: boolean
                      self_signup_auth_methods:
                        type: array
                        items:
                          type: string
                      feature_flags:
                        type: object
                      reseller_account_id:
                        type: string
                      brand_config_version:
                        type: integer

  /v1/reseller/logo:
    post:
      tags: [Reseller]
      summary: Upload logo
      description: Upload a logo image (base64 encoded). Max 2MB.
      operationId: uploadResellerLogo
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [type, content_type, data]
              properties:
                type:
                  type: string
                  enum: [dark, light, favicon]
                  description: Logo variant to upload
                content_type:
                  type: string
                  description: MIME type of the image (e.g. "image/png")
                data:
                  type: string
                  description: Base64-encoded image data
      responses:
        '200':
          description: Logo uploaded successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  type:
                    type: string
                  s3_key:
                    type: string
        '400':
          description: Invalid input or image too large
        '403':
          description: Not a reseller account

  /v1/reseller/domain:
    post:
      tags: [Reseller]
      summary: Set custom domain
      description: Set custom domain for the reseller. Adds domain to Vercel project.
      operationId: setResellerDomain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain]
              properties:
                domain:
                  type: string
                  description: Custom domain to configure (e.g. "app.example.com")
      responses:
        '200':
          description: Domain configured with DNS instructions
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    type: string
                  dns_instructions:
                    type: object
                    properties:
                      type:
                        type: string
                      name:
                        type: string
                      value:
                        type: string
                  vercel_result:
                    nullable: true
                    type: object
        '400':
          description: Invalid domain
        '403':
          description: Not a reseller account

  /v1/reseller/domain/status:
    get:
      tags: [Reseller]
      summary: Check custom domain status
      description: Check verification status of the reseller's custom domain.
      operationId: getResellerDomainStatus
      responses:
        '200':
          description: Domain verification status
          content:
            application/json:
              schema:
                type: object
                properties:
                  domain:
                    nullable: true
                    type: string
                  verified:
                    type: boolean
                  vercel_status:
                    nullable: true
                    type: object
        '403':
          description: Not a reseller account

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: API token (wnr_xxx...)

  parameters:
    DomainId:
      name: domain_id
      in: path
      required: true
      schema:
        type: string
      description: Domain ID

    UserId:
      name: user_id
      in: path
      required: true
      schema:
        type: string
      description: Email user ID

    TokenId:
      name: token_id
      in: path
      required: true
      schema:
        type: string
      description: API token ID

    JobId:
      name: job_id
      in: path
      required: true
      schema:
        type: string
      description: Job ID

    MessageId:
      name: message_id
      in: path
      required: true
      schema:
        type: string
      description: Email message ID (IMAP UID)

    Limit:
      name: limit
      in: query
      schema:
        type: integer
        default: 25
        minimum: 1
        maximum: 100
      description: Number of items per page

    Cursor:
      name: cursor
      in: query
      schema:
        type: string
      description: Pagination cursor from previous response

  responses:
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: unauthorized
              message: Invalid or revoked API token

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: not_found
              message: Resource not found

    ValidationError:
      description: Validation error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: validation_error
              message: Invalid input
              details:
                - field: email
                  message: Invalid email format

    Conflict:
      description: Resource conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: conflict
              message: Email address already exists

    RateLimited:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
        Retry-After:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: rate_limited
              message: Rate limit exceeded. Retry after 30 seconds

  schemas:
    WarmingMailbox:
      type: object
      properties:
        id:
          type: string
        full_address:
          type: string
        domain_string:
          type: string
        name:
          type: string
        warming_status:
          type: string
          enum: [active, paused, connecting, connection_problem, disabled]
        warming_emails_per_day:
          type: integer
        warming_response_rate:
          type: integer
        warming_rampup_enabled:
          type: boolean
        warming_rampup_speed:
          type: string
          enum: [slow, normal, fast]
        warming_health_score:
          type: number
        warming_inbox_rate:
          type: number
        warming_spam_rate:
          type: number
        warming_total_sent:
          type: integer
        warming_total_replies:
          type: integer
        warming_started_at:
          type: string
          format: date-time
          nullable: true
        warming_last_sync:
          type: string
          format: date-time
          nullable: true

    WarmingMailboxListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/WarmingMailbox'
        pagination:
          $ref: '#/components/schemas/Pagination'
        meta:
          $ref: '#/components/schemas/Meta'

    WarmingOverview:
      type: object
      properties:
        total_mailboxes:
          type: integer
        active:
          type: integer
        paused:
          type: integer
        problems:
          type: integer
        avg_health_score:
          type: number
        avg_inbox_rate:
          type: number
        total_sent_today:
          type: integer
        total_replies_today:
          type: integer
        monthly_cost:
          type: number

    WarmingDailyMetric:
      type: object
      properties:
        date:
          type: string
        sent:
          type: integer
        inbox:
          type: integer
        spam:
          type: integer
        replies:
          type: integer
        inbox_rate:
          type: number

    WarmingActivity:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        type:
          type: string
        email:
          type: string
        subject:
          type: string
        landed_in:
          type: string
          enum: [inbox, spam]
        direction:
          type: string
          enum: [sent, received]

    EmailUserTemplate:
      type: object
      description: Reusable email user template (list of name + username pairs)
      properties:
        id:
          type: string
        label:
          type: string
          description: Template display name (max 100 chars)
        users:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              username:
                type: string
          description: 1-10 name + username pairs
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true

    CreateTemplateRequest:
      type: object
      required: [label, users]
      properties:
        label:
          type: string
          maxLength: 100
        users:
          type: array
          items:
            type: object
            required: [name, username]
            properties:
              name:
                type: string
              username:
                type: string
          minItems: 1
          maxItems: 10

    UpdateTemplateRequest:
      type: object
      description: At least one of label or users required.
      properties:
        label:
          type: string
          maxLength: 100
        users:
          type: array
          items:
            type: object
            required: [name, username]
            properties:
              name:
                type: string
              username:
                type: string
          minItems: 1
          maxItems: 10

    TeamUser:
      type: object
      description: A team user (dashboard member) for the account
      properties:
        id:
          type: string
          description: User ID
        email:
          type: string
          format: email
        display_name:
          type: string
        first_name:
          type: string
        last_name:
          type: string
        photo_url:
          type: string
          format: uri
        role:
          type: string
          enum: [superadmin, admin, member]
          description: |
            User role:
            - superadmin: Account owner (cannot be modified or deleted)
            - admin: Can manage domains, users, and settings
            - member: Can view and use email accounts
        is_admin:
          type: boolean
        is_superadmin:
          type: boolean
        created_at:
          type: string
          format: date-time

    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  message:
                    type: string
        meta:
          $ref: '#/components/schemas/Meta'

    Meta:
      type: object
      properties:
        request_id:
          type: string
        timestamp:
          type: string
          format: date-time

    Pagination:
      type: object
      description: |
        Pagination metadata. Cursor-based endpoints (e.g. `/v1/domains`,
        `/v1/email-users`, `/v1/inbox`, `/v1/email-users/{id}/inbox`) populate
        `has_more` and `cursor`. Offset-based endpoints (e.g. `/v1/warming`)
        populate `page`, `per_page`, and `total`. `count` (number of items in
        the current page) is included where available.
      properties:
        has_more:
          type: boolean
          description: Whether more pages exist after this one (cursor-based endpoints).
        cursor:
          type: string
          nullable: true
          description: Opaque cursor to pass as `?cursor=…` for the next page.
        count:
          type: integer
          description: Number of items returned in this response.
        total:
          type: integer
          description: Total number of items across all pages (offset-based endpoints).
        page:
          type: integer
          description: 1-based page number (offset-based endpoints).
        per_page:
          type: integer
          description: Items per page (offset-based endpoints).

    AccountResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            id:
              type: string
              description: Account ID
            name:
              type: string
              description: Account name
            email:
              type: string
              description: Account owner email
            plan:
              type: string
              description: Current plan name (e.g. "Startup", "Enterprise")
            domains_limit:
              type: integer
              description: Maximum number of domains allowed
            domains_used:
              type: integer
              description: Number of domains currently in use
            domain_credits:
              type: integer
              description: Domain credits available (each credit covers one domain purchase)
            email_users_limit:
              type: integer
              description: Maximum number of email users allowed
            email_users_used:
              type: integer
              description: Number of email users currently in use
            stripe_subscription_status:
              type: string
              description: Subscription status (e.g. "active", "canceled", "past_due")
        meta:
          $ref: '#/components/schemas/Meta'

    UsageResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            domains:
              type: object
              properties:
                used:
                  type: integer
                limit:
                  type: integer
            email_users:
              type: object
              properties:
                used:
                  type: integer
                limit:
                  type: integer
        meta:
          $ref: '#/components/schemas/Meta'

    TokenListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Token'
        meta:
          $ref: '#/components/schemas/Meta'

    Token:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
          description: First 12 characters of the token
        permissions:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
          nullable: true

    CreateTokenRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          description: Human-readable name for the token
        permissions:
          type: array
          items:
            type: string
            enum: [read, write]
          default: [read, write]

    TokenCreatedResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            token:
              type: string
              description: Full token value (only shown once)
            permissions:
              type: array
              items:
                type: string
            created_at:
              type: string
              format: date-time
        meta:
          $ref: '#/components/schemas/Meta'

    DomainListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Domain'
        pagination:
          $ref: '#/components/schemas/Pagination'
        meta:
          $ref: '#/components/schemas/Meta'

    CustomDnsRecord:
      type: object
      description: |
        A user-managed DNS record on a domain's zone. These are validated to
        never collide with the records Winnr manages for mail delivery (MX,
        inbound A, DKIM, SPF base, tracking CNAMEs). DMARC is handled via the
        dedicated `/dmarc` endpoint.
      properties:
        id: {type: string, description: Winnr-generated UUID for this record}
        name: {type: string, description: '"@" for apex, otherwise a relative label (e.g. "www")'}
        type: {type: string, enum: [TXT, CNAME, A, AAAA]}
        value: {type: string}
        ttl: {type: integer}
        provider_record_id:
          type: string
          description: |
            The DNS provider's native record identifier. For Cloudflare and
            CloudDNS this is the API-returned record ID. For Route 53 (which
            has no record ID) this is a synthetic `r53:{type}:{fqdn}` string.
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
        created_by: {type: string, description: Firebase UID that created the record}

    Domain:
      type: object
      properties:
        id:
          type: string
          description: Firestore document ID for the domain.
        name:
          type: string
          description: Domain name (e.g. "example.com").
        status:
          type: string
          enum: [pending, complete, deleting]
          description: |
            Current lifecycle state of the domain.
            - `pending` — provisioning in progress (registration, mailcow, DNS).
            - `complete` — provisioning finished and DNS records verified.
            - `deleting` — domain teardown in progress; will disappear from this
              list once the async deletion job finishes.
            Some legacy documents may still report `active`; treat it as
            equivalent to `complete`.
        dns_provider:
          type: string
          nullable: true
          description: |
            Where the domain's DNS is hosted. One of `cloudflare`,
            `route53`, `clouddns1`, `clouddns2`, `clouddns3`, or null
            if not yet determined.
        dns_status:
          type: string
          nullable: true
          description: Latest DNS health check result.
        ns_status:
          type: string
          nullable: true
          description: |
            Nameserver delegation status (matches Winnr's nameservers vs.
            third-party).
        registrar:
          type: string
          nullable: true
          description: Registrar name, e.g. `dynadot`. Null for BYOD domains.
        redirect_url:
          type: string
          nullable: true
          description: Apex redirect target, if a redirect is configured.
        forward_address:
          type: string
          nullable: true
          description: Catch-all forward address, if forwarding is configured.
        tags:
          type: array
          items:
            type: string
        email_users_count:
          type: integer
          description: Number of email users currently provisioned on this domain.
        expire_date:
          type: string
          format: date-time
          nullable: true
          description: Domain registration expiration. Null for BYOD domains.
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true
        payment_status:
          type: string
          nullable: true
          description: |
            Present only for domains that go through Winnr's domain purchase
            flow. Typical values include `paid`, `pending`, `failed`.
        payment_method:
          type: string
          nullable: true
          description: Payment method used (only when `payment_status` is set).
        payment_amount:
          type: number
          nullable: true
          description: Amount charged in USD (only when `payment_status` is set).

    DomainResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/Domain'
        meta:
          $ref: '#/components/schemas/Meta'

    UpdateDomainRequest:
      type: object
      properties:
        tags:
          type: array
          items:
            type: string

    SetupDomainRequest:
      type: object
      required:
        - domain
      properties:
        domain:
          type: string
          description: Domain name to setup

    DomainSearchResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            domain:
              type: string
            available:
              type: boolean
            price:
              type: number
            currency:
              type: string
        meta:
          $ref: '#/components/schemas/Meta'

    DomainSuggestResponse:
      type: object
      properties:
        data:
          type: array
          items:
            type: object
            properties:
              domain:
                type: string
              available:
                type: boolean
              price:
                type: number
        meta:
          $ref: '#/components/schemas/Meta'

    DnsStatusResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            domain:
              type: string
            ns_verified:
              type: boolean
            mx_verified:
              type: boolean
            spf_verified:
              type: boolean
            dkim_verified:
              type: boolean
        meta:
          $ref: '#/components/schemas/Meta'

    RedirectRequest:
      type: object
      required:
        - url
      properties:
        url:
          type: string
          format: uri
          description: The destination URL to redirect to

    ForwardRequest:
      type: object
      required:
        - address
      properties:
        address:
          type: string
          format: email
          description: The email address to forward all domain emails to

    EmailUserListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/EmailUser'
        pagination:
          $ref: '#/components/schemas/Pagination'
        meta:
          $ref: '#/components/schemas/Meta'

    EmailUser:
      type: object
      properties:
        id:
          type: string
          description: Firestore document ID for the email user.
        username:
          type: string
          description: Local part of the email address (before the `@`).
        domain:
          type: string
          description: Domain string (after the `@`).
        full_address:
          type: string
          format: email
          description: Full email address (`{username}@{domain}`). Use this when sending mail.
        name:
          type: string
          nullable: true
          description: Display name (used in the From header).
        status:
          type: string
          enum: [active, paused, disabled]
          description: |
            Mailbox lifecycle state.
            - `active` — mailbox is provisioned and warming/sending normally.
            - `paused` — warming temporarily paused; mailbox still exists.
            - `disabled` — mailbox is disabled (e.g. plan past-due, account hold).
        type:
          type: string
          nullable: true
          description: Mailbox type (e.g. `cold_email`, `prewarmed`). Null for legacy mailboxes.
        daily_send_limit:
          type: integer
          nullable: true
          description: Per-mailbox daily send cap, if configured.
        footer:
          type: string
          nullable: true
          description: HTML footer appended to outbound mail, if configured.
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true

    EmailUserResponse:
      type: object
      properties:
        data:
          $ref: '#/components/schemas/EmailUser'
        meta:
          $ref: '#/components/schemas/Meta'

    CreateEmailUserRequest:
      type: object
      required:
        - username
        - domain
      properties:
        username:
          type: string
          description: Local part of the email address
        domain:
          type: string
          description: Domain ID
        name:
          type: string
          description: Display name
        password:
          type: string
          description: Optional password (generated if not provided)

    UpdateEmailUserRequest:
      type: object
      properties:
        name:
          type: string
        password:
          type: string

    GenerateNamesRequest:
      type: object
      properties:
        count:
          type: integer
          default: 5
        style:
          type: string
          enum: [professional, casual, creative]

    GenerateNamesResponse:
      type: object
      properties:
        data:
          type: array
          items:
            type: string
        meta:
          $ref: '#/components/schemas/Meta'

    BulkCreateRequest:
      type: object
      required:
        - domain
        - users
      properties:
        domain:
          type: string
          description: Domain name shared by all users in this request (e.g. "example.com"). All users in a single bulk request must be on the same domain — send one request per domain if creating users across multiple.
          example: example.com
        users:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: '#/components/schemas/BulkCreateUserItem'

    BulkCreateUserItem:
      type: object
      required:
        - username
      properties:
        username:
          type: string
          description: Local part of the email address (before the @)
          example: john
        name:
          type: string
          description: Display name. Auto-derived from username (e.g. "john.doe" → "John Doe") if omitted.
          example: John Doe
        password:
          type: string
          description: Optional password. Auto-generated if omitted.
        footer:
          type: string
          description: Optional email signature footer.

    BulkCreateResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            created:
              type: integer
            failed:
              type: integer
            results:
              type: array
              items:
                type: object
        meta:
          $ref: '#/components/schemas/Meta'

    BulkDeleteRequest:
      type: object
      required:
        - user_ids
      properties:
        user_ids:
          type: array
          items:
            type: string

    BulkDeleteResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            deleted:
              type: integer
            failed:
              type: integer
        meta:
          $ref: '#/components/schemas/Meta'

    AttachmentMeta:
      type: object
      properties:
        filename:
          type: string
        content_type:
          type: string
        size:
          type: integer
          description: File size in bytes
        part_index:
          type: integer
          description: MIME part index

    AttachmentDownload:
      type: object
      properties:
        download_url:
          type: string
          format: uri
          description: Presigned S3 download URL (valid for 5 minutes)
        filename:
          type: string
        content_type:
          type: string
        size:
          type: integer

    InboxListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Email'
        pagination:
          $ref: '#/components/schemas/Pagination'
        meta:
          $ref: '#/components/schemas/Meta'

    Email:
      type: object
      properties:
        id:
          type: string
        uid:
          type: string
          description: IMAP UID
        message_id:
          type: string
          description: RFC 2822 Message-ID header (use for In-Reply-To when replying)
        thread_id:
          type: string
          description: Thread identifier for grouping related messages
        from:
          type: string
          description: Raw from header (may include display name)
        from_name:
          type: string
          description: Sender display name
        from_email:
          type: string
          description: Sender email address
        to:
          type: string
        cc:
          type: string
        bcc:
          type: string
        reply_to:
          type: string
        subject:
          type: string
        body:
          type: string
          description: Full email body text
        body_preview:
          type: string
          description: First 200 characters of the body
        received_at:
          type: string
          format: date-time
        has_attachments:
          type: boolean
        is_read:
          type: boolean
        mailbox:
          type: string
          description: Email address of the mailbox that received this message

    SendEmailRequest:
      type: object
      required:
        - to
        - subject
      properties:
        to:
          type: string
          format: email
        subject:
          type: string
        body:
          type: string
        cc:
          type: string
        bcc:
          type: string
        html:
          type: boolean
          default: false
        attachments:
          type: array
          maxItems: 3
          items:
            type: object
            required: [key, filename, content_type]
            properties:
              key:
                type: string
                description: S3 object key from presign upload
              filename:
                type: string
              content_type:
                type: string
          description: File attachments (max 3)
        in_reply_to:
          type: string
          description: Message-ID of the email being replied to (for threading)
        references:
          type: string
          description: Space-separated Message-IDs for the thread (RFC 2822)

    SendEmailResponse:
      type: object
      properties:
        success:
          type: boolean
        from:
          type: string
        to:
          type: string
        subject:
          type: string
        message_id:
          type: string
          description: Message-ID of the sent email (use for threading subsequent replies)

    MarkReadRequest:
      type: object
      required:
        - is_read
      properties:
        is_read:
          type: boolean
          description: True to mark as read, false to mark as unread

    MarkReadResponse:
      type: object
      properties:
        message_id:
          type: string
        is_read:
          type: boolean

    JobListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Job'
        pagination:
          $ref: '#/components/schemas/Pagination'
        meta:
          $ref: '#/components/schemas/Meta'

    Job:
      type: object
      description: |
        An async job tracking a long-running operation (domain setup/deletion,
        warming enable/disable, etc.).

        **Lifecycle:** `status` starts as `pending`, transitions to `in_progress`
        once the worker picks up the message, and ends as `completed` or `failed`.
        On `failed`, the `error` object describes what went wrong.

        **Progress map:** `progress` is a map of step name to step status
        (`pending` → `in_progress` → `completed`/`failed`/`skipped`). Step names
        depend on job type:

        - `domain_setup`: any subset of `registration`, `mailcow`, `dns_zone`, `dns_records`
          (only the steps requested by the original setup call are present)
        - `domain_delete`: `dns`, `mailcow`, `firebase`
        - `email_user_delete`, `warming_enable`, `warming_disable`: counter-shaped
          progress with `total`, `completed`, `failed` integer fields
      properties:
        job_id:
          type: string
          description: Unique job identifier.
        type:
          type: string
          enum: [domain_setup, domain_delete, email_user_delete, warming_enable, warming_disable]
        status:
          type: string
          enum: [pending, in_progress, completed, failed]
        progress:
          type: object
          additionalProperties: true
          description: |
            Per-step progress for named-step jobs (domain_setup/domain_delete) or
            counter-shaped progress (`{total, completed, failed}`) for batch jobs.
          example:
            registration: completed
            mailcow: completed
            dns_zone: in_progress
            dns_records: pending
        result:
          type: object
          nullable: true
          description: Operation result (shape varies by job type). `null` until job completes.
        error:
          type: object
          nullable: true
          description: Error details when `status=failed`.
          properties:
            message:
              type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true

    JobResponse:
      # Endpoints that return a single job (POST /v1/domains/setup, DELETE
      # /v1/domains/{id}, GET /v1/jobs/{job_id}) return the Job object directly,
      # not wrapped in `{data, meta}`.
      $ref: '#/components/schemas/Job'

    ExportRequest:
      type: object
      required:
        - format
      properties:
        format:
          type: string
          description: Export format (e.g., smartlead, instantly, hubspot)
          example: smartlead
        domains:
          type: array
          items:
            type: string
          description: Optional list of domains to filter by (exports only users from these domains)
        get_all:
          type: boolean
          default: false
          description: If true, exports all email users across all domains

    ExportResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            download_url:
              type: string
              format: uri
              description: Presigned S3 URL to download the CSV (expires in 7 days)
            format:
              type: string
              description: The export format used
            user_count:
              type: integer
              description: Number of email users exported
            expires_at:
              type: string
              format: date-time
              description: When the download URL expires
        meta:
          $ref: '#/components/schemas/Meta'

    ExportFormatsResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            formats:
              type: array
              items:
                type: string
              description: List of supported export format names
        meta:
          $ref: '#/components/schemas/Meta'
