{
  "openapi": "3.1.0",
  "info": {
    "title": "VoiceHue text to speech API",
    "version": "1.0.0",
    "summary": "OpenAI-compatible text to speech with a free plan.",
    "description": "Turns text into audio: MP3 by default, or WAV. The request and error format follow the OpenAI speech endpoint, so an OpenAI client works when its base URL is https://voicehue.com/v1 and its key is a VoiceHue key. Create keys at https://voicehue.com/dashboard/keys. Free plan per key: 10,000 characters a month, 2,000 a day, 10 requests a minute, up to 1,200 characters per request, standard voices. A remote MCP server for AI assistants is available at https://voicehue.com/mcp.",
    "termsOfService": "https://voicehue.com/terms",
    "contact": {
      "name": "VoiceHue",
      "url": "https://voicehue.com/contact"
    }
  },
  "servers": [
    {
      "url": "https://voicehue.com"
    }
  ],
  "externalDocs": {
    "description": "Developer documentation, voices, limits and error codes",
    "url": "https://voicehue.com/developers"
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Speech"
    },
    {
      "name": "Voices"
    },
    {
      "name": "MCP"
    }
  ],
  "paths": {
    "/v1/audio/speech": {
      "post": {
        "operationId": "createSpeech",
        "tags": [
          "Speech"
        ],
        "summary": "Create speech",
        "description": "Generates audio from text. The response body is the audio file. Send an Idempotency-Key header to retry safely: a repeated key answers 409 duplicate_request.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 128
            },
            "description": "Optional key that makes a retry safe."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SpeechRequest"
              },
              "example": {
                "model": "voicehue-tts",
                "input": "Hello from VoiceHue.",
                "voice": "VOICE_ID",
                "response_format": "mp3"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The audio file. MP3 (audio/mpeg) unless response_format is wav.",
            "headers": {
              "X-VoiceHue-Request-Id": {
                "description": "Quote this id when you contact support.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "audio/wav": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`: The body is not valid JSON, has an unknown field, or a field has the wrong type. `invalid_input`: The input contains characters that are not allowed (control characters). `invalid_voice`: The voice value is malformed. `invalid_speed`: speed is outside 0.5 to 2. `unsupported_format`: The voice does not offer the requested response_format.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "`invalid_api_key`: The key is missing, wrong, revoked or expired. The answer is identical in every case.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "`premium_trial_exhausted`: A premium voice: today's free premium trial is used up and the account has no credits. The error body carries trial and credits. `insufficient_credits`: A premium voice: the account does not have enough credits for this input. The error body carries trial and credits (balance and needed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "`premium_not_allowed`: A premium voice was requested but the plan of this key does not include premium voices. `premium_sign_in_required`: A premium voice needs a signed-in account; the account behind the key is not one. `account_suspended`: The account is not permitted to use the service.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`voice_not_found`: The voice does not exist or is not part of your plan.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "`invalid_request`: The method is not POST.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "`duplicate_request`: The same Idempotency-Key was already processed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "`input_too_long`: The input is longer than 1,200 characters. `audio_too_large`: The generated audio is too large to convert to MP3. Send a shorter input. `request_too_large`: The request body is larger than 16 KB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`voice_daily_limit`: This voice has a daily character limit and this key reached it. The error body carries limit and resets_at (00:00 UTC). `rate_limit_exceeded`: More than 10 requests in a minute for this key, or too many attempts from one address. Wait and retry. `insufficient_quota`: A key, account-wide, or shared-network free allowance has been used up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": {
                    "message": "The usage limit for this key has been reached.",
                    "type": "insufficient_quota",
                    "code": "insufficient_quota"
                  }
                }
              }
            }
          },
          "502": {
            "description": "`service_unavailable`: The service could not complete the request. Retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`service_unavailable`: The API is switched off or temporarily unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/voices": {
      "get": {
        "operationId": "listVoices",
        "tags": [
          "Voices"
        ],
        "security": [],
        "summary": "List voices",
        "description": "Lists the voices an API key can use. No key needed. Send a voice's id as voice when creating speech. The list follows the site and is cached for up to five minutes.",
        "parameters": [
          {
            "name": "language",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional filter, for example en or zh. Matches the start of the language code."
          }
        ],
        "responses": {
          "200": {
            "description": "The voices. data is empty when none match the filter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VoiceList"
                },
                "example": {
                  "object": "list",
                  "data": [
                    {
                      "id": "vh-en-sky",
                      "object": "voice",
                      "name": "Sky",
                      "language": "en-US",
                      "gender": "female",
                      "tier": "standard"
                    }
                  ]
                }
              }
            }
          },
          "503": {
            "description": "`service_unavailable`: The voice list is not available right now.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "tags": [
          "MCP"
        ],
        "summary": "Model Context Protocol endpoint",
        "description": "Remote MCP server (Streamable HTTP, stateless, JSON-RPC 2.0 over POST) with the tools text_to_speech and list_voices. Same bearer keys as the speech endpoint. Meant for MCP clients such as Claude, Cursor and other AI assistants; see https://voicehue.com/developers#ai-tools.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "jsonrpc",
                  "method"
                ],
                "properties": {
                  "jsonrpc": {
                    "const": "2.0"
                  },
                  "id": {
                    "type": [
                      "string",
                      "integer"
                    ]
                  },
                  "method": {
                    "type": "string",
                    "examples": [
                      "tools/list",
                      "tools/call"
                    ]
                  },
                  "params": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "202": {
            "description": "Notification accepted."
          },
          "401": {
            "description": "Missing or invalid API key (tools/call)."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "vh_live_",
        "description": "A VoiceHue API key (vh_live_...) from https://voicehue.com/dashboard/keys, sent as Authorization: Bearer <key>."
      }
    },
    "schemas": {
      "SpeechRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "input",
          "voice"
        ],
        "properties": {
          "input": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1200,
            "description": "The text to speak, up to 1,200 characters."
          },
          "voice": {
            "type": "string",
            "description": "A voice id from the list below. The ids come from GET /v1/voices.",
            "examples": [
              "VOICE_ID"
            ]
          },
          "model": {
            "type": "string",
            "default": "voicehue-tts",
            "description": "Accepted for compatibility and ignored. Use \"voicehue-tts\"."
          },
          "response_format": {
            "type": "string",
            "enum": [
              "mp3",
              "wav"
            ],
            "default": "mp3",
            "description": "mp3 (default) or wav. MP3 is returned whenever you leave it out or send mp3. A voice that cannot produce wav answers unsupported_format."
          },
          "speed": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 2,
            "default": 1,
            "description": "Speaking speed from 0.5 to 2. Default 1."
          }
        }
      },
      "VoiceList": {
        "type": "object",
        "required": [
          "object",
          "data"
        ],
        "properties": {
          "object": {
            "const": "list"
          },
          "data": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "object",
                "name",
                "language",
                "gender",
                "tier"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Send this as voice."
                },
                "object": {
                  "const": "voice"
                },
                "name": {
                  "type": "string"
                },
                "language": {
                  "type": "string",
                  "examples": [
                    "en-US"
                  ]
                },
                "gender": {
                  "type": "string",
                  "enum": [
                    "female",
                    "male",
                    "neutral"
                  ]
                },
                "tier": {
                  "type": "string",
                  "enum": [
                    "standard"
                  ]
                }
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "message",
              "type",
              "code"
            ],
            "properties": {
              "message": {
                "type": "string"
              },
              "type": {
                "type": "string",
                "enum": [
                  "invalid_request_error",
                  "authentication_error",
                  "insufficient_quota",
                  "rate_limit_error",
                  "server_error"
                ]
              },
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "invalid_input",
                  "invalid_voice",
                  "invalid_speed",
                  "unsupported_format",
                  "invalid_api_key",
                  "voice_daily_limit",
                  "premium_trial_exhausted",
                  "insufficient_credits",
                  "premium_not_allowed",
                  "premium_sign_in_required",
                  "account_suspended",
                  "voice_not_found",
                  "duplicate_request",
                  "input_too_long",
                  "audio_too_large",
                  "request_too_large",
                  "rate_limit_exceeded",
                  "insufficient_quota",
                  "service_unavailable"
                ]
              }
            }
          }
        }
      }
    }
  }
}