openapi: 3.0.3
info:
  title: Everlit TTS API
  description: |
    High-quality text-to-speech as a REST API: send text, get back a
    production-ready MP3 narrated by any voice in your catalog — including
    your own cloned voices — with word-level timestamps included.

    Full documentation: https://creator.everlit.audio/docs/tts-api
  version: 1.1.0
  contact:
    name: Everlit Support
    email: support@everlit.audio
    url: https://everlit.audio
  license:
    name: Proprietary
    url: https://everlit.audio/terms

servers:
  - url: https://api.everlit.audio
    description: Production

security:
  - BearerAuth: []

tags:
  - name: Speech
    description: Text-to-speech synthesis jobs
  - name: Voices
  - name: Usage

paths:
  /v1/speech:
    post:
      tags: [Speech]
      summary: Create a speech job
      operationId: createSpeech
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          schema: { type: string, maxLength: 255 }
          description: Any unique string; safe-retry semantics (Stripe convention).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateSpeechRequest' }
      responses:
        '202':
          description: Job accepted
          headers:
            Location: { schema: { type: string } }
            RateLimit-Limit: { schema: { type: integer } }
            RateLimit-Remaining: { schema: { type: integer } }
            RateLimit-Reset: { schema: { type: integer } }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Speech' }
        '200':
          description: Idempotent replay of an existing job (Idempotency-Replayed header set)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Speech' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unspeakable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Unavailable' }
    get:
      tags: [Speech]
      summary: List speech jobs (newest first)
      operationId: listSpeech
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
        - { name: starting_after, in: query, schema: { type: string }, description: 'Job id cursor' }
      responses:
        '200':
          description: Page of jobs
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, enum: [list] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Speech' }
                  has_more: { type: boolean }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/speech/{id}:
    get:
      tags: [Speech]
      summary: Fetch a speech job
      operationId: getSpeech
      parameters: [ { $ref: '#/components/parameters/SpeechId' } ]
      responses:
        '200':
          description: The job
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Speech' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Speech]
      summary: Delete a job's audio (usage records are retained)
      operationId: deleteSpeech
      parameters: [ { $ref: '#/components/parameters/SpeechId' } ]
      responses:
        '204': { description: Audio deleted }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /v1/speech/{id}/audio:
    get:
      tags: [Speech]
      summary: Download the audio
      description: |
        The deterministic download URL — returned as `audio_url` at creation,
        stable for the full 30-day retention. 302-redirects to short-lived
        storage; follow redirects.
      operationId: getSpeechAudio
      parameters: [ { $ref: '#/components/parameters/SpeechId' } ]
      responses:
        '302':
          description: Redirect to the audio bytes
          headers:
            Location: { schema: { type: string } }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/Gone' }

  /v1/speech/{id}/timings:
    get:
      tags: [Speech]
      summary: Word-level timestamps
      operationId: getSpeechTimings
      parameters: [ { $ref: '#/components/parameters/SpeechId' } ]
      responses:
        '200':
          description: Word timings
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SpeechTimings' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '410': { $ref: '#/components/responses/Gone' }

  /v1/voices:
    get:
      tags: [Voices]
      summary: Voices available to your account
      operationId: listVoices
      parameters:
        - { name: language, in: query, schema: { type: string }, example: en }
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 100 } }
        - { name: starting_after, in: query, schema: { type: string } }
      responses:
        '200':
          description: Voice list
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, enum: [list] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Voice' }
                  has_more: { type: boolean }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/languages:
    get:
      tags: [Voices]
      summary: Supported languages with billing multipliers and transcript availability
      description: >
        Each character of `text` bills at its script's multiplier
        (billable_characters = ceil of the per-character sum), so every
        language costs roughly the same per finished hour of audio. The
        `transcripts` flag tells you whether jobs in that language produce
        word-level timestamps via /timings.
      operationId: listLanguages
      responses:
        '200':
          description: Language list
          content:
            application/json:
              schema:
                type: object
                properties:
                  object: { type: string, enum: [list] }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        code: { type: string, example: zh }
                        name: { type: string, example: Chinese }
                        billing_multiplier: { type: number, example: 3.0 }
                        transcripts: { type: boolean, example: false }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /v1/mcp:
    post:
      operationId: mcp
      summary: MCP server (Model Context Protocol)
      description: |
        Streamable HTTP MCP endpoint for AI agents (stateless, JSON responses).
        The request and response bodies are JSON-RPC 2.0 messages per the MCP
        specification; the tools mirror the REST endpoints one-to-one. See the
        "MCP" section of the TTS API guide.
      tags: [MCP]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: A single JSON-RPC 2.0 request.
      responses:
        '200':
          description: A JSON-RPC 2.0 response. Tool failures are returned as tool results with isError=true.
          content:
            application/json:
              schema:
                type: object
        '401':
          description: Missing or invalid API key.
        '403':
          description: Wrong key type or TTS API not enabled.
        '429':
          description: Read rate limit exceeded (read_rate_limit_exceeded); honor Retry-After.
  /v1/usage:
    get:
      tags: [Usage]
      summary: Usage aggregates
      operationId: getUsage
      parameters:
        - { name: from, in: query, schema: { type: string, format: date-time } }
        - { name: to, in: query, schema: { type: string, format: date-time } }
      responses:
        '200':
          description: Month-to-date and daily usage
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UsageSummary' }
        '401': { $ref: '#/components/responses/Unauthorized' }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: An `api_` key minted in Studio (API → Keys). Server-side only.

  parameters:
    SpeechId:
      name: id
      in: path
      required: true
      schema: { type: string }
      example: spch_01jxam5rk2v9q8w7e6r5t4y3u2

  schemas:
    CreateSpeechRequest:
      type: object
      required: [text, voice]
      additionalProperties: false
      properties:
        text:
          type: string
          description: >
            Text to narrate. Billed on script-weighted characters as
            submitted — see GET /v1/languages for per-script multipliers.
        voice:
          type: string
          description: Voice id from GET /v1/voices.
          example: "109"
        language:
          type: string
          example: en
          nullable: true
          description: >
            ISO 639-1 hint; see GET /v1/languages. Never lowers the billing
            rate (only disambiguates Japanese vs Chinese Han readings).
        format: { type: string, enum: [mp3], default: mp3 }
        callback_url:
          type: string
          format: uri
          nullable: true
          description: https URL that receives the finished job (signed).
        metadata:
          type: object
          nullable: true
          maxProperties: 8
          additionalProperties: { type: string }

    Speech:
      type: object
      properties:
        object: { type: string, enum: [speech] }
        id: { type: string, example: spch_01jxam5rk2v9q8w7e6r5t4y3u2 }
        status: { type: string, enum: [queued, processing, succeeded, failed, expired, deleted] }
        created_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time, nullable: true }
        expires_at: { type: string, format: date-time, nullable: true }
        voice: { type: string }
        language: { type: string, nullable: true }
        format: { type: string }
        audio_url:
          type: string
          description: Deterministic download URL, stable for the retention window.
        timings_available: { type: boolean }
        usage:
          type: object
          properties:
            characters: { type: integer }
            billable_characters: { type: integer, nullable: true }
            audio_seconds: { type: number, nullable: true }
        audio:
          type: object
          nullable: true
          properties:
            content_type: { type: string, example: audio/mpeg }
            bytes: { type: integer }
            duration_seconds: { type: number }
        error:
          type: object
          nullable: true
          properties:
            code: { type: string }
            message: { type: string }
        metadata: { type: object, additionalProperties: { type: string } }

    SpeechTimings:
      type: object
      properties:
        object: { type: string, enum: [speech.timings] }
        id: { type: string }
        duration_seconds: { type: number }
        words:
          type: array
          items:
            type: object
            properties:
              word: { type: string }
              start: { type: number }
              end: { type: number }

    Voice:
      type: object
      properties:
        id: { type: string, example: "109" }
        name: { type: string }
        style: { type: string }
        language: { type: string, nullable: true }
        gender: { type: string, nullable: true }
        owned_by: { type: string, enum: [everlit, publisher] }

    UsageSummary:
      type: object
      properties:
        object: { type: string, enum: [usage] }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        month_to_date_characters: { type: integer, nullable: true }
        monthly_character_limit: { type: integer, nullable: true }
        daily:
          type: array
          items:
            type: object
            properties:
              day: { type: string, format: date }
              auth_token_id: { type: integer }
              requests: { type: integer }
              succeeded: { type: integer }
              characters: { type: integer }
              audio_seconds: { type: number }

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            type:
              type: string
              enum: [invalid_request_error, authentication_error, permission_error,
                     not_found_error, conflict_error, rate_limit_error, api_error]
            code: { type: string, description: Stable machine-readable code — branch on this. }
            message: { type: string }
            param: { type: string, nullable: true }
            request_id: { type: string }

  responses:
    BadRequest:
      description: Invalid request
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unauthorized:
      description: Missing/invalid/revoked/expired API key
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: Wrong key type, inaccessible voice, or SKU not enabled
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: Not found (cross-account resources also 404)
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Conflict:
      description: Idempotency conflict or job not complete
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Gone:
      description: Audio expired (30-day retention) or deleted
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unspeakable:
      description: No speakable content
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    RateLimited:
      description: Rate/quota/concurrency limit (see Retry-After)
      headers:
        Retry-After: { schema: { type: integer } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Unavailable:
      description: Temporarily unavailable (see Retry-After)
      headers:
        Retry-After: { schema: { type: integer } }
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
