09Fundamentos de la API
Reintentos sin doble cargo
Si no estás seguro de que la solicitud llegó, envíala de nuevo con la misma clave de idempotencia: habrá una sola imagen y un solo cargo.
Este modelo no genera todas las resoluciones ni todas las relaciones de aspecto: los ejemplos usan las que tiene disponibles.
Para qué sirve la clave de idempotencia
La conexión puede cortarse después de que la solicitud nos llegó pero antes de que recibas la respuesta. No sabes si la solicitud se aceptó. Enviarla otra vez sin clave equivale a comprar una segunda imagen.
Sin clave
- Enviaste la solicitud: la conexión se cortó y no hay respuesta.
- La enviaste otra vez.
- Obtuviste dos imágenes y dos cargos.
Con clave de idempotencia
- Enviaste la solicitud con una clave: la conexión se cortó.
- Enviaste la misma solicitud con la misma clave.
- Obtuviste el mismo
idy un solo cargo.
Cómo usarla
- Envía el encabezado
X-Idempotency-Keyen la solicitud de generación. Sirve un UUID o el número de operación de tu sistema: una cadena no vacía de hasta 128 caracteres; si no,400 ERR_INVALID_REQUEST. - Una imagen nueva, una clave nueva. Repetir la misma solicitud, la clave anterior.
- Guarda la clave y el cuerpo exacto de la solicitud antes de enviarla: los necesitas para repetirla.
- La clave de idempotencia solo vale dentro de una misma clave de API. Otra clave de API, incluso de la misma cuenta, trata la operación como nueva.
- La clave no comprueba el cuerpo de la solicitud. Si envías otro prompt con la clave anterior, se devolverá la respuesta anterior y el prompt no cambiará nada. Para una operación nueva, usa una clave nueva.
Sin el encabezado no hay protección contra la ejecución repetida.
Qué devuelve un reintento
Si la primera solicitud ya se aceptó, repetirla con la misma clave devolverá la misma respuesta 202: los mismos id, status, task_id y cost. No habrá imagen nueva ni cargo nuevo.
{
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "queued",
"task_id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"cost": 0.04
}Es la respuesta guardada del envío, no el estado actual: puede seguir diciendo queued incluso cuando la imagen ya está lista. Toma siempre el estado actual de GET /creations/.
Ejemplo con reintentos
El código de abajo hace como máximo tres intentos, fija tiempos de espera, espera cada vez más y respeta Retry-After. La clave de idempotencia se crea una sola vez por operación y no cambia entre intentos.
# Hacen falta bash, curl y uuidgen. La clave se crea una sola vez por operación. IDEMPOTENCY_KEY="$(uuidgen)" || exit 1 curl --fail-with-body --max-time 30 \ -X POST https://api.rendergrid.io/ api/ public/ v1/ images/ generate \ -H "Authorization: Bearer $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \ -d '{ "model": "nano-banana-pro", "prompt": "Taza de cerámica sobre mármol, luz suave de ventana" }'
import os import random import time import requests API = "https://api.rendergrid.io/ api/ public/ v1" KEY = os.environ["RENDERGRID_API_KEY"] def submit( payload: dict, operation_key: str, max_attempts: int = 3 ) -> dict: headers = { "Authorization": f"Bearer {KEY}", "X-Idempotency-Key": operation_key, } for attempt in range(max_attempts): last = attempt + 1 == max_attempts delay = 2 ** (attempt + 1) + random.random() try: resp = requests.post( f"{API}/images/generate", headers=headers, json=payload, timeout=(5, 30), ) except (requests.Timeout, requests.ConnectionError): if last: raise else: if resp.status_code == 202: return resp.json() try: details = resp.json()["error"].get("details") or {} except (ValueError, KeyError, AttributeError): details = {} retryable = ( resp.status_code == 429 or resp.status_code >= 500 or details.get("retryable") is True ) blocked = details.get("reason") == "burst_block" if blocked or not retryable or last: raise RuntimeError(resp.status_code, resp.text) wait = resp.headers.get("Retry-After", "") if wait.isascii() and wait.isdigit(): delay = max(delay, int(wait)) time.sleep(delay) payload = { "model": "nano-banana-pro", "prompt": "Taza de cerámica sobre mármol, luz suave de ventana", } creation = submit(payload, operation_key="order-8412-cover-v1") print(creation["id"])
// Node.js 22+, archivo .mjs const API = "https://api.rendergrid.io/ api/ public/ v1"; const KEY = process.env.RENDERGRID_API_KEY; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function submit(payload, operationKey, maxAttempts = 3) { const body = JSON.stringify(payload); for (let attempt = 0; attempt < maxAttempts; attempt++) { const last = attempt + 1 === maxAttempts; let delay = (2 ** (attempt + 1) + Math.random()) * 1_000; let resp; try { resp = await fetch(`${API}/images/generate`, { method: "POST", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json", "X-Idempotency-Key": operationKey, }, body, signal: AbortSignal.timeout(30_000), }); } catch (error) { const network = error instanceof TypeError; if (!network && error.name !== "TimeoutError") throw error; if (last) throw error; } if (resp) { if (resp.status === 202) return resp.json(); const text = await resp.text(); let details; try { details = JSON.parse(text)?.error?.details; } catch { details = undefined; } const retryable = resp.status === 429 || resp.status >= 500 || details?.retryable === true; const blocked = details?.reason === "burst_block"; if (blocked || !retryable || last) { throw new Error(`HTTP ${resp.status}: ${text}`); } const wait = resp.headers.get("Retry-After"); if (wait && /^[0-9]+$/.test(wait)) { delay = Math.max(delay, Number(wait) * 1_000); } } await sleep(delay); } } const payload = { model: "nano-banana-pro", prompt: "Taza de cerámica sobre mármol, luz suave de ventana", }; const creation = await submit(payload, "order-8412-cover-v1"); console.log(creation.id);
El ejemplo de curl envía la solicitud una sola vez. Si no conoces el resultado, repítela a mano con la misma clave y el mismo cuerpo.
Qué hacer si…
| Situación | Qué hacer |
|---|---|
| Se agotó el tiempo de espera o se cortó la conexión, no hay respuesta | Repite la solicitud con la misma clave y el mismo cuerpo |
Respuesta 429: la primera solicitud aún se está aceptando | La respuesta trae details.reason: idempotency_in_flight. Espera Retry-After y repite con la misma clave |
Respuesta 400, 402 o 404 | La solicitud no se aceptó. Corrige la causa y repite con la misma clave |
Respuesta 5xx | Repite con la misma clave, limitando el número de intentos. 500 no indica si la solicitud se aceptó |
Llegó 202 y después la tarea pasó a failed | La clave anterior no reinicia la tarea. Para un intento nuevo hace falta una clave nueva: es una operación nueva de pago, y el cargo anterior se devuelve por sí solo |
| Necesitas una segunda imagen | Una solicitud nueva con una clave nueva |
Una solicitud fallida es gratis: el importe vuelve de inmediato al saldo.
Cuánto tiempo se guarda la clave
Las respuestas guardadas duran 30 días. Pasado ese tiempo, el registro puede borrarse y la misma clave iniciará una generación nueva de pago. Guarda por tu parte la relación «tu operación → id de la tarea» y no uses claves antiguas como forma de búsqueda.
La clave de idempotencia no da una garantía incondicional cuando falla el servicio. Envía las solicitudes de una misma operación una tras otra, limita el número de intentos y lleva tu propio control. Si después de los reintentos sigue sin estar claro si la solicitud se aceptó, mira el historial de solicitudes o escribe a soporte, antes de enviarla con una clave nueva.
Preguntas frecuentes
¿Hace falta una clave de idempotencia en cada solicitud?
¿Se puede usar una misma clave para imágenes distintas?
¿Qué pasa si la clave tiene más de 128 caracteres?
400 ERR_INVALID_REQUEST. Usa un UUID o un número de operación corto.