openapi: 3.1.0
info:
  title: VowelMarks Developer API
  version: 1.0.0
  description: |
    Server-side Persian reading infrastructure: source-preserving diacritization,
    Pinglish, Persian-to-English translation, and Basic or Premium speech.

    Requests use strict UTF-8 JSON with snake_case fields. Unknown fields are
    rejected. Generation requests are limited to a 32 KiB serialized JSON body.
    Text is limited to 6,000 Unicode code points and must contain at least one
    Persian-script letter. API keys must not be exposed in browsers.

    Some processing origins sleep while idle, so the first request can take
    several extra seconds. API v1 has no latency or availability SLA. A client
    timeout has an unknown outcome; retry the exact endpoint and body with the
    original Idempotency-Key.
  x-vowelmarks-limits:
    max_json_body_bytes: 32768
    max_text_code_points: 6000
    idempotency_retention_seconds: 86400
    successful_cache_retention_seconds: 2592000
    signed_audio_url_seconds: 3600
    speech_job_metadata_seconds: 86400
    speech_job_max_runtime_seconds: 7200
  contact:
    name: VowelMarks support
    email: support@vowelmarks.com
    url: https://vowelmarks.com/developers/docs/
servers:
  - url: https://api.vowelmarks.com
security:
  - bearerAuth: []
paths:
  /v1/diacritize:
    post:
      summary: Add source-preserving Persian reading marks
      operationId: diacritize
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        description: Serialized JSON must be at most 32 KiB (32,768 bytes).
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DiacritizeRequest'
      responses:
        '200':
          description: Marked text and code-point-indexed operations
          headers: &successHeaders
            X-Request-Id: { $ref: '#/components/headers/RequestId' }
            RateLimit-Limit: { $ref: '#/components/headers/RateLimitLimit' }
            RateLimit-Remaining: { $ref: '#/components/headers/RateLimitRemaining' }
            RateLimit-Reset: { $ref: '#/components/headers/RateLimitReset' }
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiacritizeResponse'
        default:
          $ref: '#/components/responses/Error'
  /v1/pinglish:
    post:
      summary: Render Persian text as readable or systematic Pinglish
      operationId: pinglish
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        description: Serialized JSON must be at most 32 KiB (32,768 bytes).
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PinglishRequest'
      responses:
        '200':
          description: Pinglish with word-level code-point alignment
          headers: *successHeaders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PinglishResponse'
        default:
          $ref: '#/components/responses/Error'
  /v1/translate:
    post:
      summary: Translate Persian to English
      operationId: translate
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        description: Serialized JSON must be at most 32 KiB (32,768 bytes).
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TextRequest'
      responses:
        '200':
          description: English translation
          headers: *successHeaders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TranslationResponse'
        default:
          $ref: '#/components/responses/Error'
  /v1/speech:
    post:
      summary: Generate Basic or Premium Persian speech
      operationId: speech
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        description: Serialized JSON must be at most 32 KiB (32,768 bytes). This synchronous operation returns only after complete audio is stored and usage is settled.
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/BasicSpeechRequest'
                - $ref: '#/components/schemas/PremiumSpeechRequest'
              discriminator:
                propertyName: tier
      responses:
        '200':
          description: One-hour signed audio download
          headers: *successHeaders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeechResponse'
        default:
          $ref: '#/components/responses/Error'
  /v1/speech/jobs:
    post:
      summary: Queue a durable Basic speech job
      description: |
        Basic jobs are available in production. Premium remains a development and
        staging preview; production returns premium_speech_jobs_unavailable before
        allowance reservation, storage, or Workflow start. Synchronous Premium at
        POST /v1/speech is unchanged. Returns the existing job on an exact idempotent
        replay. VowelMarks Workflow owns an accepted Basic job, and losing the client
        event connection does not cancel it.
        In the current staging Premium experiment, provider streaming is disabled.
        The Workflow buffers and checkpoints completed sections, emits no audio events
        before completion, and publishes one finished WAV for the client job. Poll the
        job until terminal status instead of requiring live preview audio.
        Premium section planning targets about 150 seconds with an estimated 180-second
        ceiling; those estimates are neither guarantees nor hard audio-duration limits.
        Asynchronous execution changes the client wait contract; it does not relax
        input, provider-interaction, or output-size limits.
      operationId: createSpeechJob
      x-vowelmarks-availability:
        production: [basic]
        development: [basic, premium]
        staging: [basic, premium]
      parameters:
        - $ref: '#/components/parameters/RequiredIdempotencyKey'
      requestBody:
        required: true
        description: Serialized JSON must be at most 32 KiB (32,768 bytes). Both tier schemas remain published; Premium asynchronous creation is disabled in production.
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/BasicSpeechRequest'
                - $ref: '#/components/schemas/PremiumSpeechRequest'
              discriminator:
                propertyName: tier
      responses:
        '202':
          description: Job durably queued or returned by an exact idempotent replay
          headers: *successHeaders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeechJob'
        '503':
          description: Service unavailable or durable start uncertain. Production uses this response for valid Premium job requests; that Premium response is non-retryable and has no Retry-After header.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/RequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                premiumJobsDisabled:
                  value:
                    request_id: 00000000-0000-4000-8000-000000000042
                    error:
                      code: premium_speech_jobs_unavailable
                      message: Asynchronous Premium speech jobs are not available in this API environment.
                      category: service
                      action: Use the synchronous speech endpoint or submit a Basic speech job.
                      retryable: false
        default:
          $ref: '#/components/responses/Error'
  /v1/speech/jobs/{job_id}:
    parameters:
      - $ref: '#/components/parameters/SpeechJobId'
    get:
      summary: Read a speech job
      description: Available in production for Basic jobs. Premium jobs remain development/staging-only. A succeeded read issues a fresh signed audio URL.
      operationId: getSpeechJob
      x-vowelmarks-availability:
        production: [basic]
        development: [basic, premium]
        staging: [basic, premium]
      responses:
        '200':
          description: Current or terminal job state
          headers: *successHeaders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeechJob'
        default:
          $ref: '#/components/responses/Error'
  /v1/speech/jobs/{job_id}/events:
    parameters:
      - $ref: '#/components/parameters/SpeechJobId'
    get:
      summary: Follow ordered speech synthesis events
      description: |
        Available in production for Basic jobs. Premium events remain a development
        and staging preview. Server-Sent Events include status, audio, reset,
        complete, and error events plus heartbeat comments.
        Basic MP3 audio events are currently emitted only for fresh Kian or Leyla
        synthesis. Legacy voices, cache hits, and fallback completion may emit no
        audio events; clients must still await complete or poll terminal status.
        The current staging Premium experiment is buffered and emits status but no
        audio events before completion. Poll for the succeeded job and finished WAV.
        The pcm_s16le value remains the Premium preview format if provider streaming
        is enabled in a future development or staging environment.
        Stream EOF is not success; reconnect or poll unless a terminal event arrived.
      operationId: streamSpeechJobEvents
      x-vowelmarks-availability:
        production: [basic]
        development: [basic, premium]
        staging: [basic, premium]
      parameters:
        - name: Last-Event-ID
          in: header
          required: false
          description: Resume after this positive integer event ID.
          schema: { type: integer, minimum: 1 }
        - name: after
          in: query
          required: false
          description: Resume after this positive integer event ID. Takes precedence over Last-Event-ID.
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: Ordered event stream; disconnecting does not cancel the job
          headers:
            X-Request-Id: { $ref: '#/components/headers/RequestId' }
          content:
            text/event-stream:
              schema:
                type: string
              examples:
                running:
                  value: "id: 1\nevent: status\ndata: {\"job_id\":\"00000000-0000-4000-8000-000000000042\",\"status\":\"running\"}\n\n"
                basicAudio:
                  value: "id: 2\nevent: audio\ndata: {\"job_id\":\"00000000-0000-4000-8000-000000000042\",\"format\":\"mp3\",\"audio_base64\":\"SUQz…\"}\n\n"
        default:
          $ref: '#/components/responses/Error'
  /v1/speech/jobs/{job_id}/cancel:
    parameters:
      - $ref: '#/components/parameters/SpeechJobId'
    post:
      summary: Request best-effort speech job cancellation
      description: Available in production for Basic jobs. Premium jobs remain development/staging-only. Idempotent; a completed race remains succeeded and normally charged.
      operationId: cancelSpeechJob
      x-vowelmarks-availability:
        production: [basic]
        development: [basic, premium]
        staging: [basic, premium]
      responses:
        '200':
          description: Current job state after the cancellation request
          headers: *successHeaders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpeechJob'
        default:
          $ref: '#/components/responses/Error'
  /v1/usage:
    get:
      summary: Read organization-wide usage for the key environment
      operationId: usage
      responses:
        '200':
          description: Current billing-period usage
          headers: *successHeaders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UsageResponse'
        default:
          $ref: '#/components/responses/Error'
  /v1/developer/audio:
    parameters:
      - name: id
        in: query
        required: true
        schema: { type: string }
      - name: expires
        in: query
        required: true
        schema: { type: integer }
      - name: signature
        in: query
        required: true
        schema: { type: string }
      - name: If-None-Match
        in: header
        required: false
        description: Return 304 when the completed file still has this ETag.
        schema: { type: string }
    get:
      summary: Download completed speech audio
      description: |
        Use the complete signed URL returned by a speech response. Complete-file
        GET, one byte range, If-Range, and ETag validation are available in production.
      operationId: downloadDeveloperSpeechAudio
      x-vowelmarks-availability: production
      security: []
      parameters:
        - name: Range
          in: header
          required: false
          description: One byte range, including open-ended or suffix forms.
          schema: { type: string, pattern: '^bytes=[0-9]*-[0-9]*$' }
        - name: If-Range
          in: header
          required: false
          description: Apply Range only while the file still has this ETag.
          schema: { type: string }
      responses:
        '200':
          description: Complete WAV or MP3 file
          headers: &audioHeaders
            Accept-Ranges: { schema: { const: bytes } }
            Content-Length: { schema: { type: integer, minimum: 1 } }
            Content-Type: { schema: { enum: [audio/wav, audio/mpeg] } }
            ETag: { schema: { type: string } }
          content:
            audio/wav: { schema: { type: string, format: binary } }
            audio/mpeg: { schema: { type: string, format: binary } }
        '206':
          description: Requested byte range
          headers:
            Accept-Ranges: { schema: { const: bytes } }
            Content-Length: { schema: { type: integer, minimum: 1 } }
            Content-Type: { schema: { enum: [audio/wav, audio/mpeg] } }
            ETag: { schema: { type: string } }
            Content-Range: { schema: { type: string } }
          content:
            audio/wav: { schema: { type: string, format: binary } }
            audio/mpeg: { schema: { type: string, format: binary } }
        '304': { description: ETag still matches }
        '403': { description: Signed URL expired or signature invalid }
        '404': { description: Audio object not found }
        '416': { description: Requested byte range is not satisfiable }
    head:
      summary: Inspect completed speech audio
      description: Available in production. Returns file metadata without transferring the audio body.
      operationId: inspectDeveloperSpeechAudio
      x-vowelmarks-availability: production
      security: []
      responses:
        '200':
          description: Audio metadata
          headers: *audioHeaders
        '304': { description: ETag still matches }
        '403': { description: Signed URL expired or signature invalid }
        '404': { description: Audio object not found }
components:
  headers:
    RequestId:
      description: Stable identifier for this request or idempotent replay.
      schema: { type: string, format: uuid }
    RateLimitLimit:
      description: Organization-wide requests allowed in the current minute.
      schema: { type: integer, minimum: 1 }
    RateLimitRemaining:
      description: Organization-wide requests remaining in the current minute.
      schema: { type: integer, minimum: 0 }
    RateLimitReset:
      description: Seconds until the organization-wide rate window resets.
      schema: { type: integer, minimum: 0 }
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A server-side vm_test_ or vm_live_ API key.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Reuse for the exact same endpoint and payload for up to 24 hours.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: '^[ -~]+$'
    RequiredIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: Reuse for the exact same speech-job payload for up to 24 hours.
      schema:
        type: string
        minLength: 8
        maxLength: 128
        pattern: '^[ -~]+$'
    SpeechJobId:
      name: job_id
      in: path
      required: true
      schema:
        type: string
        format: uuid
  responses:
    Error:
      description: VowelMarks API error
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        Retry-After:
          description: Seconds to wait when the error is retryable and a delay is known.
          schema: { type: integer, minimum: 1 }
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    PersianText:
      type: string
      minLength: 1
      maxLength: 6000
      description: Exact submitted text, limited by Unicode code points and containing a Persian-script letter.
    TextRequest:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        text:
          $ref: '#/components/schemas/PersianText'
    DiacritizeRequest:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        text:
          $ref: '#/components/schemas/PersianText'
        include_ezafe:
          type: boolean
          default: true
    PinglishRequest:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        text:
          $ref: '#/components/schemas/PersianText'
        style:
          type: string
          enum: [readable, systematic]
          default: readable
        vowel_notation:
          type: string
          enum: [double_a, macron]
          default: double_a
    BasicSpeechRequest:
      type: object
      additionalProperties: false
      required: [text, tier, voice]
      properties:
        text:
          $ref: '#/components/schemas/PersianText'
        tier:
          const: basic
        voice:
          enum: [kian, leyla, ava, mana, amir]
        pace:
          type: number
          minimum: 0.75
          maximum: 1.25
          default: 1
    PremiumSpeechRequest:
      type: object
      additionalProperties: false
      required: [text, tier, voice]
      properties:
        text:
          $ref: '#/components/schemas/PersianText'
        tier:
          const: premium
        voice:
          type: string
          enum: [Zephyr, Puck, Charon, Kore, Fenrir, Leda, Orus, Aoede, Callirrhoe, Autonoe, Enceladus, Iapetus, Umbriel, Algieba, Despina, Erinome, Algenib, Rasalgethi, Laomedeia, Achernar, Alnilam, Schedar, Gacrux, Pulcherrima, Achird, Zubenelgenubi, Vindemiatrix, Sadachbia, Sadaltager, Sulafat]
        pace:
          type: number
          minimum: 0.75
          maximum: 1.25
          default: 1
        style:
          type: string
          enum: [neutral, warm, news, conversational, instructional]
          default: neutral
        expressiveness:
          type: string
          enum: [calm, natural, lively]
          default: natural
        regional_mode:
          type: string
          enum: [standard]
          default: standard
        director_notes:
          type: [string, 'null']
          maxLength: 600
          default: null
    BaseSuccess:
      type: object
      required: [request_id, engine_version, usage]
      properties:
        request_id:
          type: string
          format: uuid
        engine_version:
          type: string
        usage:
          oneOf:
            - $ref: '#/components/schemas/TextUsage'
            - $ref: '#/components/schemas/BasicSpeechUsage'
            - $ref: '#/components/schemas/PremiumSpeechUsage'
          discriminator:
            propertyName: meter
    TextUsage:
      type: object
      additionalProperties: false
      required: [meter, input_code_points, multiplier, units, additional_units, cache_hit]
      properties:
        meter: { const: text }
        input_code_points: { type: integer, minimum: 1, maximum: 6000 }
        multiplier: { type: integer, enum: [1, 2] }
        units: { type: integer, minimum: 1, maximum: 12000 }
        additional_units: { type: integer, minimum: 0 }
        cache_hit: { type: boolean }
    BasicSpeechUsage:
      type: object
      additionalProperties: false
      required: [meter, seconds, grace_seconds, cache_hit]
      properties:
        meter: { const: basic_speech }
        seconds: { type: number, exclusiveMinimum: 0 }
        grace_seconds: { type: number, minimum: 0 }
        cache_hit: { type: boolean }
    PremiumSpeechUsage:
      type: object
      additionalProperties: false
      required: [meter, seconds, grace_seconds, cache_hit]
      properties:
        meter: { const: premium_speech }
        seconds: { type: number, exclusiveMinimum: 0 }
        grace_seconds: { type: number, const: 0 }
        cache_hit: { type: boolean }
    DiacritizeResponse:
      allOf:
        - $ref: '#/components/schemas/BaseSuccess'
        - type: object
          required: [original, marked, operations, usage]
          properties:
            usage: { $ref: '#/components/schemas/TextUsage' }
            original: { type: string }
            marked: { type: string }
            operations:
              type: array
              items:
                type: object
                additionalProperties: false
                required: [source_index, mark, type]
                properties:
                  source_index: { type: integer, minimum: 0 }
                  mark: { type: string }
                  type: { enum: [reading, ezafe] }
    PinglishResponse:
      allOf:
        - $ref: '#/components/schemas/BaseSuccess'
        - type: object
          required: [original, pinglish, style, vowel_notation, alignment, warnings, usage]
          properties:
            usage: { $ref: '#/components/schemas/TextUsage' }
            original: { type: string }
            pinglish: { type: string }
            style: { enum: [readable, systematic] }
            vowel_notation: { enum: [double_a, macron] }
            alignment:
              type: array
              items:
                type: object
                required: [source_start, source_end, source, pinglish]
                properties:
                  source_start: { type: integer, minimum: 0 }
                  source_end: { type: integer, minimum: 1 }
                  source: { type: string }
                  pinglish: { type: string }
            warnings:
              type: array
              items: { type: string }
    TranslationResponse:
      allOf:
        - $ref: '#/components/schemas/BaseSuccess'
        - type: object
          required: [original, translation, source_language, target_language, usage]
          properties:
            usage: { $ref: '#/components/schemas/TextUsage' }
            original: { type: string }
            translation: { type: string }
            source_language: { const: fa }
            target_language: { const: en }
    SpeechResponse:
      allOf:
        - $ref: '#/components/schemas/BaseSuccess'
        - type: object
          required: [original, audio_url, audio_expires_at, mime_type, duration_seconds, tier, voice, delivery]
          properties:
            original: { type: string }
            audio_url: { type: string, format: uri }
            audio_expires_at: { type: string, format: date-time }
            mime_type: { enum: [audio/wav, audio/mpeg] }
            duration_seconds: { type: number, exclusiveMinimum: 0 }
            tier: { enum: [basic, premium] }
            voice: { type: string }
            requested_voice:
              type: string
              enum: [kian, leyla, ava, mana, amir]
            fallback:
              $ref: '#/components/schemas/BasicSpeechFallback'
            delivery:
              oneOf:
                - $ref: '#/components/schemas/BasicSpeechDelivery'
                - $ref: '#/components/schemas/PremiumSpeechDelivery'
    BasicSpeechDelivery:
      type: object
      additionalProperties: false
      required: [pace]
      properties:
        pace: { type: number, minimum: 0.75, maximum: 1.25 }
    BasicSpeechFallback:
      type: object
      additionalProperties: false
      required: [from, to, reason]
      properties:
        from: { enum: [kian, leyla] }
        to: { const: ava }
        reason: { enum: [capacity, timeout, upstream, empty_audio, transcode_failure] }
    PremiumSpeechDelivery:
      type: object
      additionalProperties: false
      required: [pace, style, expressiveness, regional_mode, director_notes]
      properties:
        pace: { type: number, minimum: 0.75, maximum: 1.25 }
        style: { enum: [neutral, warm, news, conversational, instructional] }
        expressiveness: { enum: [calm, natural, lively] }
        regional_mode: { enum: [standard] }
        director_notes: { type: [string, 'null'], maxLength: 600 }
    SpeechJob:
      type: object
      additionalProperties: false
      required: [id, request_id, status, tier, stream_format, created_at, started_at, finished_at, status_url, stream_url, cancel_url]
      properties:
        id: { type: string, format: uuid }
        request_id: { type: string, format: uuid }
        status: { enum: [queued, running, succeeded, failed, cancelled] }
        tier: { enum: [basic, premium] }
        stream_format:
          description: Basic uses mp3. Premium reserves signed 16-bit little-endian PCM at 24 kHz mono for preview-enabled development/staging environments; buffered jobs may emit no audio events.
          enum: [mp3, pcm_s16le]
        created_at: { type: string, format: date-time }
        started_at: { type: [string, 'null'], format: date-time }
        finished_at: { type: [string, 'null'], format: date-time }
        status_url: { type: string, format: uri }
        stream_url: { type: string, format: uri }
        cancel_url: { type: string, format: uri }
        result:
          $ref: '#/components/schemas/SpeechJobResult'
        audio_url: { type: string, format: uri }
        audio_expires_at: { type: string, format: date-time }
        error:
          $ref: '#/components/schemas/ErrorDetail'
      description: |
        Succeeded jobs include result, audio_url, and audio_expires_at. Failed
        jobs include error. Other states omit those terminal fields.
    SpeechJobResult:
      type: object
      additionalProperties: false
      required: [original, mime_type, duration_seconds, tier, voice, delivery, usage]
      properties:
        original: { type: string }
        mime_type: { enum: [audio/wav, audio/mpeg] }
        duration_seconds: { type: number, exclusiveMinimum: 0 }
        tier: { enum: [basic, premium] }
        voice: { type: string }
        requested_voice:
          type: string
          enum: [kian, leyla, ava, mana, amir]
        sections: { type: integer, minimum: 1 }
        fallback:
          $ref: '#/components/schemas/BasicSpeechFallback'
        delivery:
          oneOf:
            - $ref: '#/components/schemas/BasicSpeechDelivery'
            - $ref: '#/components/schemas/PremiumSpeechDelivery'
        usage:
          oneOf:
            - $ref: '#/components/schemas/BasicSpeechUsage'
            - $ref: '#/components/schemas/PremiumSpeechUsage'
    ErrorDetail:
      type: object
      additionalProperties: false
      required: [code, message, category, action, retryable]
      properties:
        code: { type: string }
        message: { type: string }
        category: { enum: [authentication, authorization, validation, conflict, rate_limit, quota, service, provider, internal] }
        action: { type: string }
        retryable: { type: boolean }
        details: { type: object, additionalProperties: true }
    UsageResponse:
      type: object
      additionalProperties: false
      required: [request_id, engine_version, environment, plan, status, billing_period, text, basic_speech, premium_speech, organization_rpm]
      properties:
        request_id: { type: string, format: uuid }
        engine_version: { type: string }
        environment: { enum: [test, live] }
        plan: { enum: [sandbox, builder, growth, scale] }
        status: { enum: [pending_payment, active, canceling, past_due, disabled] }
        billing_period:
          type: object
          additionalProperties: false
          required: [starts_at, ends_at]
          properties:
            starts_at: { type: string, format: date-time }
            ends_at: { type: string, format: date-time }
        text:
          type: object
          additionalProperties: false
          required: [included_units, used_units, reserved_units, additional_usage_approved, additional_unit_ceiling, additional_units]
          properties:
            included_units: { type: integer, minimum: 0 }
            used_units: { type: integer, minimum: 0 }
            reserved_units: { type: integer, minimum: 0 }
            additional_usage_approved: { type: boolean }
            additional_unit_ceiling: { type: integer, minimum: 0 }
            additional_units: { type: integer, minimum: 0 }
        basic_speech:
          type: object
          additionalProperties: false
          required: [included_seconds, used_seconds, reserved_seconds, grace_seconds]
          properties:
            included_seconds: { type: number, minimum: 0 }
            used_seconds: { type: number, minimum: 0 }
            reserved_seconds: { type: number, minimum: 0 }
            grace_seconds: { type: number, minimum: 0 }
        premium_speech:
          type: object
          additionalProperties: false
          required: [included_seconds, used_seconds, reserved_seconds]
          properties:
            included_seconds: { type: number, minimum: 0 }
            used_seconds: { type: number, minimum: 0 }
            reserved_seconds: { type: number, minimum: 0 }
        organization_rpm: { type: integer, minimum: 1 }
    ErrorResponse:
      type: object
      additionalProperties: false
      required: [request_id, error]
      properties:
        request_id: { type: string }
        error:
          $ref: '#/components/schemas/ErrorDetail'
