Documentación de la API de Makernice: inicio rápido
- Ideal para
- Desarrolladores que llaman a la generación de video e imagen desde su propio código por REST: cotizar, crear y luego consultar el estado o recibir un webhook.
- No es para
- La API de Makernice no es para hacer videos o imágenes sin escribir código; para eso usa la sección Crear de la consola, que tiene los mismos modelos, precios y saldo.
Genera un video con una clave de API y tres solicitudes: obtén una cotización, crea la generación y consulta su estado hasta que esté lista.
1. Crea una clave de API
En la consola, abre Desarrolladores → Claves de API y crea una clave. Envíala como token Bearer en cada solicitud. Las generaciones se cobran a tu billetera; puedes ponerle a cada clave un límite de gasto.
2. Obtén una cotización
curl https://api.makernice.com/v1/generations/quote \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"seedance-2.5","mode":"t2v","prompt":"Un travelling lento a través de un invernadero","duration":5,"resolution":"720p"}'
# → (valores ilustrativos) { "id": "qt_…", "amount": "500000", "expires_at": "…", "facts": { "quantity": 5, … } }Los montos están en dólares estadounidenses, en millonésimas de dólar (micro-USD): 1,000,000 = $1.00. Una cotización deja el precio fijo por 12 minutos.
3. Crea la generación
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Idempotency-Key: 7c1e9a2b-order-1842" \
-H "Content-Type: application/json" \
-d '{"quote_id":"qt_…","input":{"model":"seedance-2.5","mode":"t2v","prompt":"Un travelling lento a través de un invernadero","duration":5,"resolution":"720p"}}'
# → 202 { "id": "gen_…", "status": "queued", "billing": "held", … }El monto se retiene de tu saldo. Si la generación falla o la cancelas mientras está en cola, la retención se libera por completo. Si reintentas con el mismo Idempotency-Key, recibes la misma generación en lugar de un segundo cobro.
4. Consulta hasta que esté lista
curl https://api.makernice.com/v1/generations/gen_… \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# status: queued → generating → moderating → succeeded | failed | canceled
# cuando es succeeded: outputs[0].url → pídelo con GET y tu clave para recibir un 302 al archivo5. Descarga el archivo
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
-H "Authorization: Bearer $MAKERNICE_API_KEY"Python: cotizar, crear y consultar
Define MAKERNICE_API_KEY y luego ejecuta:
import os
import time
import uuid
from decimal import Decimal
import requests
API = "https://api.makernice.com/v1"
ORIGIN = "https://api.makernice.com"
HEADERS = {"Authorization": f"Bearer {os.environ['MAKERNICE_API_KEY']}"}
TIMEOUT_SECONDS = 30
POLL_INTERVAL_SECONDS = 5
MAX_WAIT_SECONDS = 30 * 60
FINAL = {"succeeded", "failed", "canceled"}
def call(method, path, headers=None, **kwargs):
# Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; raises on any other error.
for _ in range(5):
r = requests.request(method, API + path, headers={**HEADERS, **(headers or {})}, timeout=TIMEOUT_SECONDS, **kwargs)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue
if not r.ok:
raise RuntimeError(f"{method} {path} -> {r.status_code}: {r.text}")
return r.json()
raise RuntimeError(f"{method} {path}: still rate limited")
job = {
"model": "seedance-2.5",
"mode": "t2v",
"prompt": "A slow dolly shot through a greenhouse",
"duration": 5,
"resolution": "720p",
}
# 1. Quote: the exact amount, locked for 12 minutes
quote = call("POST", "/generations/quote", json=job)
usd = Decimal(quote["amount"]).scaleb(-6) # micro-USD integer string -> dollars, no float rounding
print(f"Quoted ${usd} (price list v{quote['price_version']}), valid until {quote['expires_at']}")
# 2. Create: same input + quote_id. A retry with the same Idempotency-Key returns the same generation
idempotency_key = str(uuid.uuid4())
gen = call("POST", "/generations", json={"quote_id": quote["id"], "input": job}, headers={"Idempotency-Key": idempotency_key})
# 3. Poll: queued -> generating -> moderating -> succeeded | failed | canceled (checking: keep polling)
deadline = time.monotonic() + MAX_WAIT_SECONDS
while gen["status"] not in FINAL:
if time.monotonic() > deadline:
raise SystemExit(f"{gen['id']} is still {gen['status']}; poll it again later")
time.sleep(POLL_INTERVAL_SECONDS)
gen = call("GET", f"/generations/{gen['id']}")
if gen["status"] != "succeeded":
# billing is "released": the hold went back to your balance
raise SystemExit(f"{gen['status']}, billing {gen['billing']}: {gen['error']}")
# 4. Download: outputs[].url is relative to the API origin and redirects (302) to the file
output = gen["outputs"][0]
if output["state"] == "blocked":
raise SystemExit("Output blocked by moderation (the run is charged); contact support for a manual review")
r = requests.get(ORIGIN + output["url"], headers=HEADERS, timeout=120) # requests drops Authorization on the redirect
r.raise_for_status()
with open("output.mp4", "wb") as f:
f.write(r.content)
print(f"Saved output.mp4, charged ${Decimal(gen['amount']).scaleb(-6)}")Node.js: cotizar, crear y consultar
Node.js 18 o posterior, sin instalar paquetes. Guárdalo como quickstart.mjs, define MAKERNICE_API_KEY y ejecuta node quickstart.mjs:
// Node.js 18+ (built-in fetch, no packages). Save as quickstart.mjs and run: node quickstart.mjs
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
const API = "https://api.makernice.com/v1";
const ORIGIN = "https://api.makernice.com";
const HEADERS = { Authorization: `Bearer ${process.env.MAKERNICE_API_KEY}` };
const TIMEOUT_MS = 30_000;
const POLL_INTERVAL_MS = 5_000;
const MAX_WAIT_MS = 30 * 60 * 1000;
const FINAL = new Set(["succeeded", "failed", "canceled"]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// micro-USD integer string -> dollars, no float rounding
const usd = (micro) => `${BigInt(micro) / 1_000_000n}.${(BigInt(micro) % 1_000_000n).toString().padStart(6, "0")}`;
class Stop extends Error {}
async function call(method, path, { body, headers } = {}) {
// Waits out 429 (rate_limited, concurrency_limit_reached) using Retry-After; throws on any other error.
for (let attempt = 0; attempt < 5; attempt++) {
const r = await fetch(API + path, { signal: AbortSignal.timeout(TIMEOUT_MS),
method,
headers: { ...HEADERS, ...(body ? { "Content-Type": "application/json" } : {}), ...headers },
body: body ? JSON.stringify(body) : undefined,
});
if (r.status === 429) {
await sleep(Number(r.headers.get("Retry-After") ?? "5") * 1000);
continue;
}
if (!r.ok) throw new Stop(`${method} ${path} -> ${r.status}: ${await r.text()}`);
return r.json();
}
throw new Stop(`${method} ${path}: still rate limited`);
}
async function main() {
const job = {
"model": "seedance-2.5",
"mode": "t2v",
"prompt": "A slow dolly shot through a greenhouse",
"duration": 5,
"resolution": "720p",
};
// 1. Quote: the exact amount, locked for 12 minutes
const quote = await call("POST", "/generations/quote", { body: job });
console.log(`Quoted $${usd(quote.amount)} (price list v${quote.price_version}), valid until ${quote.expires_at}`);
// 2. Create: same input + quote_id. A retry with the same Idempotency-Key returns the same generation
const idempotencyKey = randomUUID();
let gen = await call("POST", "/generations", { body: { quote_id: quote.id, input: job }, headers: { "Idempotency-Key": idempotencyKey } });
// 3. Poll: queued -> generating -> moderating -> succeeded | failed | canceled (checking: keep polling)
const deadline = Date.now() + MAX_WAIT_MS;
while (!FINAL.has(gen.status)) {
if (Date.now() > deadline) throw new Stop(`${gen.id} is still ${gen.status}; poll it again later`);
await sleep(POLL_INTERVAL_MS);
gen = await call("GET", `/generations/${gen.id}`);
}
if (gen.status !== "succeeded") {
// billing is "released": the hold went back to your balance
throw new Stop(`${gen.status}, billing ${gen.billing}: ${JSON.stringify(gen.error)}`);
}
// 4. Download: outputs[].url is relative to the API origin and answers 302 with a signed file URL.
// Follow it yourself so your API key is never sent to the file host.
const [output] = gen.outputs;
if (output.state === "blocked") throw new Stop("Output blocked by moderation (the run is charged); contact support for a manual review");
const hop = await fetch(ORIGIN + output.url, { headers: HEADERS, redirect: "manual", signal: AbortSignal.timeout(TIMEOUT_MS) });
const location = hop.headers.get("Location");
if (hop.status < 300 || hop.status > 399 || !location) throw new Stop(`GET ${output.url} -> ${hop.status}`);
const file = await fetch(new URL(location, ORIGIN + output.url), { signal: AbortSignal.timeout(120_000) });
if (!file.ok) throw new Stop(`download -> ${file.status}`);
await writeFile("output.mp4", Buffer.from(await file.arrayBuffer()));
console.log(`Saved output.mp4, charged $${usd(gen.amount)}`);
}
main().catch((err) => {
console.error(err instanceof Stop ? err.message : err);
process.exit(1);
});Para ejecutar varias generaciones a la vez, inícialas en paralelo (por ejemplo con un pool de hilos, asyncio con un cliente HTTP asíncrono o Promise.all). Cada cuenta tiene por defecto 2 generaciones en curso al mismo tiempo; al pasar de ahí, la API responde 429 concurrency_limit_reached con Retry-After, y call() en ambos ejemplos espera ese tiempo. Contacta al soporte para aumentar el límite.
Autenticación
Cada solicitud necesita una clave de API en el encabezado Authorization, excepto GET /v1/models y GET /v1/pricing. Las claves se ven como mn- seguido de 48 letras y dígitos.
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY"- La clave completa se muestra una sola vez, al crearla. Solo guardamos una huella, así que una clave perdida no se puede recuperar: crea una nueva y elimina la anterior.
- Una clave ausente, con formato incorrecto, desconocida, desactivada, vencida o eliminada siempre recibe la misma respuesta
401 invalid_api_key. Desactivar o eliminar una clave tiene efecto en la siguiente solicitud. - Llama a la API desde tu servidor. No acepta solicitudes de navegador de otro origen (sin CORS); nunca pongas una clave en el código del front-end.
- Todas las claves de una cuenta ven las mismas generaciones: cualquier clave puede consultar, listar y cancelar todas las generaciones de la cuenta, incluidas las creadas en la consola.
- Todas las claves de una cuenta usan el mismo saldo prepagado. Hoy una cuenta de Makernice tiene un solo inicio de sesión; los equipos comparten un saldo mediante varias claves de API.
- La página Claves de API muestra el gasto total de cada clave, y la página Uso desglosa el gasto de la API por clave y modelo.
- En la consola puedes darle a una clave una fecha de vencimiento y un límite de gasto. El gasto de una clave es lo cobrado más lo retenido en este momento de las generaciones que creó; una solicitud de creación que rebasaría el límite recibe
402 key_spend_limit_reached. Las generaciones fallidas y canceladas no cuentan.
Precios y facturación
- El cobro es en dólares estadounidenses. Todos los montos son cadenas de enteros en micro-USD (millonésimas de dólar):
1,000,000= $1.00. Analízalos como enteros o decimales, no como números de punto flotante. - Los videos tienen precio por segundo (la cantidad es la duración solicitada; en una plantilla con un clip de video de referencia incluye los segundos del clip) y las imágenes por solicitud (una imagen por generación). Precio = precio unitario × cantidad.
- Una cotización deja el precio fijo por 12 minutos (
expires_at). La generación se cobra con el monto cotizado, aunque los precios cambien mientras tanto.
GET /v1/pricing lista los precios unitarios de la versión vigente de la lista de precios. No necesita clave de API y puede quedar en caché hasta 60 segundos. official_unit_price, cuando aparece, es el precio de lista oficial de la misma combinación, solo para comparar; siempre se te cobra unit_price.
{
"price_version": 3,
"data": [
{ "model": "seedance-2.5", "mode": "t2v", "resolution": "720p", "unit": "second", "unit_price": "100000", "unit_price_usd": "0.10", "official_unit_price": "120000" }
]
}¿Se cobran las generaciones fallidas?
No. La retención se libera por completo: nunca se te cobra una generación fallida. Nota: si un resultado terminado es bloqueado por la moderación del resultado por infringir la política, la ejecución se cobra de todos modos; puedes pedir una revisión manual al soporte.
Cuando creas una generación, retenemos el monto cotizado. La retención se convierte en cobro cuando la ejecución termina; las generaciones fallidas y las que agotan el tiempo la liberan por completo, y cancelar mientras está en cola también. Una retención liberada no es un reembolso: el dinero nunca salió de tu saldo.
| Qué pasó | billing | ¿Se cobra? |
|---|---|---|
| La generación termina con éxito | charged | Sí, el monto cotizado |
La generación falla (error.code generation_failed, upstream_unavailable o invalid_request) | released | No |
Rebasa su tiempo máximo de ejecución (error.code: timeout) | released | No |
La cancelas mientras está en queued | released | No |
El prompt se bloquea al crearla (422 content_rejected, reason: prompt) | — | No; no se crea ninguna generación y no se retiene nada |
Una imagen de entrada o la política de seguridad del proveedor del modelo la rechaza (content_rejected, reason input_image o model) | released | No |
La moderación del resultado bloquea el resultado terminado (state: blocked en el resultado) | charged | Sí; pide una revisión manual al soporte |
Se está confirmando el resultado (status: checking) | held | Solo si se confirma que terminó con éxito |
Obtener una cotización
Envía la entrada de la generación; la respuesta te dice exactamente cuánto costará la generación. Cada cotización se puede usar para una sola generación.
| Campo | Tipo | Descripción |
|---|---|---|
model | string | Obligatorio. Un id de modelo de GET /v1/models. |
mode | string | Obligatorio. t2v, i2v, flf, r2v, t2i o i2i; cada modelo indica los modos que admite. |
prompt | string | Obligatorio. De 1 a 4,000 caracteres por defecto; algunos modelos permiten menos (consulta maxPromptChars en GET /v1/models). |
duration | integer | Solo video. Segundos, dentro del rango del modelo; por defecto, el valor predeterminado del modelo. |
resolution | string | Uno de los valores de resolutions del modelo; por defecto, defaultResolution. No se permite si el modelo no lista ninguno. |
aspect_ratio | string | Uno de los valores de aspectRatios del modo. |
image_url | string | Primer fotograma. Obligatorio para i2v y flf. |
last_image_url | string | Último fotograma. Obligatorio para flf. |
reference_images | string[] | Obligatorio para r2v e i2i; como máximo el maxReferences del modo. |
seed | integer | Opcional. De 0 a 2,147,483,647. La misma semilla con las mismas opciones da resultados más parecidos. |
generate_audio | boolean | Opcional. Si el video lleva pista de sonido. |
prompt_expansion | string | Opcional. off, balanced o quality: cuánto puede reescribir el modelo tu prompt. |
negative_prompt | string | Opcional. Hasta 2,000 caracteres que describen lo que hay que evitar. |
watermark | boolean | Opcional. Agrega la marca de agua de contenido generado por IA del proveedor. |
Los últimos cinco campos son opciones extra. Cada modelo admite solo algunas: GET /v1/models lista las disponibles para cada modo y resolución en prices[].options, con los valores permitidos y los predeterminados. Un campo que ninguna ruta de modelo puede aplicar devuelve 400 invalid_request; cambiar un campo opcional requiere una nueva cotización, igual que cualquier otro campo.
Las imágenes son URL https públicas (hasta 2,048 caracteres). Los campos que el modo no usa y los campos desconocidos se rechazan con 400 invalid_request; en los campos de entrada, error.details lista cada problema como { path, message } (otros errores invalid_request pueden tener otra forma de details o no tenerla). Una combinación sin precio devuelve 422 price_unavailable.
Cada imagen debe ser una URL https accesible públicamente que sirva una imagen fija JPEG, PNG o WebP (no se admiten WebP animados ni GIF), de 10 MB como máximo, directamente en esa URL: no se siguen redirecciones. Descargamos cada imagen una vez y guardamos una copia; la moderación de contenido y el modelo usan esa copia. Si una imagen no se puede usar, la generación falla con invalid_request, error.input indica cuál es la imagen y la retención se libera.
curl https://api.makernice.com/v1/generations/quote \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"seedance-2.5","mode":"t2v","prompt":"Un travelling lento a través de un invernadero","duration":5,"resolution":"720p"}'{
"id": "qt_6f1c…",
"amount": "500000",
"price_version": 3,
"expires_at": "2026-10-09T09:24:41.000Z",
"facts": { "model": "seedance-2.5", "mode": "t2v", "resolution": "720p", "variant": "default", "unit": "second", "quantity": 5 }
}¿Por cuánto tiempo fija el precio una cotización?
Cada cotización deja su precio fijo por 12 minutos, y una generación siempre se cobra exactamente lo que se cotizó.
| Campo o caso | Qué significa |
|---|---|
expires_at | 12 minutos después de crear la cotización. Crea la generación antes de ese momento. |
amount | Precio unitario × facts.quantity (segundos en video, 1 en imagen), en micro-USD. Se retiene al crear la generación y se cobra si termina con éxito. |
price_version | La lista de precios de la que salió la cotización. Si se publica una lista nueva antes de que crees la generación, pagas igualmente el monto cotizado. |
| Pasados 12 minutos | Crear con la cotización devuelve 410 quote_expired. No se retuvo nada; obtén una cotización nueva. |
| Cambió el modelo, el modo, la resolución o la duración | 422 quote_mismatch. Si solo cambias el prompt, la cotización se conserva. |
| La cotización ya se usó | 409 quote_already_used. Cada cotización sirve para una sola generación. |
Crear una generación
Crea la generación con la cotización y la misma entrada. El encabezado Idempotency-Key es obligatorio (consulta Idempotencia).
curl https://api.makernice.com/v1/generations \
-H "Authorization: Bearer $MAKERNICE_API_KEY" \
-H "Idempotency-Key: 7c1e9a2b-order-1842" \
-H "Content-Type: application/json" \
-d '{"quote_id":"qt_6f1c…","input":{"model":"seedance-2.5","mode":"t2v","prompt":"Un travelling lento a través de un invernadero","duration":5,"resolution":"720p"}}'- Responde
202con la generación (status: queued,billing: held). Un reintento con el mismo Idempotency-Key responde200con la generación original. - Puedes cambiar el prompt, pero cambiar cualquier cosa que afecte el precio (modelo, modo, resolución, duración) devuelve
422 quote_mismatch. - Una cotización ya usada devuelve
409 quote_already_used, una expirada410 quote_expiredy una que no está en tu cuenta404 quote_not_found. - Si no tienes saldo suficiente, se devuelve
402 insufficient_balance; si se rebasa el límite de gasto de la clave,402 key_spend_limit_reached. callback_urlopcional en el nivel superior (https, hasta 2048 caracteres, sin credenciales en la URL, no una dirección privada ni reservada): cuando la generación termina, la enviamos ahí por POST. Tiene prioridad sobre la dirección de webhook de tu cuenta. Consulta Webhooks.
Consultar el estado
Consulta la generación hasta que llegue a un estado final. Consultar cada pocos segundos está bien; revisa el límite de lectura en Límites de solicitudes. Una generación de video puede tardar unos minutos, y más si el clip es largo o de alta resolución. Si una ejecución rebasa su tiempo máximo, falla y la retención se libera por completo.
| Estado | Significado |
|---|---|
queued | Esperando a empezar. Todavía se puede cancelar. |
generating | El modelo está trabajando en ella. |
moderating | La generación terminó y el monto se cobró; los resultados están en revisión de seguridad, y outputs sigue vacío hasta que termine. Sigue consultando. |
succeeded | Lista. El monto se cobró y los outputs están listos. |
failed | No terminó. La retención se libera; error tiene un código y un mensaje. |
canceled | Cancelada mientras estaba en cola. La retención se libera. |
checking | Estamos confirmando el resultado. El monto sigue retenido hasta que se resuelva. |
El error.code de una generación fallida es uno de estos: invalid_request (el modelo rechazó la solicitud; ajusta el prompt o las opciones), content_rejected (rechazada por la moderación de contenido o por la política de seguridad del proveedor del modelo; la retención se libera), timeout, upstream_unavailable o generation_failed. En content_rejected, error.reason indica qué se rechazó: prompt (el prompt), input_image (una imagen de referencia) o model (la política de seguridad del proveedor del modelo). El mismo reason aparece en la respuesta 422 cuando se bloquea un prompt enviado. Cuando invalid_request se debe a una imagen de referencia que no se puede usar, error.input la indica: image_url, last_image_url o reference_images[0] (índice desde cero). Reemplaza esa imagen y crea una nueva generación.
{
"id": "gen_3b9e…",
"type": "video",
"model": "seedance-2.5",
"mode": "t2v",
"status": "succeeded",
"prompt": "Un travelling lento a través de un invernadero",
"params": { "duration": 5, "resolution": "720p", "aspectRatio": null },
"amount": "500000",
"billing": "charged",
"price_version": 3,
"outputs": [
{
"index": 0,
"url": "/v1/generations/gen_3b9e…/outputs/0",
"state": "stored",
"contentType": "video/mp4",
"expiresAt": "2026-11-08T09:14:03.000Z"
}
],
"error": null,
"created_at": "2026-10-09T09:12:41.000Z",
"finished_at": "2026-10-09T09:14:03.000Z"
}90 días después de que termina una generación, el contenido de su solicitud se borra según nuestra política de retención de datos: prompt pasa a ser una cadena vacía. Los datos de cobro (model, params, amount, status y las marcas de tiempo) se conservan.
GET /v1/generations/{id}/events devuelve el historial de la generación. Códigos: created, submitted, generating, delivered, failed_refunded, canceled_refunded, checking, requeued.
{
"data": [
{ "code": "created", "at": "2026-10-09T09:12:41.000Z" },
{ "code": "submitted", "at": "2026-10-09T09:12:43.000Z" },
{ "code": "generating", "at": "2026-10-09T09:12:50.000Z" },
{ "code": "delivered", "at": "2026-10-09T09:14:03.000Z" }
]
}Descargar resultados
Cada elemento de outputs tiene una url como /v1/generations/gen_…/outputs/0, relativa a https://api.makernice.com. Pídela con tu clave de API; la respuesta es una redirección 302 a un enlace de corta duración al archivo. Vuelve a pedir el endpoint cada vez que necesites el archivo, en lugar de guardar el destino de la redirección.
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
-H "Authorization: Bearer $MAKERNICE_API_KEY"- Agrega
?download=1para que el archivo se sirva como descarga adjunta. Funciona cuando el archivo está enstored; mientras está enpendingodegraded, la redirección va al archivo original y el parámetro no tiene efecto. curl -Lno envía tu encabezadoAuthorizational host de la redirección, así que tu clave se queda con nosotros.- Los resultados solo existen cuando la generación está en
succeeded; si no, el endpoint devuelve404 output_not_found. state:pending(todavía se está guardando; el enlace ya funciona),stored(guardado hastaexpiresAt),degraded(el guardado no se completó; el enlace puede dejar de funcionar antes, descárgalo pronto),expired(el archivo guardado expiró; el endpoint devuelve410 output_expired),blocked(el resultado no pasó la moderación de contenido;urles una cadena vacía; contacta al soporte para pedir una revisión manual).
Cancelar
Solo se pueden cancelar las generaciones en queued; la retención se libera por completo y la respuesta es la generación cancelada. Cuando una generación ya está en generating no se puede detener, y cancelar devuelve 409 not_cancelable.
curl -X POST https://api.makernice.com/v1/generations/gen_…/cancel \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# → { "id": "gen_…", "status": "canceled", "billing": "released", … }Listar generaciones
Lista las generaciones de tu cuenta, de la más reciente a la más antigua, incluidas las creadas en la consola.
| Query | Descripción |
|---|---|
limit | Tamaño de página, de 1 a 100. Predeterminado: 20. |
cursor | El next_cursor de la página anterior. |
status | Uno o varios estados, separados por comas, p. ej. queued,generating. |
type | video o image. |
model | Un id de modelo. |
q | Parte de un id de generación o texto contenido en el prompt (sin distinguir mayúsculas y minúsculas). |
curl "https://api.makernice.com/v1/generations?status=queued,generating&limit=50" \
-H "Authorization: Bearer $MAKERNICE_API_KEY"
# → { "data": [ …generaciones… ], "next_cursor": "gen_…" | null, "total": 12 }total cuenta todas las generaciones que coinciden con los filtros. Cuando next_cursor es null, llegaste a la última página.
Webhooks
En lugar de consultar el estado, puedes hacer que enviemos la generación por POST a tu servidor cuando llegue a un estado final. La enviamos al callback_url indicado al crear la generación; si no hay, a la dirección de webhook de tu cuenta (configúrala en la configuración de la consola). Si no hay ninguna de las dos, no se envía nada.
| Evento | Se envía cuando |
|---|---|
generation.succeeded | La generación terminó con éxito y se cobró. |
generation.failed | La generación falló; el monto retenido se liberó. |
generation.canceled | La generación se canceló; el monto retenido se liberó. |
Solicitud
POST /your/webhook/path HTTP/1.1
Content-Type: application/json
Makernice-Signature: t=1791364443,v1=5f0c2d…e81a
Makernice-Event-Id: evt_1b7f…{
"id": "evt_1b7f…",
"type": "generation.succeeded",
"created_at": "2026-10-09T09:14:03.000Z",
"data": {
"id": "gen_3b9e…",
"status": "succeeded",
"billing": "charged",
"outputs": [
{ "index": 0, "url": "https://api.makernice.com/v1/generations/gen_3b9e…/outputs/0", "state": "stored", … }
],
…
}
}dataes el mismo objeto de generación que devuelveGET /v1/generations/{id}, salvo queoutputs[].urles una URL absoluta.Makernice-Event-Ides igual aliddel cuerpo y no cambia entre reintentos. Úsalo para ignorar duplicados.
Verifica la firma
Makernice-Signature es t=<segundos unix>,v1=<hex>, donde v1 es HMAC-SHA256 con tu secreto de firma sobre t + "." + cuerpo sin procesar. Calcúlalo sobre los bytes exactos que recibiste, compara en tiempo constante y rechaza las solicitudes cuyo t difiera más de 5 minutos de tu reloj. El secreto de firma está en la configuración de la consola; los envíos a callback_url se firman con el mismo secreto. Después de rotarlo, los envíos que aún no salen se firman con el nuevo.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the body exactly as received (Buffer). Don't re-serialize parsed JSON.
export function verifyWebhook(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => [p.slice(0, p.indexOf("=")), p.slice(p.indexOf("=") + 1)]));
const t = Number(parts.t);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}import hashlib
import hmac
import time
# raw_body: the body exactly as received (bytes). Don't re-serialize parsed JSON.
def verify_webhook(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance_seconds:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Entrega y reintentos
- Solo HTTPS. No se siguen redirecciones. Cada intento tiene un tiempo límite de 10 segundos.
- Cualquier respuesta 2xx cuenta como entregada. Responde rápido y haz el trabajo pesado después.
- Si no, reintentamos después de 1 min, 5 min, 30 min, 2 h, 6 h, 12 h (7 intentos en total) y luego marcamos el envío como fallido. Los envíos recientes y su última respuesta se muestran en la configuración de la consola.
- Normalmente cada generación envía un evento, y un envío puede llegar más de una vez (por ejemplo, después de un tiempo agotado de tu lado). Si un resultado retenido por la revisión de contenido se libera más tarde, volvemos a enviar la generación con un nuevo id de evento, así que puedes recibir dos eventos para ella; vale el más reciente.
Modelos
GET /v1/models lista los modelos y los modos, resoluciones y duraciones que puedes usar. Solo se listan los modos que tienen precio, con sus precios en modes[].prices. No necesita clave de API y puede quedar en caché hasta 60 segundos.
resolutions/defaultResolution: valores permitidos pararesolution. Una lista vacía significa que el modelo tiene una resolución fija y no debes enviarresolution(si lo envías, aunque sea con el valor fijo, recibes400 invalid_request). El nivel fijo es el que aparece enmodes[].prices[].resolution: por ejemplo,seedance-2.0-fastsiempre genera en 720p. Una resolución listada puede no tener precio en algunos modos: revisamodes[].prices; cotizarla devuelve422 price_unavailable.durations:{ min, max, default }en segundos para videos,nullpara imágenes.modes[].needs: qué imágenes requiere el modo (image→image_url,lastImage→last_image_url,references→reference_images, hastamaxReferences).modes[].aspectRatios: valores permitidos deaspect_ratio.modes[].prices: precio unitario por resolución.
{
"data": [
{
"id": "seedance-2.5",
"type": "video",
"resolutions": ["480p", "720p", "1080p", "4k"],
"defaultResolution": "720p",
"durations": { "min": 4, "max": 30, "default": 5 },
"modes": [
{
"mode": "t2v",
"needs": { "image": false, "lastImage": false, "references": false },
"maxReferences": null,
"aspectRatios": ["adaptive", "16:9", "9:16", "1:1", "4:3", "3:4", "21:9", "9:21"],
"prices": [{ "resolution": "720p", "unit": "second", "unitPrice": "100000" }]
}
]
}
]
}¿Qué modelos puedo llamar?
Una sola clave de API llama a los 17 modelos: 13 de video y 4 de imagen de ByteDance, Kuaishou, MiniMax y Alibaba, entre ellos Seedance, Kling, Wan y MiniMax H3. Todos usan el mismo formato de solicitud; define model con el id de modelo de la API que aparece abajo.
| Modelo | Proveedor | Id de modelo de la API | Tipo |
|---|---|---|---|
| Seedance 2.5 | ByteDance | seedance-2.5 | Video |
| Seedance 2.0 | ByteDance | seedance-2.0 | Video |
| Seedance 2.0 Fast | ByteDance | seedance-2.0-fast | Video |
| MiniMax H3 | MiniMax | minimax-h3 | Video |
| MiniMax H3 Max | MiniMax | minimax-h3-max | Video |
| MiniMax H3 Max Turbo | MiniMax | minimax-h3-max-turbo | Video |
| Wan 3.0 | Alibaba | wan-3.0 | Video |
| Wan 3.0 Prime | Alibaba | wan-3.0-prime | Video |
| Kling 3.0 Pro | Kuaishou | kling-3.0-pro | Video |
| Kling 3.0 Standard | Kuaishou | kling-3.0-standard | Video |
| Kling 3.0 4K | Kuaishou | kling-3.0-4k | Video |
| Kling 4.0 | Kuaishou | kling-4.0 | Video |
| Happy Horse 1.1 | Alibaba | happy-horse-1.1 | Video |
| Seedream 5.0 Pro | ByteDance | seedream-5.0-pro | Imagen |
| Seedream 5.0 | ByteDance | seedream-5.0 | Imagen |
| Qwen3 Image | Alibaba | qwen3-image | Imagen |
| Qwen3 Pro Image | Alibaba | qwen3-pro-image | Imagen |
Modelos por tarea, con los modos compatibles y precios de ejemplo (páginas en inglés): image to video (en inglés), AI video generator (en inglés) y AI image generator (en inglés).
Errores
Los errores son JSON: { "error": { "code", "message", "reason"?, "details"?, "request_id" } }. Decide según code; los mensajes pueden cambiar. Cada respuesta lleva un encabezado x-request-id (con el mismo valor que request_id); inclúyelo cuando contactes al soporte para que podamos encontrar la solicitud.
| Estado | Código | Qué hacer |
|---|---|---|
| 400 | invalid_request | El cuerpo, la query o un encabezado no es válido. details indica cada campo y el problema; corrige la solicitud. |
| 400 | template_conflict | Una solicitud con plantilla solo acepta template, images y, de forma opcional, prompt, resolution y aspect_ratio; la plantilla fija el modelo, el modo, la duración y las demás opciones. Quita los demás campos. |
| 401 | invalid_api_key | Falta la clave, tiene un formato incorrecto, o está desactivada, vencida o eliminada. |
| 402 | insufficient_balance | Recarga saldo en tu billetera. |
| 402 | key_spend_limit_reached | Aumenta el límite de gasto de la clave o usa otra clave. |
| 402 | daily_spend_limit_reached | Tu cuenta llegó a su límite de gasto diario; se reinicia a las 00:00 UTC. Contacta al soporte para aumentarlo. |
| 403 | org_frozen | Tu cuenta está congelada. Contacta al soporte. |
| 404 | quote_not_found | La cotización no existe en tu cuenta. Obtén una nueva cotización. |
| 404 | task_not_found | La generación no existe en tu cuenta. Revisa el id. |
| 404 | output_not_found | La generación no ha terminado con éxito o no tiene un resultado con ese índice. |
| 404 | template_not_found | No hay ninguna plantilla con este id. Consulta las plantillas disponibles con GET /v1/templates. |
| 404 | not_found | Endpoint desconocido. Revisa el método y la ruta. |
| 409 | quote_already_used | Obtén una nueva cotización. |
| 409 | not_cancelable | Solo se pueden cancelar las generaciones en cola. |
| 410 | quote_expired | Obtén una nueva cotización. |
| 410 | output_expired | El archivo guardado expiró y ya no se puede descargar. |
| 410 | template_retired | Esta plantilla ya no está disponible. Elige otra en GET /v1/templates; las generaciones que ya hiciste no se ven afectadas. |
| 413 | payload_too_large | El cuerpo de la solicitud pesa más de 64 KB. Acorta el prompt o envía menos URL de imágenes. |
| 422 | idempotency_conflict | Este Idempotency-Key se usó para otra solicitud. Usa un valor nuevo. |
| 422 | quote_mismatch | La entrada cambia el precio (modelo, modo, resolución o duración). Vuelve a cotizar esta entrada. |
| 422 | price_unavailable | Esta combinación no tiene precio. Elige una de las que lista GET /v1/models. |
| 422 | content_rejected | Rechazado por la moderación de contenido o por la política de seguridad del proveedor del modelo; error.reason indica cuál (prompt, input_image o model). Cambia el prompt o las imágenes; no se te cobró. |
| 429 | rate_limited | Espera los segundos que indica Retry-After y vuelve a intentarlo. |
| 429 | concurrency_limit_reached | Tu cuenta tiene demasiadas generaciones en curso. Espera a que termine una (consulta Retry-After) o contacta al soporte para aumentar el límite. |
| 500 | internal_error | Error inesperado del servidor. Reintenta con espera progresiva (backoff); incluye el request_id si contactas al soporte. |
Ante respuestas 5xx inesperadas o errores de red, reintenta con espera progresiva (backoff). Al reintentar una solicitud de creación, reutiliza el mismo Idempotency-Key para que nunca se te cobre dos veces.
Límites de solicitudes
Los límites son por minuto. Si los rebasas, recibes 429 rate_limited con un encabezado Retry-After (en segundos).
| Ámbito | Límite |
|---|---|
| Por clave: escrituras (cotizar, crear, cancelar) | 60 / min |
| Por clave: lecturas (consultar, listar, eventos, resultados) | 600 / min |
| Por cuenta: escrituras sumando todas las claves | 120 / min |
| Por IP: modelos, precios y solicitudes con una clave rechazada | 120 / min |
Además, una clave puede tener como máximo 4 solicitudes de creación en curso al mismo tiempo; las que pasen de ahí también reciben 429. Los límites pueden ajustarse.
Cada cuenta tiene además dos límites de uso: por defecto, 2 generaciones en curso al mismo tiempo y $100 de gasto por día UTC. Si los rebasas, recibes 429 concurrency_limit_reached (con Retry-After) o 402 daily_spend_limit_reached. Contacta al soporte para aumentarlos. Una generación cuenta para el límite de en curso mientras está en queued, generating o checking, sin importar qué modelo o clave la creó. Deja de contar cuando llega a moderating, succeeded, failed o canceled. Las generaciones que inicias en la app web cuentan para el mismo límite.
Idempotencia
POST /v1/generationsrequiere un encabezadoIdempotency-Key: de 1 a 200 caracteres ASCII imprimibles, sin espacios. Usa un valor nuevo, como un UUID, para cada nueva generación.- Los valores son únicos dentro de tu cuenta y los comparten todas sus claves de API.
- Repetir una solicitud con el mismo valor, la misma cotización y la misma entrada devuelve la generación original con
200y no vuelve a cobrar. - Usar el mismo valor con otro
quote_id, otrainput(incluidos el prompt y las URL de imágenes) u otrocallback_urldevuelve422 idempotency_conflict. - Las cotizaciones y las cancelaciones no llevan Idempotency-Key. Repetir una cancelación que ya se completó devuelve
409 not_cancelable; revisa el estado conGET.
Preguntas frecuentes
¿Una cotización cuesta algo?
POST /v1/generations/quote solo calcula el precio de tu entrada; no se retiene ni se cobra nada hasta que creas una generación con esa cotización.¿Necesito una clave de API distinta para cada modelo?
model de la solicitud.