Técnica
Llamar la API de Vellria desde tu código
Cuatro llamadas te llevan de cero a un archivo terminado: crear una clave, iniciar una generación, consultar el registro y descargar el resultado. Aquí tienes ese ciclo de punta a punta, con el error que devuelve cada paso y lo que un cliente debería hacer con él.
Crea una clave y envíala como token Bearer
Las claves se crean en la Consola, en Claves de API, y esa respuesta es el único lugar donde aparece la clave completa: cada lectura posterior de tu lista devuelve un ID, un nombre y un prefijo, y nunca más el secreto. Guárdala en ese momento. El prefijo existe para que una línea de log pueda nombrar una clave sin contenerla.
Mándala en el encabezado Authorization de cada solicitud, como la palabra Bearer seguida de la clave. No hay variante en la query string: la RFC 6750 incluye entre sus recomendaciones que "Bearer tokens SHOULD NOT be passed in page URLs (for example, as query string parameters)", porque las URL que llevan tokens terminan en el historial del navegador, en los referrers y en los logs del servidor.
Si no envías clave, o envías una revocada, la respuesta es 401 con el código authentication_required. Todos los errores comparten un solo cuerpo: un objeto error con message, type, param y code. Ramifica según code; el message está escrito para una persona y su redacción va a cambiar.
Lee el catálogo en lugar de fijarlo en el código
GET /v1/models devuelve dos listas, image y video. Una entrada de imagen enumera los tamaños que acepta, lo que cuesta cada uno en créditos y un tamaño predeterminado; una de video trae resoluciones con un costo por segundo, una duración mínima y otra máxima, y las proporciones que admita. La lista de tamaños es de un modelo y no del catálogo, igual que el predeterminado, así que ningún valor único sirve para todos. Las páginas del catálogo de modelos y de precios leen este mismo endpoint.
El campo kind te dice qué forma de cuerpo espera un modelo: texto a imagen, imagen a imagen, texto a video, imagen a video. Úsalo en vez de deducirlo según si hay o no un objeto de imagen de referencia, un atajo que hoy acierta y mañana se equivoca en silencio con lo próximo que se agregue. Un modelo desconocido, un tamaño inválido o una duración fuera de rango vuelven como 400 con param nombrando el campo, y una duración fuera del rango se rechaza en vez de recortarse para que entre.
Inicia la generación y consulta el registro
Haz POST a /v1/generate/image o /v1/generate/video con un ID de modelo, un prompt y las opciones que ese modelo acepta. La respuesta es 202 y trae un id, el estado processing y cuántos créditos se acaban de descontar: los créditos salen al comenzar la generación, no al terminar. Después sigue leyendo GET /v1/generations/{id}; cuando status deja de ser queued o processing, la generación terminó. Si se completó, el output_url del registro apunta a nuestro endpoint de archivos; una fallida trae error_code, un valor de un conjunto cerrado, junto a error, un resumen breve pensado para tus logs.
Deja una espera entre consultas y mete azar en esa espera. El experimento de backoff de Marc Brooker en el AWS Architecture Blog simula clientes que se disputan una misma fila y reporta que, al sumar jitter, "in the case with 100 contending clients, we've reduced our call count by more than half". Consultar es la forma de enterarte del resultado, no la forma en que ocurre: las generaciones se concilian en el servidor aunque nadie esté mirando, así que una conexión caída te hace perder el hilo del resultado, no el resultado.
El ciclo completo en un solo archivo
Guarda esto como archivo .mjs, define VELLRIA_API_KEY y ejecútalo con un Node reciente. No lleva ningún ID de modelo, tamaño ni costo escrito a mano.
```js import { writeFile } from "node:fs/promises"; const BASE = "https://vellria.com"; const AUTH = { Authorization: `Bearer ${process.env.VELLRIA_API_KEY}` }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function call(path, init) { const res = await fetch(BASE + path, { ...init, headers: { ...AUTH, ...init?.headers } }); if (res.status === 429) { await sleep(Number(res.headers.get("retry-after") ?? 5) * 1000); return call(path, init); } if (!res.ok) throw new Error((await res.json().catch(() => null))?.error?.code ?? res.status); return res.json(); } const catalog = await call("/v1/models"); const model = catalog.image.find((m) => m.kind === "text-to-image"); let rec = await call("/v1/generate/image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: model.id, prompt: "a lighthouse in fog", size: model.default_size }), }); while (rec.status === "queued" || rec.status === "processing") { await sleep(2000 + Math.random() * 2000); rec = await call(`/v1/generations/${rec.id}`); } if (rec.status !== "completed") throw new Error(rec.error_code ?? "generation_failed"); const file = await fetch(rec.output_url, { headers: AUTH }); await writeFile(`${rec.id}.png`, Buffer.from(await file.arrayBuffer())); ```
Para un modelo que recibe imágenes, primero haz POST del archivo a /v1/uploads con un encabezado Content-Type que coincida con los bytes y después pasa la dirección que te devuelven en image_urls; convertir una imagen en video trae los formatos aceptados y la lista de rechazos. Que un modelo pida imágenes es una propiedad del modelo, y un cuerpo con la forma equivocada se rechaza en lugar de ajustarse en silencio, tema de mantener un personaje consistente. Los cuerpos de video llevan duración y resolución en vez de tamaño, como detalla elegir resolución y duración. Todos los ID de modelos de video, con su tipo y su precio por segundo, están en una sola tabla en la página de la API de video NSFW.
Fallos, contrapresión y vencimiento
Cuando una generación falla después de empezar, sus créditos vuelven solos al saldo en cuanto el registro pasa a failed; el historial guarda las dos entradas, el fallo y la reversión. Trata failed como un estado final y ya liquidado: ramifica según error_code para saber la causa y nunca concilies saldos por tu cuenta. El reembolso se escribe bajo una condición que solo un escritor puede cumplir, así que una notificación del proveedor entregada dos veces no puede acreditarte dos veces ni dejarte sin reembolso.
Pasarte de un límite de frecuencia devuelve 429 con el código rate_limit_exceeded y un encabezado Retry-After. La RFC 6585 define ese estado: "the 429 status code indicates that the user has sent too many requests in a given amount of time". La RFC 9110, en su sección 10.2.3, define el encabezado como el que indica cuánto esperar: "indicates how long the user agent ought to wait before making a follow-up request". Léelo en lugar de adivinar, y revisa storyboard y previsualización para ver qué implica cuando disparas un lote. Con saldo insuficiente la respuesta es 402 y no se crea ningún registro. El detalle completo está en la referencia de la API.
Preguntas frecuentes
¿Tengo que seguir consultando para que termine una generación?
No. Consultar es cómo te enteras, no cómo sucede. Las generaciones se concilian en nuestro servidor haya o no un cliente conectado, así que un trabajo lanzado por un proceso que después murió igual llega a completed o failed; recupéralo más tarde por su id.
¿Puedo llamarla desde código que corre en el navegador?
No con tu clave. Una clave gasta créditos y no se puede restringir a un alcance menor, así que cualquier cosa que envíes a un navegador es una clave que publicaste. Pon las llamadas detrás de tu propio servidor, guarda la clave en sus variables de entorno y deja que tu front end hable con él.
¿Puedo fijar en mi código los ID de modelo, los tamaños y los costos?
Puedes, y se van a desfasar. Del endpoint del catálogo salen las páginas de modelos y también lo que cobra el servidor, así que un valor copiado deja de coincidir el día que cambia el catálogo. Pídelo al arrancar y guárdalo en caché mientras viva el proceso.
