openapi: 3.1.0
info:
  title: YiDoc Public Translation API
  version: 1.0.0
  summary: Public API v1 for PDF translation workflows.
  description: |
    This OpenAPI document covers all eight public API endpoints exposed under
    `/v1`: identity (`/v1/me`), file upload (`/v1/files`), translation
    estimates (`/v1/translation-estimates`), translation confirmation
    (`/v1/translations`), translation status (`/v1/translations/{id}`),
    translation events (`/v1/translations/{id}/events`), artifact download
    (`/v1/translations/{id}/artifacts/{artifact_id}`), and credit/request
    usage reports (`/v1/usage`).
servers:
  - url: /
    description: Current origin
  - url: http://localhost:8000
    description: Local backend
security:
  - bearerAuth: []
tags:
  - name: Files
    description: PDF upload endpoints.
  - name: Translation estimates
    description: Estimate token and credit usage before confirming a job.
  - name: Translations
    description: Confirm and inspect translation jobs.
  - name: Account
    description: Identity, API key, and credit summary for the current bearer key.
  - name: Usage
    description: Credit and request usage reports for the API key owner.
paths:
  /v1/me:
    get:
      tags:
        - Account
      operationId: getMe
      summary: Get current API key identity
      description: Returns the account, available/frozen credits, and the API key metadata for the bearer key currently authenticating the request.
      responses:
        "200":
          description: API key identity.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeResponse"
              examples:
                active:
                  value:
                    account:
                      user_id: "018f0a00-0000-7000-8000-000000000001"
                      organization_id: null
                      available_credits: 100
                      frozen_credits: 0
                    api_key:
                      id: "018f0a00-0000-7000-8000-0000000000aa"
                      name: "ci-pipeline"
                      prefix: "yd_sk_live_abcdef1234"
                      status: active
                      last_used_at: "2026-07-04T14:35:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /v1/files:
    post:
      tags:
        - Files
      operationId: uploadFile
      summary: Upload a PDF file
      description: Uploads a PDF and returns a file id. This endpoint does not create a translation job or hold credits.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                  description: PDF file to translate.
      responses:
        "201":
          description: File uploaded.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FileUploadResponse"
              examples:
                uploaded:
                  value:
                    file_id: "018f0b90-f27c-74f9-8c7d-1bbf87e5df50"
                    file_name: demo.pdf
                    size_bytes: 482391
                    status: uploaded
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/FileTooLarge"
        "422":
          $ref: "#/components/responses/UnsupportedPdf"

  /v1/translation-estimates:
    post:
      tags:
        - Translation estimates
      operationId: createTranslationEstimate
      summary: Create a translation estimate
      description: Inspects an uploaded PDF and estimates token and credit usage. The estimate must be confirmed before it expires.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TranslationEstimateRequest"
            examples:
              article:
                value:
                  file_id: "018f0b90-f27c-74f9-8c7d-1bbf87e5df50"
                  source_language: auto
                  target_language: zh-CN
                  mode: article
      responses:
        "201":
          description: Estimate created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TranslationEstimateResponse"
              examples:
                estimated:
                  value:
                    estimate_id: "018f0ba1-d42f-7f04-a275-469f6a8cefb1"
                    file_id: "018f0b90-f27c-74f9-8c7d-1bbf87e5df50"
                    source_language: auto
                    target_language: zh-CN
                    mode: article
                    page_count: 3
                    text_chars: 2400
                    estimated_input_tokens: 1200
                    estimated_output_tokens: 1500
                    estimated_total_tokens: 2700
                    estimated_credits: 3
                    expires_at: "2026-07-04T15:30:00Z"
                    credits:
                      available: 100
                      frozen: 0
                      can_afford: true
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/FileNotFound"
        "422":
          $ref: "#/components/responses/UnsupportedPdf"

  /v1/translations:
    post:
      tags:
        - Translations
      operationId: confirmTranslation
      summary: Confirm a translation job
      description: Creates a translation job from an estimate, holds estimated credits, and queues the job. This endpoint requires an Idempotency-Key header.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ConfirmTranslationRequest"
            examples:
              confirm:
                value:
                  estimate_id: "018f0ba1-d42f-7f04-a275-469f6a8cefb1"
      responses:
        "201":
          description: Translation job queued.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TranslationResponse"
              examples:
                queued:
                  value:
                    translation_id: "018f0bb5-f3de-78ef-bef5-9b5d76cf8c0f"
                    file_id: "018f0b90-f27c-74f9-8c7d-1bbf87e5df50"
                    file_name: demo.pdf
                    status: queued
                    source_language: auto
                    target_language: zh-CN
                    mode: article
                    estimated_input_tokens: 1200
                    estimated_output_tokens: 1500
                    estimated_total_tokens: 2700
                    estimated_credits: 3
                    actual_input_tokens: null
                    actual_output_tokens: null
                    actual_total_tokens: null
                    actual_credits: null
                    error_code: null
                    error_message: null
                    created_at: "2026-07-04T14:31:00Z"
                    updated_at: "2026-07-04T14:31:00Z"
                    artifacts: []
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/InsufficientCredits"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/EstimateNotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "503":
          $ref: "#/components/responses/QueueUnavailable"

  /v1/translations/{translation_id}:
    get:
      tags:
        - Translations
      operationId: getTranslation
      summary: Get translation status
      description: Returns the current translation status, estimated and actual token usage, credit usage, errors, and downloadable artifacts.
      parameters:
        - name: translation_id
          in: path
          required: true
          schema:
            type: string
          description: Translation id returned by POST /v1/translations.
      responses:
        "200":
          description: Translation status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TranslationResponse"
              examples:
                succeeded:
                  value:
                    translation_id: "018f0bb5-f3de-78ef-bef5-9b5d76cf8c0f"
                    file_id: "018f0b90-f27c-74f9-8c7d-1bbf87e5df50"
                    file_name: demo.pdf
                    status: succeeded
                    source_language: auto
                    target_language: zh-CN
                    mode: article
                    estimated_input_tokens: 1200
                    estimated_output_tokens: 1500
                    estimated_total_tokens: 2700
                    estimated_credits: 3
                    actual_input_tokens: 1188
                    actual_output_tokens: 1432
                    actual_total_tokens: 2620
                    actual_credits: 3
                    error_code: null
                    error_message: null
                    created_at: "2026-07-04T14:31:00Z"
                    updated_at: "2026-07-04T14:35:00Z"
                    artifacts:
                      - artifact_id: "018f0bc0-930d-7d1b-a225-218329a36efa"
                        type: visual_pdf
                        download_url: "/v1/translations/018f0bb5-f3de-78ef-bef5-9b5d76cf8c0f/artifacts/018f0bc0-930d-7d1b-a225-218329a36efa"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TranslationNotFound"

  /v1/translations/{translation_id}/events:
    get:
      tags:
        - Translations
      operationId: listTranslationEvents
      summary: List translation timeline events
      description: Returns the full ordered timeline of events for a translation job (insertion order, not paginated). The same list is exposed under both `events` and `items` for client compatibility.
      parameters:
        - $ref: "#/components/parameters/TranslationId"
      responses:
        "200":
          description: Translation events.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TranslationEventsResponse"
              examples:
                succeeded:
                  value:
                    events:
                      - id: "018f0bc0-1111-7000-8000-000000000001"
                        translation_id: "018f0bb5-f3de-78ef-bef5-9b5d76cf8c0f"
                        stage: analyze
                        progress: 25
                        message: "PDF 解析完成"
                        metadata: {}
                        created_at: "2026-07-04T14:31:30Z"
                        event_type: translation.analyze
                      - id: "018f0bc0-2222-7000-8000-000000000002"
                        translation_id: "018f0bb5-f3de-78ef-bef5-9b5d76cf8c0f"
                        stage: translate
                        progress: 80
                        message: "翻译进行中"
                        metadata:
                          page_index: 2
                        created_at: "2026-07-04T14:32:30Z"
                        event_type: translation.translate
                    items:
                      - id: "018f0bc0-1111-7000-8000-000000000001"
                        translation_id: "018f0bb5-f3de-78ef-bef5-9b5d76cf8c0f"
                        stage: analyze
                        progress: 25
                        message: "PDF 解析完成"
                        metadata: {}
                        created_at: "2026-07-04T14:31:30Z"
                        event_type: translation.analyze
                      - id: "018f0bc0-2222-7000-8000-000000000002"
                        translation_id: "018f0bb5-f3de-78ef-bef5-9b5d76cf8c0f"
                        stage: translate
                        progress: 80
                        message: "翻译进行中"
                        metadata:
                          page_index: 2
                        created_at: "2026-07-04T14:32:30Z"
                        event_type: translation.translate
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/TranslationNotFound"

  /v1/translations/{translation_id}/artifacts/{artifact_id}:
    get:
      tags:
        - Translations
      operationId: downloadTranslationArtifact
      summary: Download translated PDF
      description: Downloads a generated translation artifact. Use the `download_url` returned in `artifacts[]` from GET /v1/translations/{translation_id}.
      parameters:
        - name: translation_id
          in: path
          required: true
          schema:
            type: string
          description: Translation id returned by POST /v1/translations.
        - name: artifact_id
          in: path
          required: true
          schema:
            type: string
          description: Artifact id from the translation status response.
      responses:
        "200":
          description: Translated PDF artifact.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/ArtifactNotFound"

  /v1/usage:
    get:
      tags:
        - Usage
      operationId: listUsage
      summary: List credit and request usage
      description: |
        Returns aggregated usage for the API key owner. The `groupBy` parameter
        changes the shape of each item: `overview` returns per-channel totals,
        `api_key` returns per-API-key breakdowns, `translation` returns one row
        per credit transaction tied to a translation.
      parameters:
        - $ref: "#/components/parameters/UsageFromDate"
        - $ref: "#/components/parameters/UsageToDate"
        - $ref: "#/components/parameters/UsageGroupBy"
        - $ref: "#/components/parameters/UsageChannel"
        - $ref: "#/components/parameters/UsagePage"
        - $ref: "#/components/parameters/UsagePageSize"
      responses:
        "200":
          description: Usage report.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UsageResponse"
              examples:
                overview:
                  value:
                    summary:
                      apiCredits: 12
                      webCredits: 4
                      totalCredits: 16
                      requestCount: 8
                      failedRequests: 1
                    items:
                      - channel: api
                        credits: 12
                        requestCount: 8
                        failedRequests: 1
                      - channel: web
                        credits: 4
                        requestCount: 0
                        failedRequests: 0
                    page: 1
                    pageSize: 10
                    total: 2
                    totalPages: 1
                    range:
                      from: "2026-07-01"
                      to: "2026-07-04"
                    groupBy: overview
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API key
      description: "Use `Authorization: Bearer yd_sk_...`."

  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 255
      description: Stable client-generated key used to safely retry POST /v1/translations.
      example: job-001

    TranslationId:
      name: translation_id
      in: path
      required: true
      schema:
        type: string
      description: Translation id returned by POST /v1/translations.
      example: 018f0bb5-f3de-78ef-bef5-9b5d76cf8c0f

    UsageFromDate:
      name: from
      in: query
      required: false
      schema:
        type: string
        format: date
      description: Inclusive lower bound for the usage window (ISO date YYYY-MM-DD).
      example: "2026-07-01"

    UsageToDate:
      name: to
      in: query
      required: false
      schema:
        type: string
        format: date
      description: Exclusive upper bound for the usage window (ISO date YYYY-MM-DD).
      example: "2026-07-05"

    UsageGroupBy:
      name: groupBy
      in: query
      required: false
      schema:
        $ref: "#/components/schemas/UsageGroupBy"
      description: How items should be grouped. Defaults to `overview` when omitted.

    UsageChannel:
      name: channel
      in: query
      required: false
      schema:
        $ref: "#/components/schemas/UsageChannel"
      description: Restrict credit-transaction aggregation to one channel.

    UsagePage:
      name: page
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        default: 1
      description: 1-based page number for paginated items.

    UsagePageSize:
      name: pageSize
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 10
      description: Page size for paginated items (1-100).

  responses:
    BadRequest:
      description: Invalid request.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            idempotencyKeyRequired:
              value:
                error:
                  code: idempotency_key_required
                  message: Idempotency-Key header is required
            fileEmpty:
              value:
                error:
                  code: file_empty
                  message: Uploaded file is empty
    Unauthorized:
      description: API key is missing, malformed, deleted, or unknown.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            invalidApiKey:
              value:
                error:
                  code: invalid_api_key
                  message: API key is missing or invalid
    Forbidden:
      description: API key is disabled.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            apiKeyDisabled:
              value:
                error:
                  code: api_key_disabled
                  message: API key is disabled
    InsufficientCredits:
      description: Available credits are insufficient.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            insufficientCredits:
              value:
                error:
                  code: insufficient_credits
                  message: Available credits are insufficient
    FileTooLarge:
      description: Uploaded file exceeds the configured limit.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            fileTooLarge:
              value:
                error:
                  code: file_too_large
                  message: Uploaded file exceeds the size limit
    UnsupportedPdf:
      description: PDF is unsupported or OCR is required.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            unsupportedPdf:
              value:
                error:
                  code: unsupported_pdf
                  message: Only PDF files are supported
            ocrRequired:
              value:
                error:
                  code: ocr_required
                  message: OCR is required for this PDF
    FileNotFound:
      description: File was not found or does not belong to this API key owner.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            fileNotFound:
              value:
                error:
                  code: file_not_found
                  message: File was not found
    EstimateNotFound:
      description: Estimate was not found or does not belong to this API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            estimateNotFound:
              value:
                error:
                  code: estimate_not_found
                  message: Estimate was not found
    TranslationNotFound:
      description: Translation was not found or does not belong to this API key owner.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            translationNotFound:
              value:
                error:
                  code: translation_not_found
                  message: Translation was not found
    ArtifactNotFound:
      description: Artifact was not found or does not belong to this translation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            artifactNotFound:
              value:
                error:
                  code: artifact_not_found
                  message: Artifact was not found
    Conflict:
      description: Estimate or idempotency conflict.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            idempotencyConflict:
              value:
                error:
                  code: idempotency_conflict
                  message: Idempotency-Key was reused with different parameters
            estimateExpired:
              value:
                error:
                  code: estimate_expired
                  message: Estimate has expired
    QueueUnavailable:
      description: Translation queue is unavailable.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/PublicApiError"
          examples:
            queueUnavailable:
              value:
                error:
                  code: queue_unavailable
                  message: Translation queue is unavailable

  schemas:
    FileUploadResponse:
      type: object
      required:
        - file_id
        - file_name
        - size_bytes
        - status
      properties:
        file_id:
          type: string
          description: Public file id used by POST /v1/translation-estimates.
        file_name:
          type: string
        size_bytes:
          type: integer
          minimum: 0
        status:
          type: string
          enum:
            - uploaded

    TranslationEstimateRequest:
      type: object
      required:
        - file_id
      properties:
        file_id:
          type: string
          description: Public file id returned by POST /v1/files.
        source_language:
          type: string
          default: auto
        target_language:
          type: string
          default: zh-CN
        mode:
          $ref: "#/components/schemas/TranslationMode"

    TranslationEstimateResponse:
      type: object
      required:
        - estimate_id
        - file_id
        - source_language
        - target_language
        - mode
        - page_count
        - text_chars
        - estimated_input_tokens
        - estimated_output_tokens
        - estimated_total_tokens
        - estimated_credits
        - expires_at
        - credits
      properties:
        estimate_id:
          type: string
          description: Estimate id used by POST /v1/translations.
        file_id:
          type: string
        source_language:
          type: string
        target_language:
          type: string
        mode:
          $ref: "#/components/schemas/TranslationMode"
        page_count:
          type:
            - integer
            - "null"
          minimum: 0
        text_chars:
          type:
            - integer
            - "null"
          minimum: 0
        estimated_input_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        estimated_output_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        estimated_total_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        estimated_credits:
          type: integer
          minimum: 0
        expires_at:
          type: string
          format: date-time
        credits:
          $ref: "#/components/schemas/CreditSummary"

    ConfirmTranslationRequest:
      type: object
      required:
        - estimate_id
      properties:
        estimate_id:
          type: string
          description: Estimate id returned by POST /v1/translation-estimates.

    TranslationResponse:
      type: object
      required:
        - translation_id
        - file_id
        - file_name
        - status
        - source_language
        - target_language
        - mode
        - estimated_input_tokens
        - estimated_output_tokens
        - estimated_total_tokens
        - estimated_credits
        - actual_input_tokens
        - actual_output_tokens
        - actual_total_tokens
        - actual_credits
        - error_code
        - error_message
        - created_at
        - updated_at
        - artifacts
      properties:
        translation_id:
          type: string
        file_id:
          type:
            - string
            - "null"
        file_name:
          type: string
        status:
          $ref: "#/components/schemas/TranslationStatus"
        source_language:
          type: string
        target_language:
          type: string
        mode:
          $ref: "#/components/schemas/TranslationMode"
        estimated_input_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        estimated_output_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        estimated_total_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        estimated_credits:
          type:
            - integer
            - "null"
          minimum: 0
        actual_input_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        actual_output_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        actual_total_tokens:
          type:
            - integer
            - "null"
          minimum: 0
        actual_credits:
          type:
            - integer
            - "null"
          minimum: 0
        error_code:
          type:
            - string
            - "null"
        error_message:
          type:
            - string
            - "null"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        artifacts:
          type: array
          items:
            $ref: "#/components/schemas/ArtifactSummary"

    ArtifactSummary:
      type: object
      required:
        - artifact_id
        - type
        - download_url
      properties:
        artifact_id:
          type: string
        type:
          type: string
          description: Public artifact type. Translated PDF artifacts are exposed as visual_pdf.
          examples:
            - visual_pdf
        download_url:
          type: string
          description: Relative URL for downloading the artifact.

    CreditSummary:
      type: object
      required:
        - available
        - frozen
        - can_afford
      properties:
        available:
          type: integer
          minimum: 0
        frozen:
          type: integer
          minimum: 0
        can_afford:
          type: boolean

    TranslationMode:
      type: string
      default: article
      enum:
        - article
        - bilingual
        - preserve-layout

    TranslationStatus:
      type: string
      enum:
        - uploaded
        - inspecting
        - awaiting_confirmation
        - estimated
        - queued
        - running
        - pending_payment
        - succeeded
        - failed
        - canceled

    PublicApiError:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
            message:
              type: string

    ApiKeySummary:
      type: object
      required:
        - id
        - name
        - prefix
        - status
        - last_used_at
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
          description: Public key prefix (e.g. `yd_sk_live_xxxxxxxxxx`).
          example: yd_sk_live_abcdef1234
        status:
          type: string
          description: "Key lifecycle state. One of `active`, `disabled`, `deleted`."
          example: active
        last_used_at:
          type:
            - string
            - "null"
          format: date-time

    AccountSummary:
      type: object
      required:
        - user_id
        - organization_id
        - available_credits
        - frozen_credits
      properties:
        user_id:
          type: string
        organization_id:
          type:
            - string
            - "null"
        available_credits:
          type: integer
          minimum: 0
        frozen_credits:
          type: integer
          minimum: 0

    MeResponse:
      type: object
      required:
        - account
        - api_key
      properties:
        account:
          $ref: "#/components/schemas/AccountSummary"
        api_key:
          $ref: "#/components/schemas/ApiKeySummary"

    EventStage:
      type: string
      enum:
        - analyze
        - extract
        - translate
        - build
      description: Pipeline stage inferred from the underlying event type.

    TranslationEvent:
      type: object
      required:
        - id
        - translation_id
        - stage
        - progress
        - message
        - metadata
        - created_at
        - event_type
      properties:
        id:
          type: string
        translation_id:
          type: string
        stage:
          $ref: "#/components/schemas/EventStage"
        progress:
          type: integer
          minimum: 0
          maximum: 100
        message:
          type: string
        metadata:
          type: object
          additionalProperties: true
          description: |
            Free-form event metadata. Internal keys (`user_id`, `task_id`,
            `file_id`, `account_id`, `organization_id`, `credit_hold_id`,
            `owner_user_id`, `storage_uri`) are stripped before returning.
        created_at:
          type: string
          format: date-time
        event_type:
          type: string

    TranslationEventsResponse:
      type: object
      required:
        - events
        - items
      properties:
        events:
          type: array
          items:
            $ref: "#/components/schemas/TranslationEvent"
        items:
          type: array
          items:
            $ref: "#/components/schemas/TranslationEvent"
          description: Alias of `events` retained for client compatibility.

    UsageGroupBy:
      type: string
      enum:
        - overview
        - api_key
        - translation
      default: overview
      description: Grouping strategy for `UsageResponse.items`.

    UsageChannel:
      type: string
      enum:
        - web
        - api
      description: Restrict credit-transaction aggregation to one channel.

    UsageSummary:
      type: object
      required:
        - apiCredits
        - webCredits
        - totalCredits
        - requestCount
        - failedRequests
      properties:
        apiCredits:
          type: integer
          minimum: 0
        webCredits:
          type: integer
          minimum: 0
        totalCredits:
          type: integer
          minimum: 0
        requestCount:
          type: integer
          minimum: 0
        failedRequests:
          type: integer
          minimum: 0

    UsageRange:
      type: object
      required:
        - from
        - to
      properties:
        from:
          type:
            - string
            - "null"
          format: date
        to:
          type:
            - string
            - "null"
          format: date

    UsageOverviewItem:
      type: object
      required:
        - channel
        - credits
        - requestCount
        - failedRequests
      properties:
        channel:
          $ref: "#/components/schemas/UsageChannel"
        credits:
          type: integer
          minimum: 0
        requestCount:
          type: integer
          minimum: 0
        failedRequests:
          type: integer
          minimum: 0

    UsageApiKeySummary:
      type: object
      required:
        - id
        - name
        - prefix
        - status
      properties:
        id:
          type: string
        name:
          type: string
        prefix:
          type: string
        status:
          type: string

    UsageApiKeyItem:
      type: object
      required:
        - apiKey
        - credits
        - requestCount
        - failedRequests
        - latestErrorCode
      properties:
        apiKey:
          oneOf:
            - $ref: "#/components/schemas/UsageApiKeySummary"
            - type: "null"
        credits:
          type: integer
          minimum: 0
        requestCount:
          type: integer
          minimum: 0
        failedRequests:
          type: integer
          minimum: 0
        latestErrorCode:
          type:
            - string
            - "null"

    UsageTranslationItem:
      type: object
      required:
        - id
        - channel
        - credits
        - tokens
        - status
        - translationId
        - fileName
        - occurredAt
        - apiKey
      properties:
        id:
          type: string
        channel:
          $ref: "#/components/schemas/UsageChannel"
        credits:
          type: integer
          minimum: 0
        tokens:
          type:
            - integer
            - "null"
          minimum: 0
        status:
          type:
            - string
            - "null"
        translationId:
          type:
            - string
            - "null"
        fileName:
          type:
            - string
            - "null"
        occurredAt:
          type: string
          format: date-time
        apiKey:
          oneOf:
            - $ref: "#/components/schemas/UsageApiKeySummary"
            - type: "null"

    UsageResponse:
      type: object
      required:
        - summary
        - items
        - page
        - pageSize
        - total
        - totalPages
        - range
        - groupBy
      properties:
        summary:
          $ref: "#/components/schemas/UsageSummary"
        items:
          oneOf:
            - type: array
              items:
                $ref: "#/components/schemas/UsageOverviewItem"
            - type: array
              items:
                $ref: "#/components/schemas/UsageApiKeyItem"
            - type: array
              items:
                $ref: "#/components/schemas/UsageTranslationItem"
          discriminator:
            propertyName: groupBy
            mapping:
              overview: "#/components/schemas/UsageOverviewItem"
              api_key: "#/components/schemas/UsageApiKeyItem"
              translation: "#/components/schemas/UsageTranslationItem"
        page:
          type: integer
          minimum: 1
        pageSize:
          type: integer
          minimum: 1
          maximum: 100
        total:
          type: integer
          minimum: 0
        totalPages:
          type: integer
          minimum: 0
        range:
          $ref: "#/components/schemas/UsageRange"
        groupBy:
          $ref: "#/components/schemas/UsageGroupBy"
