Makernice

Documentación de la API de Makernice: inicio rápido

Equipo de Makernice ·

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

POSThttps://api.makernice.com/v1/generations/quote
curl
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

POSThttps://api.makernice.com/v1/generations
curl
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

GEThttps://api.makernice.com/v1/generations/{id}
curl
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 archivo

5. Descarga el archivo

curl
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:

Python (requests)
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 (fetch)
// 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
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.

GEThttps://api.makernice.com/v1/pricing
Ejemplo de respuesta (valores ilustrativos)
{
  "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 éxitochargedSí, el monto cotizado
La generación falla (error.code generation_failed, upstream_unavailable o invalid_request)releasedNo
Rebasa su tiempo máximo de ejecución (error.code: timeout)releasedNo
La cancelas mientras está en queuedreleasedNo
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)releasedNo
La moderación del resultado bloquea el resultado terminado (state: blocked en el resultado)chargedSí; pide una revisión manual al soporte
Se está confirmando el resultado (status: checking)heldSolo si se confirma que terminó con éxito

Ejemplos de cobro

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.

POSThttps://api.makernice.com/v1/generations/quote
CampoTipoDescripción
modelstringObligatorio. Un id de modelo de GET /v1/models.
modestringObligatorio. t2v, i2v, flf, r2v, t2i o i2i; cada modelo indica los modos que admite.
promptstringObligatorio. De 1 a 4,000 caracteres por defecto; algunos modelos permiten menos (consulta maxPromptChars en GET /v1/models).
durationintegerSolo video. Segundos, dentro del rango del modelo; por defecto, el valor predeterminado del modelo.
resolutionstringUno de los valores de resolutions del modelo; por defecto, defaultResolution. No se permite si el modelo no lista ninguno.
aspect_ratiostringUno de los valores de aspectRatios del modo.
image_urlstringPrimer fotograma. Obligatorio para i2v y flf.
last_image_urlstringÚltimo fotograma. Obligatorio para flf.
reference_imagesstring[]Obligatorio para r2v e i2i; como máximo el maxReferences del modo.
seedintegerOpcional. De 0 a 2,147,483,647. La misma semilla con las mismas opciones da resultados más parecidos.
generate_audiobooleanOpcional. Si el video lleva pista de sonido.
prompt_expansionstringOpcional. off, balanced o quality: cuánto puede reescribir el modelo tu prompt.
negative_promptstringOpcional. Hasta 2,000 caracteres que describen lo que hay que evitar.
watermarkbooleanOpcional. 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
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"}'
Ejemplo de respuesta (valores ilustrativos)
{
  "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 casoQué significa
expires_at12 minutos después de crear la cotización. Crea la generación antes de ese momento.
amountPrecio 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_versionLa 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 minutosCrear 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ón422 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).

POSThttps://api.makernice.com/v1/generations
curl
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 202 con la generación (status: queued, billing: held). Un reintento con el mismo Idempotency-Key responde 200 con 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 expirada 410 quote_expired y una que no está en tu cuenta 404 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_url opcional 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.

GEThttps://api.makernice.com/v1/generations/{id}
EstadoSignificado
queuedEsperando a empezar. Todavía se puede cancelar.
generatingEl modelo está trabajando en ella.
moderatingLa 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.
succeededLista. El monto se cobró y los outputs están listos.
failedNo terminó. La retención se libera; error tiene un código y un mensaje.
canceledCancelada mientras estaba en cola. La retención se libera.
checkingEstamos 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.

Ejemplo de respuesta (valores ilustrativos)
{
  "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.

GEThttps://api.makernice.com/v1/generations/{id}/events
Ejemplo de respuesta (valores ilustrativos)
{
  "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.

GEThttps://api.makernice.com/v1/generations/{id}/outputs/{n}
curl
curl -L -o video.mp4 https://api.makernice.com/v1/generations/gen_…/outputs/0 \
  -H "Authorization: Bearer $MAKERNICE_API_KEY"
  • Agrega ?download=1 para que el archivo se sirva como descarga adjunta. Funciona cuando el archivo está en stored; mientras está en pending o degraded, la redirección va al archivo original y el parámetro no tiene efecto.
  • curl -L no envía tu encabezado Authorization al 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 devuelve 404 output_not_found.
  • state: pending (todavía se está guardando; el enlace ya funciona), stored (guardado hasta expiresAt), degraded (el guardado no se completó; el enlace puede dejar de funcionar antes, descárgalo pronto), expired (el archivo guardado expiró; el endpoint devuelve 410 output_expired), blocked (el resultado no pasó la moderación de contenido; url es 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.

POSThttps://api.makernice.com/v1/generations/{id}/cancel
curl
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.

GEThttps://api.makernice.com/v1/generations
QueryDescripción
limitTamaño de página, de 1 a 100. Predeterminado: 20.
cursorEl next_cursor de la página anterior.
statusUno o varios estados, separados por comas, p. ej. queued,generating.
typevideo o image.
modelUn id de modelo.
qParte de un id de generación o texto contenido en el prompt (sin distinguir mayúsculas y minúsculas).
curl
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.

EventoSe envía cuando
generation.succeededLa generación terminó con éxito y se cobró.
generation.failedLa generación falló; el monto retenido se liberó.
generation.canceledLa generación se canceló; el monto retenido se liberó.

Solicitud

Ejemplo de solicitud (valores ilustrativos)
POST /your/webhook/path HTTP/1.1
Content-Type: application/json
Makernice-Signature: t=1791364443,v1=5f0c2d…e81a
Makernice-Event-Id: evt_1b7f…
Ejemplo de cuerpo (valores ilustrativos)
{
  "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", … }
    ],
    …
  }
}
  • data es el mismo objeto de generación que devuelve GET /v1/generations/{id}, salvo que outputs[].url es una URL absoluta.
  • Makernice-Event-Id es igual al id del 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.

Node.js
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);
}
Python
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.

GEThttps://api.makernice.com/v1/models
  • resolutions / defaultResolution: valores permitidos para resolution. Una lista vacía significa que el modelo tiene una resolución fija y no debes enviar resolution (si lo envías, aunque sea con el valor fijo, recibes 400 invalid_request). El nivel fijo es el que aparece en modes[].prices[].resolution: por ejemplo, seedance-2.0-fast siempre genera en 720p. Una resolución listada puede no tener precio en algunos modos: revisa modes[].prices; cotizarla devuelve 422 price_unavailable.
  • durations: { min, max, default } en segundos para videos, null para imágenes.
  • modes[].needs: qué imágenes requiere el modo (image → image_url, lastImage → last_image_url, references → reference_images, hasta maxReferences).
  • modes[].aspectRatios: valores permitidos de aspect_ratio. modes[].prices: precio unitario por resolución.
Ejemplo de respuesta (valores ilustrativos)
{
  "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.

ModeloProveedorId de modelo de la APITipo
Seedance 2.5ByteDanceseedance-2.5Video
Seedance 2.0ByteDanceseedance-2.0Video
Seedance 2.0 FastByteDanceseedance-2.0-fastVideo
MiniMax H3MiniMaxminimax-h3Video
MiniMax H3 MaxMiniMaxminimax-h3-maxVideo
MiniMax H3 Max TurboMiniMaxminimax-h3-max-turboVideo
Wan 3.0Alibabawan-3.0Video
Wan 3.0 PrimeAlibabawan-3.0-primeVideo
Kling 3.0 ProKuaishoukling-3.0-proVideo
Kling 3.0 StandardKuaishoukling-3.0-standardVideo
Kling 3.0 4KKuaishoukling-3.0-4kVideo
Kling 4.0Kuaishoukling-4.0Video
Happy Horse 1.1Alibabahappy-horse-1.1Video
Seedream 5.0 ProByteDanceseedream-5.0-proImagen
Seedream 5.0ByteDanceseedream-5.0Imagen
Qwen3 ImageAlibabaqwen3-imageImagen
Qwen3 Pro ImageAlibabaqwen3-pro-imageImagen

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.

EstadoCódigoQué hacer
400invalid_requestEl cuerpo, la query o un encabezado no es válido. details indica cada campo y el problema; corrige la solicitud.
400template_conflictUna 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.
401invalid_api_keyFalta la clave, tiene un formato incorrecto, o está desactivada, vencida o eliminada.
402insufficient_balanceRecarga saldo en tu billetera.
402key_spend_limit_reachedAumenta el límite de gasto de la clave o usa otra clave.
402daily_spend_limit_reachedTu cuenta llegó a su límite de gasto diario; se reinicia a las 00:00 UTC. Contacta al soporte para aumentarlo.
403org_frozenTu cuenta está congelada. Contacta al soporte.
404quote_not_foundLa cotización no existe en tu cuenta. Obtén una nueva cotización.
404task_not_foundLa generación no existe en tu cuenta. Revisa el id.
404output_not_foundLa generación no ha terminado con éxito o no tiene un resultado con ese índice.
404template_not_foundNo hay ninguna plantilla con este id. Consulta las plantillas disponibles con GET /v1/templates.
404not_foundEndpoint desconocido. Revisa el método y la ruta.
409quote_already_usedObtén una nueva cotización.
409not_cancelableSolo se pueden cancelar las generaciones en cola.
410quote_expiredObtén una nueva cotización.
410output_expiredEl archivo guardado expiró y ya no se puede descargar.
410template_retiredEsta plantilla ya no está disponible. Elige otra en GET /v1/templates; las generaciones que ya hiciste no se ven afectadas.
413payload_too_largeEl cuerpo de la solicitud pesa más de 64 KB. Acorta el prompt o envía menos URL de imágenes.
422idempotency_conflictEste Idempotency-Key se usó para otra solicitud. Usa un valor nuevo.
422quote_mismatchLa entrada cambia el precio (modelo, modo, resolución o duración). Vuelve a cotizar esta entrada.
422price_unavailableEsta combinación no tiene precio. Elige una de las que lista GET /v1/models.
422content_rejectedRechazado 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ó.
429rate_limitedEspera los segundos que indica Retry-After y vuelve a intentarlo.
429concurrency_limit_reachedTu cuenta tiene demasiadas generaciones en curso. Espera a que termine una (consulta Retry-After) o contacta al soporte para aumentar el límite.
500internal_errorError 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).

ÁmbitoLímite
Por clave: escrituras (cotizar, crear, cancelar)60 / min
Por clave: lecturas (consultar, listar, eventos, resultados)600 / min
Por cuenta: escrituras sumando todas las claves120 / min
Por IP: modelos, precios y solicitudes con una clave rechazada120 / 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/generations requiere un encabezado Idempotency-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 200 y no vuelve a cobrar.
  • Usar el mismo valor con otro quote_id, otra input (incluidos el prompt y las URL de imágenes) u otro callback_url devuelve 422 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 con GET.

Preguntas frecuentes

¿Una cotización cuesta algo?
No. 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?
No. Una sola clave de API llama a los 17 modelos; para cambiar de modelo, cambia el campo model de la solicitud.