10Fundamentos de la API
Webhooks
Te enviaremos un POST firmado a tu dirección cuando la tarea pase a completed o failed. No hace falta consultar el estado de forma continua.
Este modelo no genera todas las resoluciones ni todas las relaciones de aspecto: los ejemplos usan las que tiene disponibles.
Preparación
Consigue el secreto de firma
En el portal, en la sección «Webhooks», copia el secreto de firma de la cuenta. Empieza por
whsec_y es uno solo para todas tus claves de API. El secreto debe existir en el momento en que la tarea termine. No es una clave de API: los valores son distintos.Prepara la dirección
Hace falta una dirección HTTPS accesible desde internet, con un certificado válido. No sirven localhost, las direcciones de redes internas ni otros destinos prohibidos. La longitud de la dirección es de hasta 2 048 caracteres. En la entrega no se siguen las redirecciones, así que indica la dirección final.
Añade
webhook_urla la solicitudIndica la dirección en cada solicitud de generación para la que quieras recibir una notificación. Una dirección incorrecta o prohibida devuelve
400antes del cargo.{ "model": "nano-banana-pro", "prompt": "Taza de cerámica sobre mármol", "webhook_url": "https://example.com/ webhooks/ rendergrid" }
Qué llega a tu dirección
| Encabezado | Valor |
|---|---|
Content-Type | application/json |
X-Webhook-Event | creation.completed o creation.failed. En tu lógica apóyate en el campo event del cuerpo firmado |
X-Webhook-Delivery-ID | El identificador de la entrega. No cambia en los reintentos de la misma entrega |
X-Timestamp | La hora de la firma, en segundos Unix. Se calcula de nuevo en cada intento |
X-Webhook-Signature-256 | sha256= y la firma HMAC-SHA256 en caracteres hexadecimales en minúsculas |
Eventos
El cuerpo del evento es un JSON con los campos event, id, status, result_urls y cost. El evento de error añade error y failure.
{
"event": "creation.completed",
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "completed",
"result_urls": [
"https:// cdn.rendergrid.io/ images/ example/ result.jpg"
],
"cost": 0.04
}{
"event": "creation.failed",
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "failed",
"result_urls": [],
"cost": 0.04,
"error": "Generation failed.",
"failure": {
"type": "error",
"code": "internal",
"retryable": true
}
}{
"event": "creation.completed",
"id": "test",
"test": true
}- cost
- El cargo original en dólares estadounidenses (USD). No es una confirmación de reembolso ni el saldo. Si no hay importe, llega
null. - error
- Un texto para mostrar o
null. El éxito o el fallo se determinan porstatus, no por el texto. - failure
- La causa del fallo para el programa:
type,code,retryable. Qué significan. - result_urls
- Enlaces a los archivos. En un evento fallido la lista está vacía. Los archivos duran un día: descárgalos enseguida.
Más adelante pueden aparecer campos nuevos. Si validas el cuerpo con un esquema estricto, permite los campos desconocidos; de lo contrario, las entregas empezarán a rechazarse.
Verificación de la firma
La firma sirve para comprobar que el mensaje viene de nosotros y que nadie lo ha modificado. Se calcula así:
- Toma los bytes originales del cuerpo, antes de que el framework analice el JSON.
- Concatena el valor de
X-Timestamp, un punto y el cuerpo. - Calcula el HMAC-SHA256 de esa cadena. La clave es el secreto completo, con
whsec_, en bytes UTF-8. - Compara
sha256=y el resultado con el encabezadoX-Webhook-Signature-256en tiempo constante. - Rechaza la solicitud si la marca de tiempo queda fuera de la ventana. En los ejemplos la ventana es de 300 segundos; es una regla del receptor, no un plazo de reentrega.
import hashlib import hmac import re import time TOLERANCE_SECONDS = 300 def verify_webhook( secret: str, timestamp: str, raw_body: bytes, signature: str ) -> bool: if not isinstance(secret, str) or not secret: return False if not isinstance(raw_body, bytes): return False if not re.fullmatch(r"[0-9]{1,12}", timestamp or ""): return False if not re.fullmatch(r"sha256=[0-9a-f]{64}", signature or ""): return False if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False expected = "sha256=" + hmac.new( secret.encode("utf-8"), timestamp.encode("ascii") + b"." + raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature)
import { createHmac, timingSafeEqual } from "node:crypto"; const TOLERANCE_SECONDS = 300; const TS_RE = /^[0-9]{1,12}$/; const SIG_RE = /^sha256=[0-9a-f]{64}$/; export function verifyWebhook(secret, timestamp, rawBody, signature) { if (typeof secret !== "string" || !secret) return false; if (!Buffer.isBuffer(rawBody)) return false; if (typeof timestamp !== "string" || !TS_RE.test(timestamp)) { return false; } if (typeof signature !== "string" || !SIG_RE.test(signature)) { return false; } const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (age > TOLERANCE_SECONDS) return false; const expected = "sha256=" + createHmac("sha256", Buffer.from(secret, "utf8")) .update(timestamp + ".", "ascii") .update(rawBody) .digest("hex"); return timingSafeEqual(Buffer.from(expected, "ascii"), Buffer.from(signature, "ascii")); }
No analices ni vuelvas a montar el JSON antes de la comprobación: cambian los espacios, el escape y el orden de las claves, y la firma deja de coincidir. No decodifiques el secreto ni le quites whsec_. Sincroniza el reloj del servidor.
Analiza el JSON y ejecuta acciones solo después de una comprobación correcta.
Compruébalo con un ejemplo
Tres solicitudes ya preparadas y firmadas de verdad. Elige una y pulsa el botón.
Confirmación y reintentos
Cualquier respuesta 2xx significa «recibido». Las demás respuestas y los errores de red los consideramos un fallo y repetimos la entrega: por defecto, hasta cinco intentos en total.
| Intento | Cuándo |
|---|---|
| 1 | de inmediato, al terminar la tarea |
| 2 | 60 segundos después del primero |
| 3 | 120 segundos después del segundo |
| 4 | 240 segundos después del tercero |
| 5 | 480 segundos después del cuarto |
- Esperamos la respuesta 10 segundos. Confirma la recepción rápido y deja para después la descarga de imágenes y el trabajo largo.
- Primero guarda el evento de forma fiable o ponlo en una cola, y después responde
2xx. Si no has podido aceptarlo, responde con otro estado. - Ejecuta la acción una sola vez por cada pareja
(id, event).X-Webhook-Delivery-IDayuda a eliminar duplicados. Una repetición debe ser segura incluso si tu confirmación se perdió. - Un error de entrega no pasa la tarea a
failed. El estado siempre se puede consultar enGET /creations/, y el historial de entregas, en el portal.{id}
El secreto de firma y su sustitución
Todas las claves de API de la cuenta firman los webhooks con un mismo secreto. Guárdalo en el servidor, en un almacén de secretos.
Tras la sustitución en el portal, el secreto nuevo pasa a ser el actual al instante: con él se firman todos los intentos siguientes, incluidas las repeticiones. Actualiza el controlador de inmediato y durante 24 horas más acepta también el secreto anterior, sin dejar de comprobar la frescura de la marca de tiempo. Solo se conserva un secreto anterior: la siguiente sustitución lo desplazará.
Envío de prueba
En el portal puedes enviar una prueba firmada a una dirección HTTPS permitida. Como máximo una vez cada 30 segundos por cuenta; la prueba sale una sola vez, sin reintentos. El cuerpo es más corto que el de un evento real:
{
"event": "creation.completed",
"id": "test",
"test": true
}Comprueba la firma, verifica que llega test: true, responde 2xx y no proceses el resultado. La prueba no lleva status, result_urls ni cost: no los exijas. El registro de las últimas entregas y de sus resultados está ahí mismo, en el portal, con las más recientes arriba.
Preguntas frecuentes
¿Hay que indicar webhook_url en cada solicitud?
¿Qué hacer si el webhook no llegó?
GET /creations/ {id}. La entrega no está garantizada al cien por cien, así que el sondeo es la vía de reserva.¿Por qué no coincide la firma?
whsec_ al secreto o que la firma se calcule sobre otros bytes. Toma el cuerpo original y el valor de X-Timestamp tal cual.