openapi: 3.1.0
info:
  title: ExportDou API
  version: 1.0.0
  description: Durable API for exporting public Douyin video comments and replies.
  license:
    name: Proprietary API; CLI is MIT licensed
    url: https://exportdou.cn/terms
  contact:
    url: https://exportdou.cn/developers
servers:
  - url: https://exportdou.cn/api/v1
security:
  - apiKey: []
tags:
  - name: Account
    description: Account identity and credit balance.
  - name: Videos
    description: Resolve public Douyin video inputs before exporting.
  - name: Exports
    description: Create, monitor, preview, download and delete durable exports.
paths:
  /account:
    get:
      operationId: getAccount
      tags: [Account]
      summary: Validate a credential and return the current account
      responses:
        '200':
          description: Current account
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Account'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /credits:
    get:
      operationId: getCredits
      tags: [Account]
      summary: Return available and reserved credits
      responses:
        '200':
          description: Credit wallet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Credits'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /videos/preview:
    post:
      operationId: previewDouyinVideo
      tags: [Videos]
      summary: Resolve a Douyin link or share text before exporting
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [input]
              properties:
                input:
                  type: string
                  minLength: 1
                  maxLength: 8000
      responses:
        '200':
          description: Video metadata and a small public comment sample
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VideoPreview'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
  /exports:
    get:
      operationId: listExports
      tags: [Exports]
      summary: List exports newest first
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: cursor
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Export page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportList'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createExport
      tags: [Exports]
      summary: Create an idempotent comment export
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
            minLength: 1
            maxLength: 200
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateExport'
      responses:
        '201':
          description: Export accepted and credits reserved
          content:
            application/json:
              schema:
                type: object
                required: [id, status, reservedCredits, dispatchDelayed]
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    $ref: '#/components/schemas/ExportStatus'
                  reservedCredits:
                    type: integer
                  dispatchDelayed:
                    type: boolean
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: Insufficient credits
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Idempotency or concurrency conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
  /export-requests/{idempotencyKey}:
    get:
      operationId: recoverExportByIdempotencyKey
      tags: [Exports]
      summary: Recover a create request after an ambiguous network failure
      parameters:
        - name: idempotencyKey
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Matching export
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportDetail'
        '404':
          $ref: '#/components/responses/NotFound'
  /exports/{exportId}:
    get:
      operationId: getExport
      tags: [Exports]
      summary: Return public progress, billing and result metadata
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: Export detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportDetail'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      operationId: deleteExport
      tags: [Exports]
      summary: Delete a closed export and its private objects
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '204':
          description: Deleted
        '409':
          description: Export is still running or billing is not closed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /exports/{exportId}/cancel:
    post:
      operationId: cancelExport
      tags: [Exports]
      summary: Request cancellation of a running export
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: Cancellation state
          content:
            application/json:
              schema:
                type: object
                required: [id, status, cancelRequested]
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    $ref: '#/components/schemas/ExportStatus'
                  cancelRequested:
                    type: boolean
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /exports/{exportId}/preview:
    get:
      operationId: previewExportResult
      tags: [Exports]
      summary: Return up to 50 normalized comments from the final result
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: Final result preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResultPreview'
        '404':
          $ref: '#/components/responses/NotFound'
  /exports/{exportId}/download:
    get:
      operationId: downloadExport
      tags: [Exports]
      summary: Download the complete private CSV or XLSX file
      parameters:
        - $ref: '#/components/parameters/ExportId'
      responses:
        '200':
          description: Complete private export after following the redirect
          headers:
            X-ExportDou-Row-Count:
              schema:
                type: integer
            X-ExportDou-SHA256:
              schema:
                type: string
          content:
            text/csv:
              schema:
                type: string
                format: binary
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
        '302':
          description: Short-lived signed download redirect
        '404':
          $ref: '#/components/responses/NotFound'
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: ed_live_...
      description: Create at https://exportdou.cn/settings/api-keys or run npx exportdou login.
  parameters:
    ExportId:
      name: exportId
      in: path
      required: true
      schema:
        type: string
        format: uuid
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Missing, invalid or revoked credential
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found or not owned by the credential
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Request rate exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
        message:
          type: string
        retryAfterSeconds:
          type: integer
          minimum: 1
    Account:
      type: object
      required: [id, email, displayName, accountStatus, createdAt]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: [string, 'null']
        displayName:
          type: [string, 'null']
        accountStatus:
          type: string
          enum: [active, suspended, deleted]
        createdAt:
          type: string
          format: date-time
    Credits:
      type: object
      required: [availableBalance, reservedBalance, lifetimeGranted, lifetimeSpent, lifetimeExpired, updatedAt]
      properties:
        availableBalance:
          type: integer
        reservedBalance:
          type: integer
        lifetimeGranted:
          type: integer
        lifetimeSpent:
          type: integer
        lifetimeExpired:
          type: integer
        updatedAt:
          type: string
          format: date-time
    VideoPreview:
      type: object
      required: [canonicalUrl, cached, comments, playUrls]
      properties:
        canonicalUrl:
          type: string
          format: uri
        title:
          type: [string, 'null']
        authorName:
          type: [string, 'null']
        coverUrl:
          type: [string, 'null']
        reportedCommentCount:
          type: [integer, 'null']
        playUrls:
          type: array
          items:
            type: string
            format: uri
        comments:
          type: array
          items:
            type: object
        cached:
          type: boolean
    CreateExport:
      type: object
      required: [input, resultLimit, includeReplies, format]
      properties:
        input:
          type: string
          minLength: 1
          maxLength: 8000
        resultLimit:
          type: integer
          minimum: 1
          maximum: 200000
        includeReplies:
          type: boolean
        format:
          type: string
          enum: [csv, xlsx]
    ExportStatus:
      type: string
      enum: [queued, processing, rendering, completed, partial, failed, cancelled, expired, deleting]
    ExportSummary:
      type: object
      required: [id, status, resultLimit, includeReplies, format, deliveredRows, billedCredits, cancelRequested, title, createdAt, updatedAt, completedAt]
      properties:
        id:
          type: string
          format: uuid
        status:
          $ref: '#/components/schemas/ExportStatus'
        resultLimit:
          type: integer
        includeReplies:
          type: boolean
        format:
          type: string
          enum: [csv, xlsx]
        deliveredRows:
          type: integer
        billedCredits:
          type: integer
        cancelRequested:
          type: boolean
        title:
          type: [string, 'null']
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        completedAt:
          type: [string, 'null']
          format: date-time
    ExportDetail:
      allOf:
        - $ref: '#/components/schemas/ExportSummary'
        - type: object
          required: [inputUrl, canonicalUrl, progress, result, error, reservedCredits, releasedCredits, replyCoverage, retryAfterSeconds, startedAt]
          properties:
            inputUrl:
              type: string
            canonicalUrl:
              type: [string, 'null']
            progress:
              type: object
              required: [stage, targetRows, deliveredRows, rootRows, replyRows, percentage]
              properties:
                stage:
                  type: string
                  enum: [queued, collecting_roots, collecting_replies, rendering, done]
                targetRows:
                  type: integer
                deliveredRows:
                  type: integer
                rootRows:
                  type: integer
                replyRows:
                  type: integer
                percentage:
                  type: integer
                  minimum: 0
                  maximum: 100
            result:
              oneOf:
                - type: 'null'
                - type: object
                  required: [ready, format, rowCount, sizeBytes, sha256, expiresAt, previewPath, downloadPath]
                  properties:
                    ready:
                      const: true
                    format:
                      type: string
                      enum: [csv, xlsx]
                    rowCount:
                      type: integer
                    sizeBytes:
                      type: integer
                    sha256:
                      type: string
                    expiresAt:
                      type: string
                      format: date-time
                    previewPath:
                      type: string
                    downloadPath:
                      type: string
            error:
              type: [object, 'null']
            reservedCredits:
              type: integer
            releasedCredits:
              type: integer
            replyCoverage:
              type: string
              enum: [not_requested, complete, partial, unknown]
            retryAfterSeconds:
              type: [integer, 'null']
            startedAt:
              type: [string, 'null']
              format: date-time
    ExportList:
      type: object
      required: [exports, nextCursor]
      properties:
        exports:
          type: array
          items:
            $ref: '#/components/schemas/ExportSummary'
        nextCursor:
          type: [string, 'null']
    ResultPreview:
      type: object
      required: [jobId, comments, previewCount, totalRows, rootRows, replyRows]
      properties:
        jobId:
          type: string
          format: uuid
        comments:
          type: array
          maxItems: 50
          items:
            type: object
        previewCount:
          type: integer
        totalRows:
          type: integer
        rootRows:
          type: integer
        replyRows:
          type: integer
