openapi: 3.1.0
info:
  title: Tanvo public API
  version: "1.0"
  license: { name: Proprietary }
  description: |
    Generate images and video. Identity is an API key (`Authorization: Bearer sk_…`) or,
    for the free tier, an `x-anon-id` header the client makes up. Submit a generation,
    then poll it until `status` is SUCCEEDED or FAILED.
servers:
  - url: https://tanvo.ai/api/v1
security:
  - apiKey: []
  - anonId: []
components:
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      description: "sk_ followed by 40 hex characters, from Settings → API keys"
    anonId:
      type: apiKey
      in: header
      name: x-anon-id
      description: "Any client-chosen id, 8–80 chars of [A-Za-z0-9._-]. Opens a 5-credit free wallet."
  schemas:
    Error:
      type: object
      required: [error, message]
      properties:
        error:
          type: string
          enum: [invalid, rejected, unauthorized, invalid_api_key, unknown_anon_id, credits, allowance, needs_api_key, not_found, busy, too_large, anon_ip_daily, rate_limited, limit, provider, storage_unavailable, moderation_unavailable, paused, trial_closed, server_error]
        message: { type: string }
        issues:
          type: array
          items: { type: string }
    App:
      type: object
      properties:
        slug: { type: string }
        kind: { type: string, enum: [image, video, music] }
        name: { type: string }
        description: { type: string }
        category: { type: string, nullable: true }
        tags: { type: array, items: { type: string } }
        season: { type: string, nullable: true }
        model: { type: string, description: Default model id }
        models: { type: array, items: { type: string } }
        spec: { type: string }
        url: { type: string, format: uri }
        example: { $ref: "#/components/schemas/ExampleMedia" }
        inputs:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              label: { type: string }
              hint: { type: string }
              accept: { type: string }
              min: { type: integer }
              max: { type: integer }
        looks:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              name: { type: string }
              prompt: { type: string }
              model: { type: string }
              options: { type: object, additionalProperties: true }
              example: { $ref: "#/components/schemas/ExampleMedia" }
              url: { type: string, format: uri, description: Opens the app on this look (?look=<id>) }
    ExampleMedia:
      type: object
      nullable: true
      properties:
        url: { type: string, format: uri, description: Image, or the poster of a video }
        kind: { type: string, enum: [image, video] }
        video: { type: string, format: uri }
    Model:
      type: object
      properties:
        id: { type: string }
        kind: { type: string, enum: [image, video, music] }
        name: { type: string }
        vendor: { type: string }
        description: { type: string }
        free_tier: { type: boolean }
        inputs:
          type: array
          items: { type: string, enum: [text, image, audio] }
        options:
          type: object
          description: Allowed values per option; null means the model does not take that option.
          properties:
            aspect: { type: [array, "null"], items: { type: string } }
            tier: { type: [array, "null"], items: { type: string } }
            resolution: { type: array, items: { type: string } }
            duration: { type: [array, "null"], items: { type: integer } }
            format: { type: [array, "null"], items: { type: string } }
            audio: { type: [array, "null"], items: { type: boolean } }
        reference_images_max: { type: integer }
        defaults: { $ref: "#/components/schemas/Options" }
        credits_at_defaults: { type: integer }
        prompt_max_chars: { type: integer }
        music:
          type: object
          description: Only on music models. The mode is options.tier; the prompt limit depends on it.
          properties:
            modes: { type: object, additionalProperties: { type: string } }
            prompt_max_chars_by_mode: { type: object, additionalProperties: { type: integer } }
            style_max_chars: { type: integer }
            title_max_chars: { type: integer }
            vocal: { type: array, items: { type: string, enum: [m, f] } }
            songs_per_run: { type: integer }
    Options:
      type: object
      required: [resolution]
      properties:
        aspect: { type: string, example: "16:9" }
        tier: { type: string, example: Lite }
        resolution: { type: string, example: 1K }
        duration: { type: integer, example: 5 }
        format: { type: string, enum: [PNG, JPG, WEBP] }
        audio: { type: boolean }
        style: { type: string, maxLength: 1000, description: "Music, Describe and Lyrics modes: the sound, e.g. acoustic folk, warm female vocal." }
        title: { type: string, maxLength: 80, description: Music song title (Lyrics and Instrumental modes). }
        vocal: { type: string, enum: [m, f], description: Music Lyrics mode only; a preferred voice. }
    GenerationRequest:
      type: object
      required: [kind, model, prompt, options]
      properties:
        kind: { type: string, enum: [image, video, music] }
        model: { type: string }
        prompt: { type: string, minLength: 1 }
        negativePrompt: { type: string, maxLength: 2000 }
        options: { $ref: "#/components/schemas/Options" }
        imageUrls:
          type: array
          maxItems: 14
          items: { type: string, format: uri }
          description: Public https image URLs or URLs returned by POST /uploads.
        endImageUrl: { type: string, format: uri }
        webhookUrl:
          type: string
          format: uri
          maxLength: 500
          description: "https URL on a public host. When the run finishes we POST the Generation object here (signed, Standard Webhooks; see docs/api.md). Takes precedence over the account's endpoints."
        audioUrl: { type: string, format: uri, description: "Driving audio for talking-avatar models (inputs contains audio); upload it through POST /uploads first." }
        requestKey:
          type: string
          maxLength: 120
          description: Idempotency key. Resubmitting with the same key returns the first generation.
    Generation:
      type: object
      properties:
        id: { type: string }
        kind: { type: string, enum: [image, video, music] }
        model: { type: string }
        prompt: { type: string }
        options: { $ref: "#/components/schemas/Options" }
        cost: { type: integer, description: Credits charged; 0 for a free run. }
        status: { type: string, enum: [QUEUED, PENDING, PROCESSING, SUCCEEDED, FAILED, CANCELED] }
        error: { type: [string, "null"] }
        createdAt: { type: integer, description: Unix milliseconds }
        outputs:
          type: array
          items:
            type: object
            properties:
              url: { type: string, format: uri }
              mime: { type: string }
              cover: { type: string, format: uri, description: Music only. Cover art for this song. }
              title: { type: string, description: Music only. }
              lyrics: { type: string, description: Music only. The lyrics as sung. }
              seconds: { type: integer, description: Music only. Length of the song. }
        refunded: { type: integer, description: "Webhook body of a failed run only: credits returned." }
  responses:
    Error:
      description: Error envelope
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
paths:
  /models:
    get:
      operationId: listModels
      summary: List models
      security: []
      responses:
        "200":
          description: The catalogue
          content:
            application/json:
              schema:
                type: object
                properties:
                  models:
                    type: array
                    items: { $ref: "#/components/schemas/Model" }
  /apps:
    get:
      operationId: listApps
      summary: List ready-made apps and their looks
      description: Every live app with its upload slots and preset looks. Each look carries its prompt, model, options, a real example output and a link that opens the app on that look. Cached for up to an hour.
      security: []
      responses:
        "200":
          description: The app catalogue
          content:
            application/json:
              schema:
                type: object
                properties:
                  apps:
                    type: array
                    items: { $ref: "#/components/schemas/App" }
  /me:
    get:
      operationId: getMe
      summary: Current identity and balance
      responses:
        "200":
          description: The caller
          content:
            application/json:
              schema:
                type: object
                properties:
                  tier: { type: string, enum: [paid, free, anonymous] }
                  email: { type: string }
                  credits: { type: integer }
                  watermarked: { type: boolean }
                  concurrency: { type: integer }
        "401": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
  /generations:
    post:
      operationId: createGeneration
      summary: Submit a generation
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/GenerationRequest" }
      responses:
        "202":
          description: Accepted; poll the generation until it settles
          content:
            application/json:
              schema:
                type: object
                properties:
                  generation: { $ref: "#/components/schemas/Generation" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/Error" }
        "502": { $ref: "#/components/responses/Error" }
    get:
      operationId: listGenerations
      summary: List recent generations
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
        - { name: before, in: query, schema: { type: integer }, description: createdAt cursor from next_before }
      responses:
        "200":
          description: One page, newest first
          content:
            application/json:
              schema:
                type: object
                properties:
                  generations:
                    type: array
                    items: { $ref: "#/components/schemas/Generation" }
                  next_before: { type: [integer, "null"] }
  /generations/{id}:
    get:
      operationId: getGeneration
      summary: Get one generation (settles it with the provider)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        "200":
          description: The generation
          content:
            application/json:
              schema:
                type: object
                properties:
                  generation: { $ref: "#/components/schemas/Generation" }
        "401": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
  /uploads:
    post:
      operationId: createUpload
      summary: Request an upload slot for a local file
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mime, bytes]
              properties:
                mime: { type: string, example: image/png }
                bytes: { type: integer }
      responses:
        "200":
          description: Where to PUT the file
          content:
            application/json:
              schema:
                type: object
                properties:
                  upload_url: { type: string, format: uri, description: PUT the bytes here with the same Content-Type within expires_in seconds }
                  url: { type: string, format: uri, description: Pass this in imageUrls }
                  expires_in: { type: integer }
        "400": { $ref: "#/components/responses/Error" }
        "413": { $ref: "#/components/responses/Error" }
        "503": { $ref: "#/components/responses/Error" }
