FSMB API

Text to speech and voice cloning, over HTTP

Send text, get an MP3. Clone a voice from a short recording. Every call is a plain HTTPS request with your API key.

Base URL
https://fsmb616.com/v1
Authentication
Header xi-api-key: YOUR_API_KEY
Responses
JSON. Audio is delivered as MP3.

Your first audio file

Speech is generated in the background, so it takes two calls: submit the text, then collect the result.

  1. Submit the text

    The call returns straight away with a task_id.

    Request
    curl -X POST "https://fsmb616.com/v1/text-to-speech/" \
      -H "xi-api-key: YOUR_API_KEY" \
      -F "text=Hello from FSMB" \
      -F "voice_id=minimax_209533299589198" \
      -F "speed=1"
    Response
    { "success": true, "task_id": "7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10" }
  2. Collect the result

    Ask for the task every few seconds until status is done, then download audio_url. Or pass a receive_url and we will call you when it is ready (see Webhooks).

    Request
    curl "https://fsmb616.com/v1/tasks/?id=7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10" \
      -H "xi-api-key: YOUR_API_KEY"
    Response once finished
    {
      "success": true,
      "task_id": "7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10",
      "status": "done",
      "characters": 17,
      "audio_url": "https://fsmb616.com/v1/files/7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10.mp3"
    }

Authentication

Send your API key in the xi-api-key header on every request. Each key has its own character quota (see Limits and billing).

Keep your key on your server Never put it in browser JavaScript or a mobile app. These endpoints do not send CORS headers, so call them from your backend.

A missing or wrong key returns 401. A key we have disabled returns 403.

Text to speech

POST/v1/text-to-speech/

Starts a speech task. Send the fields as multipart/form-data or application/x-www-form-urlencoded. JSON request bodies are not supported.

FieldDescription
textrequiredThe text to speak. Every character counts against your quota.
voice_idrequiredA voice from the voice list, or a voice you cloned. Example: minimax_209533299589198.
speedoptionalSpeaking speed from 0.5 to 1.5. Default 1.
receive_urloptionalAn HTTPS address (port 443) that we call once when the task finishes. See Webhooks.
curl -X POST "https://fsmb616.com/v1/text-to-speech/" \
  -H "xi-api-key: YOUR_API_KEY" \
  -F "text=Hello from FSMB" \
  -F "voice_id=minimax_209533299589198" \
  -F "speed=1" \
  -F "receive_url=https://your-site.com/fsmb-webhook"
import requests

r = requests.post(
    "https://fsmb616.com/v1/text-to-speech/",
    headers={"xi-api-key": "YOUR_API_KEY"},
    data={
        "text": "Hello from FSMB",
        "voice_id": "minimax_209533299589198",
        "speed": "1",
        "receive_url": "https://your-site.com/fsmb-webhook",  # optional
    },
    timeout=30,
)
print(r.status_code, r.json())
const form = new FormData();
form.append("text", "Hello from FSMB");
form.append("voice_id", "minimax_209533299589198");
form.append("speed", "1");
form.append("receive_url", "https://your-site.com/fsmb-webhook"); // optional

const res = await fetch("https://fsmb616.com/v1/text-to-speech/", {
  method: "POST",
  headers: { "xi-api-key": "YOUR_API_KEY" },
  body: form,
});
console.log(res.status, await res.json());
<?php
$ch = curl_init("https://fsmb616.com/v1/text-to-speech/");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => [
        "text"        => "Hello from FSMB",
        "voice_id"    => "minimax_209533299589198",
        "speed"       => "1",
        "receive_url" => "https://your-site.com/fsmb-webhook", // optional
    ],
    CURLOPT_HTTPHEADER     => ["xi-api-key: YOUR_API_KEY"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
echo curl_exec($ch);
curl_close($ch);

On success you get {"success": true, "task_id": "..."} and the characters are deducted from your quota. Short texts usually finish within 10 to 30 seconds; longer texts take proportionally longer.

If the request is rejected (a 4xx status), nothing is deducted. See Errors.

Check a task

GET/v1/tasks/?id=TASK_ID

Returns the current state of a task you created. A task is visible only to the key that created it.

FieldDescription
statusprocessing, done or failed.
charactersCharacters this task used.
audio_urlOnly when status is done. The MP3 address, see Download audio.
curl "https://fsmb616.com/v1/tasks/?id=TASK_ID" \
  -H "xi-api-key: YOUR_API_KEY"
import requests

s = requests.get(
    "https://fsmb616.com/v1/tasks/",
    headers={"xi-api-key": "YOUR_API_KEY"},
    params={"id": "TASK_ID"},
    timeout=30,
).json()
print(s["status"], s.get("audio_url"))
const res = await fetch("https://fsmb616.com/v1/tasks/?id=TASK_ID", {
  headers: { "xi-api-key": "YOUR_API_KEY" },
});
const s = await res.json();
console.log(s.status, s.audio_url);
<?php
$ch = curl_init("https://fsmb616.com/v1/tasks/?id=" . urlencode("TASK_ID"));
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => ["xi-api-key: YOUR_API_KEY"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$s = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $s["status"] ?? "unknown";

Poll every 3 to 5 seconds and stop after a few minutes. Task records are kept for at least 14 days.

Webhooks

If you send receive_url with a speech request, we make one POST to that address when the task ends, with a JSON body:

Body we send when a task is done
{
  "id": "7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10",
  "task_id": "7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10",
  "status": "done",
  "characters": 17,
  "progress": 100,
  "audio_url": "https://fsmb616.com/v1/files/7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10.mp3"
}
Body we send when a task fails
{
  "id": "7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10",
  "task_id": "7f3c2a1e-5b4d-4c8a-9e21-3d6b0a9c4f10",
  "status": "failed",
  "characters": 17
}

What to expect

  • The address must be HTTPS on port 443 and resolve to a public server. Private or internal addresses are never called, and a receive_url that breaks these rules is rejected with invalid_receive_url.
  • We try once, wait up to 10 seconds, and do not follow redirects. There are no retries, so also keep a polling fallback.
  • Answer with any 2xx status and keep the handler fast.
  • The call is not signed. Treat it as a nudge: confirm the result with GET /v1/tasks/ and your key before you act on it.

A minimal receiver that does exactly that:

from flask import Flask, request
import requests

app = Flask(__name__)
API = "https://fsmb616.com/v1"
HEADERS = {"xi-api-key": "YOUR_API_KEY"}

@app.post("/fsmb-webhook")
def fsmb_webhook():
    task_id = (request.get_json(silent=True) or {}).get("task_id", "")
    # Do not trust the payload on its own: confirm the result with the API.
    s = requests.get(f"{API}/tasks/", headers=HEADERS, params={"id": task_id}, timeout=30).json()
    if s.get("status") == "done":
        print("audio ready:", s["audio_url"])   # download and store it here
    elif s.get("status") == "failed":
        print("task failed:", task_id)
    return "", 204
const express = require("express");
const app = express();
app.use(express.json());

app.post("/fsmb-webhook", async (req, res) => {
  const taskId = (req.body && req.body.task_id) || "";
  res.sendStatus(204); // answer quickly, then confirm the result with the API

  const r = await fetch(`https://fsmb616.com/v1/tasks/?id=${encodeURIComponent(taskId)}`, {
    headers: { "xi-api-key": process.env.FSMB_API_KEY },
  });
  const s = await r.json();
  if (s.status === "done") console.log("audio ready:", s.audio_url); // download and store it
  if (s.status === "failed") console.log("task failed:", taskId);
});

app.listen(3000);
<?php
$payload = json_decode(file_get_contents("php://input"), true);
$taskId  = is_array($payload) ? (string)($payload["task_id"] ?? "") : "";
http_response_code(204);

// Confirm the result with the API instead of trusting the payload alone.
$ch = curl_init("https://fsmb616.com/v1/tasks/?id=" . urlencode($taskId));
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => ["xi-api-key: YOUR_API_KEY"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$status = json_decode(curl_exec($ch), true);
curl_close($ch);

if (($status["status"] ?? "") === "done") {
    // download $status["audio_url"] and store it
}

Download audio

GET/v1/files/TASK_ID.mp3

This is the audio_url you receive. The file is an MP3 and downloading it needs no API key.

Links are private by obscurity only Anyone who has the link can download the file, so do not publish it.
Files are removed after a few days Audio is kept for at least 7 days and then deleted. Download it and store it on your side.

Voices

GET/v1/voices/

Lists the ready-made voices you can use as voice_id.

ParameterDescription
providerOne of minimax (default), elevenlabs, edge, kokoro, vbee, fishaudio. Anything else returns 400 invalid_provider.
page, page_sizePaging. Defaults are page 1 and 20 voices.
languageOptional filter, for example English.
genderOptional filter: Male or Female.
curl "https://fsmb616.com/v1/voices/?provider=minimax&page=1&page_size=20" \
  -H "xi-api-key: YOUR_API_KEY"
import requests

r = requests.get(
    "https://fsmb616.com/v1/voices/",
    headers={"xi-api-key": "YOUR_API_KEY"},
    params={"provider": "minimax", "page": 1, "page_size": 20},
    timeout=30,
)
for v in r.json()["data"]:
    print(v["voice_id"], v["name"])
const url = "https://fsmb616.com/v1/voices/?provider=minimax&page=1&page_size=20";
const res = await fetch(url, { headers: { "xi-api-key": "YOUR_API_KEY" } });
const body = await res.json();
for (const v of body.data) console.log(v.voice_id, v.name);
<?php
$ch = curl_init("https://fsmb616.com/v1/voices/?provider=minimax&page=1&page_size=20");
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => ["xi-api-key: YOUR_API_KEY"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);
foreach ($body["data"] ?? [] as $v) {
    echo $v["voice_id"] . " " . $v["name"] . "\n";
}

The response has success, format_version and a data list. Each item describes one voice:

FieldDescription
voice_idPass this as voice_id in a speech request, exactly as returned.
nameThe voice's display name.
language, localeThe main language, for example English and en.
genderMale or Female.
age, accentDescriptive labels, for example Middle Age and EN-US (General).
description, categoryFree text. Can be null.
use_cases, descriptives, tagsLists of labels you can show or filter on.
languagesThe languages the voice supports. Each item has language, locale, accent and a preview_url.
Response (shortened)
{
  "success": true,
  "format_version": "2026-05-29",
  "data": [
    {
      "voice_id": "minimax_209533299589191",
      "name": "Bossy Lady",
      "description": null,
      "language": "English",
      "locale": "en",
      "gender": "Female",
      "age": "Middle Age",
      "accent": "EN-US (General)",
      "category": null,
      "use_cases": ["Mature"],
      "descriptives": [],
      "tags": ["English", "Female", "Middle Age", "Mature", "EN-US (General)", "en"],
      "languages": [
        {
          "language": "English",
          "locale": "en",
          "accent": "EN-US (General)",
          "preview_url": "https://fsmb616.com/v1/preview/?..."
        }
      ]
    }
  ]
}

Not every voice fills every field, so rely only on voice_id and name being present. A preview_url is a short sample hosted by us (https://fsmb616.com/v1/preview/...). It opens directly in an audio or image tag and needs no API key.

A voice ID starts with its provider: minimax_, elevenlabs_, edge_, kokoro_, vbee_ or fishaudio_. Voices you clone start with clone_ and are not part of this list.

Voice cloning

POST/v1/text-to-speech/voice-clone/

Creates a new voice from a recording. Send multipart/form-data.

FieldDescription
filesrequiredThe audio file (MP3, WAV or M4A).
nameoptionalA label for the voice, up to 100 characters. Default My Voice.
curl -X POST "https://fsmb616.com/v1/text-to-speech/voice-clone/" \
  -H "xi-api-key: YOUR_API_KEY" \
  -F "name=My Voice" \
  -F "files=@sample.mp3"
import requests

with open("sample.mp3", "rb") as f:
    r = requests.post(
        "https://fsmb616.com/v1/text-to-speech/voice-clone/",
        headers={"xi-api-key": "YOUR_API_KEY"},
        data={"name": "My Voice"},
        files={"files": ("sample.mp3", f, "audio/mpeg")},
        timeout=180,
    )
print(r.status_code, r.json())
const { readFile } = require("node:fs/promises");

async function cloneVoice() {
  const form = new FormData();
  form.append("name", "My Voice");
  form.append("files", new Blob([await readFile("sample.mp3")], { type: "audio/mpeg" }), "sample.mp3");

  const res = await fetch("https://fsmb616.com/v1/text-to-speech/voice-clone/", {
    method: "POST",
    headers: { "xi-api-key": "YOUR_API_KEY" },
    body: form,
  });
  console.log(res.status, await res.json());
}

cloneVoice();
<?php
$ch = curl_init("https://fsmb616.com/v1/text-to-speech/voice-clone/");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => [
        "name"  => "My Voice",
        "files" => new CURLFile("sample.mp3", "audio/mpeg", "sample.mp3"),
    ],
    CURLOPT_HTTPHEADER     => ["xi-api-key: YOUR_API_KEY"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 180,
]);
echo curl_exec($ch);
curl_close($ch);
Response
{
  "success": true,
  "data": { "voice_id": "clone_2673890", "voice_name": "My Voice" }
}

Use the returned voice_id (it already starts with clone_) as the voice_id in a speech request. Cloning does not use your character quota.

A cloned voice belongs to your key Only the API key that created a cloned voice can use it. Any other key gets 403 voice_not_allowed. Keep your voice IDs and your API key private.

For a good clone

  • One speaker, clear speech, 30 to 120 seconds, with no music or background noise.
  • Keep the file under 10 MB. Cloning can take a minute, so allow a long timeout.

Errors

Errors return a non-2xx status and a JSON body with "success": false. Check the HTTP status first, then the error.code.

StatusCodeWhat to do
401missing_api_keySend the xi-api-key header.
401invalid_api_keyCheck the key for typos.
403key_disabledThe key was disabled. Contact support.
403voice_not_allowedThe voice is a clone that belongs to another API key.
400missing_textSend a non-empty text.
400invalid_receive_urlreceive_url must be HTTPS on port 443.
400missing_fileCloning needs the audio in the files field, and the upload must succeed.
400invalid_providerUse one of the providers listed under Voices.
400invalid_voice_idThe voice_id is malformed, for example it contains spaces or unusual characters.
400invalid_task_idThe id is not a task ID.
402quota_exhaustedNo characters left. Top up your plan.
402insufficient_quotaThe text is longer than what remains. The body includes remaining_characters.
404task_not_foundThe task does not exist, has expired, or belongs to another key.
404voice_not_foundThe voice_id does not exist. Use an ID from the voice list or one of your own clones.
502upstream_unreachable, upstream_errorA temporary problem on our side. Nothing was charged. Retry after a short wait.

Other 4xx responses carry a message that explains the problem, for example a voice_id that is not valid.

Limits and billing

  • One character is one unit. The characters in text (letters, spaces and punctuation) are deducted from the key's quota when a task is accepted.
  • Rejected requests are free. Nothing is deducted for a 4xx or 5xx response.
  • Failed tasks. If a task ends with status: failed, contact support with its task_id.
  • Cloning does not use the character quota.
  • Retention. Audio files stay for at least 7 days; task records for at least 14 days.
  • Volume. There is no per-minute limit documented here. If you plan to send many requests at once, tell us first.

To check or top up your quota, contact support below.

Full examples

Text in, MP3 file out. Each example submits the text, waits for the task, and saves the audio.

import time
import requests

API = "https://fsmb616.com/v1"
HEADERS = {"xi-api-key": "YOUR_API_KEY"}


def text_to_mp3(text, voice_id, out_path, timeout=180):
    r = requests.post(
        f"{API}/text-to-speech/",
        headers=HEADERS,
        data={"text": text, "voice_id": voice_id, "speed": "1"},
        timeout=30,
    )
    body = r.json()
    if not body.get("success"):
        raise RuntimeError(body)
    task_id = body["task_id"]

    deadline = time.time() + timeout
    while time.time() < deadline:
        s = requests.get(f"{API}/tasks/", headers=HEADERS, params={"id": task_id}, timeout=30).json()
        if s.get("status") == "done":
            audio = requests.get(s["audio_url"], timeout=120)
            audio.raise_for_status()
            with open(out_path, "wb") as f:
                f.write(audio.content)
            return out_path
        if s.get("status") == "failed":
            raise RuntimeError(f"Task {task_id} failed")
        time.sleep(4)
    raise TimeoutError(f"Task {task_id} is still processing")


text_to_mp3("Hello from FSMB.", "minimax_209533299589198", "hello.mp3")
const { writeFile } = require("node:fs/promises");

const API = "https://fsmb616.com/v1";
const KEY = process.env.FSMB_API_KEY;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function textToMp3(text, voiceId, outPath) {
  const form = new FormData();
  form.append("text", text);
  form.append("voice_id", voiceId);
  form.append("speed", "1");

  const submit = await (
    await fetch(`${API}/text-to-speech/`, {
      method: "POST",
      headers: { "xi-api-key": KEY },
      body: form,
    })
  ).json();
  if (!submit.success) throw new Error(JSON.stringify(submit));

  const deadline = Date.now() + 180000;
  while (Date.now() < deadline) {
    const res = await fetch(`${API}/tasks/?id=${submit.task_id}`, { headers: { "xi-api-key": KEY } });
    const s = await res.json();
    if (s.status === "done") {
      const audio = await fetch(s.audio_url);
      await writeFile(outPath, Buffer.from(await audio.arrayBuffer()));
      return outPath;
    }
    if (s.status === "failed") throw new Error(`Task ${submit.task_id} failed`);
    await sleep(4000);
  }
  throw new Error("Timed out waiting for audio");
}

textToMp3("Hello from FSMB.", "minimax_209533299589198", "hello.mp3").then(console.log);

Use it on your website

A website has two sides: the pages your visitors see (the frontend) and your own server (the backend). Your API key must live only on the backend. The page talks to your backend, and your backend talks to FSMB.

  1. The visitor types text and clicks a button. The page sends it to your backend.
  2. Your backend calls POST /v1/text-to-speech/ with your key and hands the task_id back to the page.
  3. The page asks your backend for the task every few seconds. Your backend calls GET /v1/tasks/ and returns the status.
  4. When the status is done, the page plays the audio_url.

1. Your backend

Pick the language your site runs on. Every version offers the same two addresses, /api/tts and /api/task (the PHP version uses two files, api/tts.php and api/task.php). It checks the input, adds your key, and passes the answer back to your page.

# app.py  -  install:  pip install flask requests
# run:  FSMB_API_KEY=your_key flask run
import os
import re

import requests
from flask import Flask, jsonify, request

app = Flask(__name__)

API = "https://fsmb616.com/v1"
KEY = os.environ["FSMB_API_KEY"]   # the key lives only here, never in page code
HEADERS = {"xi-api-key": KEY}
MAX_TEXT = 2000                    # your own limit per request


def forward(response):
    try:
        data = response.json()
    except ValueError:
        return jsonify(error="Bad response"), 502
    return jsonify(data), response.status_code


@app.post("/api/tts")
def tts():
    body = request.get_json(silent=True) or {}
    text = str(body.get("text", "")).strip()
    voice_id = str(body.get("voice_id", "minimax_209533299589198"))
    if not text or len(text) > MAX_TEXT:
        return jsonify(error="Text is empty or too long"), 400
    if not re.fullmatch(r"[A-Za-z0-9_.\-]{1,120}", voice_id):
        return jsonify(error="Invalid voice"), 400
    try:
        r = requests.post(
            f"{API}/text-to-speech/",
            headers=HEADERS,
            data={"text": text, "voice_id": voice_id, "speed": "1"},
            timeout=30,
        )
    except requests.RequestException:
        return jsonify(error="Service unavailable, please try again"), 502
    return forward(r)


@app.get("/api/task")
def task():
    task_id = request.args.get("id", "")
    if not re.fullmatch(r"[A-Za-z0-9\-]{36}", task_id):
        return jsonify(error="Invalid task id"), 400
    try:
        r = requests.get(f"{API}/tasks/", headers=HEADERS, params={"id": task_id}, timeout=30)
    except requests.RequestException:
        return jsonify(error="Service unavailable, please try again"), 502
    return forward(r)
// server.js  -  run:  FSMB_API_KEY=your_key node server.js   (Node 18 or newer, no packages needed)
const http = require("node:http");

const API = "https://fsmb616.com/v1";
const KEY = process.env.FSMB_API_KEY;   // the key lives only here, never in page code
const MAX_TEXT = 2000;                  // your own limit per request

if (!KEY) {
  console.error("Set FSMB_API_KEY first.");
  process.exit(1);
}

function send(res, status, body) {
  res.writeHead(status, { "Content-Type": "application/json" });
  res.end(JSON.stringify(body));
}

function readJson(req) {
  return new Promise((resolve, reject) => {
    let raw = "";
    let tooBig = false;
    req.on("data", (chunk) => {
      if (raw.length < 20000) raw += chunk;
      else tooBig = true;
    });
    req.on("end", () => {
      if (tooBig) return reject(new Error("too large"));
      try { resolve(JSON.parse(raw || "{}")); } catch (e) { reject(e); }
    });
    req.on("error", reject);
  });
}

http.createServer(async (req, res) => {
  try {
    const url = new URL(req.url, "http://localhost");

    if (req.method === "POST" && url.pathname === "/api/tts") {
      let body;
      try { body = await readJson(req); } catch { return send(res, 400, { error: "Invalid request" }); }
      const text = String(body.text || "").trim();
      const voiceId = String(body.voice_id || "minimax_209533299589198");
      if (!text || text.length > MAX_TEXT) return send(res, 400, { error: "Text is empty or too long" });
      if (!/^[A-Za-z0-9_.\-]{1,120}$/.test(voiceId)) return send(res, 400, { error: "Invalid voice" });

      const form = new FormData();
      form.append("text", text);
      form.append("voice_id", voiceId);
      form.append("speed", "1");
      const r = await fetch(`${API}/text-to-speech/`, {
        method: "POST",
        headers: { "xi-api-key": KEY },
        body: form,
      });
      return send(res, r.status, await r.json().catch(() => ({ error: "Bad response" })));
    }

    if (req.method === "GET" && url.pathname === "/api/task") {
      const id = url.searchParams.get("id") || "";
      if (!/^[A-Za-z0-9\-]{36}$/.test(id)) return send(res, 400, { error: "Invalid task id" });
      const r = await fetch(`${API}/tasks/?id=${encodeURIComponent(id)}`, {
        headers: { "xi-api-key": KEY },
      });
      return send(res, r.status, await r.json().catch(() => ({ error: "Bad response" })));
    }

    send(res, 404, { error: "Not found" });
  } catch {
    send(res, 502, { error: "Service unavailable, please try again" });
  }
}).listen(3000, () => console.log("Listening on http://localhost:3000"));
<?php
// ---------- File 1: api/tts.php ----------
// Keep the key outside your web folder, for example in an environment variable
// or in a file such as /home/youruser/fsmb_key.php that contains:  <?php $FSMB_KEY = "your_key";
$KEY = getenv("FSMB_API_KEY");
header("Content-Type: application/json");

$in    = json_decode(file_get_contents("php://input"), true);
$text  = trim((string)($in["text"] ?? ""));
$voice = (string)($in["voice_id"] ?? "minimax_209533299589198");

if ($text === "" || mb_strlen($text) > 2000) {
    http_response_code(400);
    echo json_encode(["error" => "Text is empty or too long"]);
    exit;
}
if (!preg_match('/^[A-Za-z0-9_.\-]{1,120}$/', $voice)) {
    http_response_code(400);
    echo json_encode(["error" => "Invalid voice"]);
    exit;
}

$ch = curl_init("https://fsmb616.com/v1/text-to-speech/");
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => ["text" => $text, "voice_id" => $voice, "speed" => "1"],
    CURLOPT_HTTPHEADER     => ["xi-api-key: " . $KEY],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$out  = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($out === false || $code === 0) {
    http_response_code(502);
    echo json_encode(["error" => "Service unavailable, please try again"]);
    exit;
}
http_response_code($code);
echo $out;

<?php
// ---------- File 2: api/task.php ----------
$KEY = getenv("FSMB_API_KEY");
header("Content-Type: application/json");

$id = (string)($_GET["id"] ?? "");
if (!preg_match('/^[A-Za-z0-9\-]{36}$/', $id)) {
    http_response_code(400);
    echo json_encode(["error" => "Invalid task id"]);
    exit;
}

$ch = curl_init("https://fsmb616.com/v1/tasks/?id=" . urlencode($id));
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => ["xi-api-key: " . $KEY],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$out  = curl_exec($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($out === false || $code === 0) {
    http_response_code(502);
    echo json_encode(["error" => "Service unavailable, please try again"]);
    exit;
}
http_response_code($code);
echo $out;

2. Your page

Put this in any page of your site. It never sees your key.

Your page (HTML and JavaScript)
<textarea id="text" rows="4" maxlength="2000" placeholder="Type something to speak"></textarea>
<button id="speak" type="button">Speak</button>
<p id="status"></p>
<audio id="player" controls hidden></audio>

<script>
// These point to YOUR backend. Never call the FSMB API straight from the page (it would expose your key).
const TTS_URL  = "/api/tts";    // PHP version: "/api/tts.php"
const TASK_URL = "/api/task";   // PHP version: "/api/task.php"

const $ = (id) => document.getElementById(id);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function message(data) {
  if (data && data.error && data.error.message) return data.error.message;
  if (data && typeof data.error === "string") return data.error;
  if (data && data.message) return data.message;
  return "Request failed";
}

async function speak() {
  const button = $("speak");
  const status = $("status");
  const player = $("player");
  button.disabled = true;
  player.hidden = true;
  status.textContent = "Working...";

  try {
    const res = await fetch(TTS_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text: $("text").value, voice_id: "minimax_209533299589198" }),
    });
    const job = await res.json();
    if (!res.ok || !job.task_id) throw new Error(message(job));

    for (let i = 0; i < 60; i++) {            // about 3 minutes at most
      await sleep(3000);
      const r = await fetch(TASK_URL + "?id=" + encodeURIComponent(job.task_id));
      const task = await r.json();
      if (!r.ok) throw new Error(message(task));
      if (task.status === "done") {
        player.src = task.audio_url;
        player.hidden = false;
        player.play().catch(() => {});        // some browsers block autoplay
        status.textContent = "Done";
        return;
      }
      if (task.status === "failed") throw new Error("Generation failed");
    }
    throw new Error("Timed out, please try again");
  } catch (e) {
    status.textContent = "Error: " + e.message;
  } finally {
    button.disabled = false;
  }
}

$("speak").addEventListener("click", speak);
</script>

Before you go live

  • Protect your own endpoint. /api/tts is public by nature, so anyone who finds it can spend your quota. Require a login or session, limit the text length (the examples allow 2000 characters), and limit how often one visitor can call it.
  • Keep the key off the page and out of your code repository. Use an environment variable or a file outside your web folder.
  • Serve your site over HTTPS, so your visitors' text is not sent in plain text.
  • Save the audio if you need it later. Links stay valid for at least 7 days. Download the file in your backend and keep your own copy.
  • For busy sites, use a webhook instead of polling. Send receive_url with the request. Your backend receives the result and stores the audio, and your page only reads from your own database.
  • Show friendly messages. Quota errors (402) and temporary errors (502) are normal. Tell the visitor what to do, for example try again later.

Acceptable use

Only clone a voice if it is yours or you have the speaker's permission. Do not use cloned or generated voices to impersonate, deceive or defraud anyone. We may disable keys that are used this way.

Support

Write to us with your task_id and the time of the request, and we can trace it.

WhatsApp
+92 313 6725508
Email
mohammadmajid616@gmail.com

More Tools (all under https://fsmb616.com/v1/)

Same auth as other endpoints: header xi-api-key: YOUR_KEY. All are POST and return {"success":true,"task_id":"..."}. Poll the result with GET /v1/tasks/?id=TASK_ID (same key). When status is done you get audio_url and/or files (links on fsmb616.com) or result (text). Optional receive_url (https only) gets a callback when finished.

EndpointInputCharge
/v1/dialogue/form: text ("A> hi" / "B> hello" lines), speakers (JSON array, min 2, each with voice_id), optional delay, with_transcript1 per character of text
/v1/dubbing/multipart: file, target_lang, optional source_lang (default auto), num_speakers, voice_id5,000
/v1/sound-effect/JSON body: text, optional duration_seconds50 per second (min 50; 200 if no duration)
/v1/speech-to-text/multipart: file, optional tag_audio_events1,000
/v1/voice-changer/multipart: file, voice_id, optional model_id, remove_background_noise3,000
/v1/voice-isolate/multipart: file2,000

Max upload 100MB. Charges are deducted when the task is accepted and are not refunded if a task fails later.

Example

curl -X POST https://fsmb616.com/v1/sound-effect/ \
  -H "xi-api-key: YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"text":"door knock","duration_seconds":3}'

curl "https://fsmb616.com/v1/tasks/?id=TASK_ID" -H "xi-api-key: YOUR_KEY"

Errors

401 missing/invalid key · 403 key disabled or voice not allowed · 400 bad parameters · 402 not enough characters · 413 file too large · 502 temporary service problem (try again).

Models

GET /v1/models/ (header xi-api-key) lists available models. Pass one as model_id in POST /v1/text-to-speech/, e.g. fsmb-ultra, fsmb-flash, fsmb-multilingual. If model_id is omitted the default model is used.