71 voces en 20 idiomas, con etiquetas de expresión
- 71 voces
- 20 idiomas
- Texto a voz
Llámalo con el nombre de modelo xai/grok-tts/text-to-speech en POST /jobs/createTask, desde 3 créditos por 1.000 caracteres (0,03 US$). Las cuentas nuevas reciben 30 créditos gratis para que lo pruebes primero en el Playground.
xai/grok-tts/text-to-speechPOST https://api.popnow.ai/api/v1/jobs/createTask# 1. Create the task. Keep the Idempotency-Key if you retry after a network error.
export IDEMPOTENCY_KEY="$(uuidgen)"
curl -X POST https://api.popnow.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"model": "xai/grok-tts/text-to-speech",
"input": {
"text": "Welcome back. Your files finished rendering a minute ago and are ready to download.",
"voice_id": "eve",
"language": "auto"
}
}'
# 2. Copy data.taskId from the response, then poll until state is succeeded, failed, canceled or expired.
TASK_ID="TASK_ID_FROM_RESPONSE"
curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $POPNOW_API_KEY"// Node 18+ (or any runtime with fetch). Keep the API key on your server.
const BASE = "https://api.popnow.ai/api/v1";
const headers = { Authorization: `Bearer ${process.env.POPNOW_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function call(path, init = {}) {
const res = await fetch(BASE + path, { ...init, headers: { ...headers, ...init.headers } });
const body = await res.json();
if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);
return body.data;
}
const { taskId } = await call("/jobs/createTask", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
"model": "xai/grok-tts/text-to-speech",
"input": {
"text": "Welcome back. Your files finished rendering a minute ago and are ready to download.",
"voice_id": "eve",
"language": "auto"
}
}),
});
// Poll with backoff. A finished task may still be copying files: wait while an asset is pending.
let task;
for (let delay = 2000; ; delay = Math.min(delay * 1.5, 15000)) {
await sleep(delay);
task = await call(`/jobs/recordInfo?taskId=${taskId}`);
const copying = (task.output?.assets ?? []).some((a) => a.pending && !a.unavailable);
if (["succeeded", "failed", "canceled", "expired"].includes(task.state) && !copying) break;
}
if (task.state !== "succeeded") throw new Error(`${task.state}: ${task.errorMessage ?? "no detail"}`);
// Each asset has a signed url (no API key needed) that expires at expiresAt: save the file.
for (const asset of task.output.assets) console.log(asset.url);import os, time, uuid, requests
BASE = "https://api.popnow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['POPNOW_API_KEY']}"}
def call(method, path, **kwargs):
body = requests.request(method, BASE + path, headers={**HEADERS, **kwargs.pop("headers", {})}, timeout=30, **kwargs).json()
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]}')
return body["data"]
task_id = call(
"POST",
"/jobs/createTask",
headers={"Idempotency-Key": str(uuid.uuid4())}, # reuse it if you retry
json={
"model": "xai/grok-tts/text-to-speech",
"input": {
"text": "Welcome back. Your files finished rendering a minute ago and are ready to download.",
"voice_id": "eve",
"language": "auto"
}
},
)["taskId"]
# Poll with backoff. A finished task may still be copying files: wait while an asset is pending.
delay = 2.0
while True:
time.sleep(delay)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
assets = (task.get("output") or {}).get("assets") or []
copying = any(a.get("pending") and not a.get("unavailable") for a in assets)
if task["state"] in ("succeeded", "failed", "canceled", "expired") and not copying:
break
delay = min(delay * 1.5, 15.0)
if task["state"] != "succeeded":
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
# Each asset has a signed url (no API key needed) that expires at expiresAt: save the file.
for asset in assets:
print(asset["url"])El input se genera a partir del esquema de abajo, así que pasa la validación tal cual. Cambia el prompt y los enlaces por los tuyos.
Precio
| Opción | Créditos por 1.000 caracteres | ≈ USD |
|---|---|---|
| Estándar | 2,7 | 0,027 US$ |
1 crédito = 0,01 $ · Todas las opciones y su precio: GET /models/xai/grok-tts/text-to-speech
Cada tarea se redondea al alza a créditos enteros, así que una ejecución en el nivel más bajo cuesta 3 créditos por 1.000 caracteres (0,03 US$).
Límites
- Longitud del texto (caracteres)
- 15,000
- Voces
- 71
- Idiomas
- 20
Parámetros
Estos campos salen del mismo esquema con el que validamos cada petición. Los campos que no aparecen se rechazan.
textstringObligatorioThe words to be spoken, with optional speech tags such as [laugh] or <whisper>…</whisper>. Billed per 1,000 characters, tags included.
voice_idstringOpcionalElige entre las voces disponibles.
languagestringOpcionalEl idioma de la locución.
speednumberOpcionalRitmo al hablar.
output_formatstringOpcionalTipo de archivo del resultado.
sample_rateintegerOpcionalSample rate in Hz. 8000 for phone lines, 44100 or 48000 for studio work.
bitrateintegerOpcionalMP3 bit rate in bits per second. Only applies to mp3.
text_normalizationbooleanOpcionalRewrite numbers, abbreviations and symbols as spoken words before reading.
optimize_streaming_latencyintegerOpcionalStreaming latency level: 0 best quality, 1 smaller first chunk, 2 smallest first chunk with more quality trade-off at chunk boundaries.
Esquema JSON sin procesar
{
"type": "object",
"required": [
"text"
],
"properties": {
"text": {
"type": "string",
"maxLength": 15000,
"minLength": 1,
"description": "The words to be spoken, with optional speech tags such as [laugh] or <whisper>…</whisper>. Billed per 1,000 characters, tags included."
},
"speed": {
"type": "number",
"default": 1,
"maximum": 1.5,
"minimum": 0.7,
"description": "Speaking speed from 0.7 (slower) to 1.5 (faster)."
},
"bitrate": {
"enum": [
32000,
64000,
96000,
128000,
192000
],
"type": "integer",
"default": 128000,
"description": "MP3 bit rate in bits per second. Only applies to mp3."
},
"language": {
"enum": [
"auto",
"en",
"zh",
"ar-EG",
"ar-SA",
"ar-AE",
"bn",
"fr",
"de",
"hi",
"id",
"it",
"ja",
"ko",
"pt-BR",
"pt-PT",
"ru",
"es-MX",
"es-ES",
"tr",
"vi"
],
"type": "string",
"default": "auto",
"description": "Reading language, or auto to detect it from the text."
},
"voice_id": {
"enum": [
"ara",
"eve",
"leo",
"rex",
"sal",
"jpi39icg",
"d18jlf6v",
"33g9t0jl",
"26w6ihxi",
"dr8gqysu",
"wy0m9l5w",
"om17cury",
"x7avnu1k",
"bcs7l2c3",
"hqxr4yub",
"h27ltdnz",
"89q2pnko",
"73xd5dum",
"0p0rt7o1",
"hbxkrnwm",
"69smp8rm",
"yis75yfp",
"ekhwx401",
"jupvcf34",
"0hhfxxqq",
"0ih5oi34",
"gwnexu6y",
"97zmdc6s",
"fc7de6afcf6c",
"7a9ee820b342",
"0895a5b8ce5c",
"f8cf5c2c78d4",
"96819d0bd28d",
"79f3a8b96d43",
"78a495fdbb39",
"e22152e06fd8",
"490ea3be50b1",
"1f046a033914",
"dfe7b9e7d217",
"c3a2c594479e",
"83c6f4fea98e",
"34fd4dce1ba3",
"d634b6da3d3b",
"670a0c3ac005",
"182a91893636",
"d0cb9ff07d95",
"b1a7441b97a1",
"bf9fe5b5f981",
"b5ae17439907",
"a0401c9101f8",
"23be42535a45",
"abfbdf26f115",
"6da5baee46d0",
"3d030bc92a87",
"a13662ba951c",
"58d27475085e",
"247783ebdd51",
"244e27b39200",
"97fabd54445f",
"37329fd8895a",
"2badb5f46b1e",
"1b12d5daee6b",
"908c4626660f",
"4ff93971bfdc",
"70013edeb8e8",
"35c8d7f60dc8",
"23468361b4ef",
"458705c07139",
"41321eb41295",
"40f31906b23d",
"3a7889066fa2"
],
"type": "string",
"default": "eve",
"description": "Preset voice. Eve, Ara, Leo, Rex and Sal speak every supported language; the others are tuned for one language."
},
"sample_rate": {
"enum": [
8000,
16000,
22050,
24000,
44100,
48000
],
"type": "integer",
"default": 24000,
"description": "Sample rate in Hz. 8000 for phone lines, 44100 or 48000 for studio work."
},
"output_format": {
"enum": [
"mp3",
"wav",
"pcm",
"mulaw",
"alaw"
],
"type": "string",
"default": "mp3",
"description": "Audio encoding. pcm, mulaw and alaw are raw streams without a file header, for phone systems."
},
"text_normalization": {
"type": "boolean",
"default": false,
"description": "Rewrite numbers, abbreviations and symbols as spoken words before reading."
},
"optimize_streaming_latency": {
"enum": [
0,
1,
2
],
"type": "integer",
"default": 0,
"description": "Streaming latency level: 0 best quality, 1 smaller first chunk, 2 smallest first chunk with more quality trade-off at chunk boundaries."
}
},
"additionalProperties": false
}Respuestas, consultas y callbacks
Todos los modelos funcionan igual: crea una tarea y luego consulta su estado o espera el callback.
Consulta el precio primero
curl -X POST https://api.popnow.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "xai/grok-tts/text-to-speech",
"input": {
"text": "Welcome back. Your files finished rendering a minute ago and are ready to download.",
"voice_id": "eve",
"language": "auto"
}
}'El mismo cuerpo que createTask. Devuelve el cargo esperado y el máximo posible, sin ejecutar nada.
Aceptada
HTTP/1.1 202 Accepted
{
"code": 200,
"msg": "success",
"data": {
"taskId": "task_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued"
},
"request_id": "req_example_create"
}HTTP 202 significa que la tarea se aceptó y los créditos quedan reservados, todavía sin cobrar. El cargo final nunca supera la reserva.
Consultar estado
curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $POPNOW_API_KEY"Consulta cada pocos segundos espaciando cada vez más y para cuando state sea succeeded, failed, canceled o expired.
Tarea terminada
{
"code": 200,
"msg": "success",
"data": {
"taskId": "task_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "xai/grok-tts/text-to-speech",
"state": "succeeded",
"output": {
"assets": [
{
"url": "https://cdn.popnow.ai/tasks/task_01k3m8x9q2z4v7n5p6r8s0t1w3/output.mp3?signature=FROM_RESPONSE",
"expiresAt": "2026-10-17T12:20:00Z",
"mime": "audio/mpeg",
"durationSeconds": 30
}
]
},
"settled": true
},
"request_id": "req_example_record"
}Descarga desde la url antes de expiresAt. ¿Necesitas un enlace nuevo más tarde? Pídelo con POST /common/download-url.
Verificar un callback
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
// express.raw keeps the exact bytes that were signed; express.json would not match.
app.post("/hooks/popnow", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.header("X-Webhook-Timestamp");
const signature = req.header("X-Webhook-Signature") ?? "";
const body = JSON.parse(req.body.toString("utf8"));
const taskId = body.data?.taskId ?? body.task_id;
const digest = createHash("sha256").update(req.body).digest("hex");
const expected = createHmac("sha256", process.env.POPNOW_WEBHOOK_SECRET)
.update(`${taskId}.${timestamp}.${digest}`)
.digest("base64");
const ok = expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
// Check the signature first, then reject anything older than five minutes.
if (!ok || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end();
res.status(200).end(); // answer fast, then fetch the task and store its files
});Con callBackUrl te enviamos la tarea terminada. Comprueba la firma sobre el cuerpo sin procesar antes de fiarte de ella.
Enviar un archivo local
curl -X POST https://api.popnow.ai/api/v1/common/upload-url \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contentType": "image/png", "bytes": 482113}'
# PUT the file bytes to data.uploadUrl with every header in data.headers, then commit it:
curl -X POST https://api.popnow.ai/api/v1/files/$FILE_ID/commit \
-H "Authorization: Bearer $POPNOW_API_KEY"
# The commit returns a popnow://f/... reference. Put it in any image, video or
# audio field of the task input instead of a public https URL.Los enlaces https públicos funcionan tal cual. Los archivos locales pasan antes por upload-url y commit.