Webhooks
Los webhooks avisan a tu servidor cuando termina un job, sin necesidad de consultar su estado en bucle.
Configurar un webhook
Sección titulada «Configurar un webhook»En cloud.pictulab.com, sección Developers, crea un webhook con la URL de tu endpoint y los eventos que quieres recibir.
- Disponible desde el plan Pro.
- Hasta 5 webhooks por equipo.
- La URL debe ser pública (no se admiten direcciones internas).
- Al crearlo obtienes un secreto de firma con el formato
whsec_…. Guárdalo: lo necesitas para verificar las peticiones.
Eventos
Sección titulada «Eventos»| Evento | Cuándo se envía | data |
|---|---|---|
job.completed |
Un job termina correctamente. | { job_id, type, result } |
job.failed |
Un job falla tras todos los reintentos. | { job_id, type, error } |
result tiene el mismo formato que en GET /v2/jobs/:jobId.
La petición
Sección titulada «La petición»Pictulab envía un POST con cuerpo JSON:
POST /tu-endpoint HTTP/1.1Content-Type: application/jsonUser-Agent: Pictulab-Webhooks/1.0X-Pictulab-Event: job.completedX-Pictulab-Delivery: 0b6c9f5e-2a41-4d0e-9d63-1f2a7c4e8b10X-Pictulab-Timestamp: 2026-10-07T08:00:31.000ZX-Pictulab-Attempt: 1X-Pictulab-Signature: 5d41402abc4b2a76b9719d911017c592…{ "id": "0b6c9f5e-2a41-4d0e-9d63-1f2a7c4e8b10", "event": "job.completed", "timestamp": "2026-10-07T08:00:31.000Z", "team_id": "…", "data": { "job_id": "6f1c2a7e-…", "type": "image_generation", "result": { "images": [{ "url": "https://cdn.pictulab.com/…", "file_name": "…", "mime_type": "image/png", "file_size": 1843211 }], "tokens_used": 6, "prompt": "…" } }}| Cabecera | Descripción |
|---|---|
X-Pictulab-Signature |
Firma HMAC-SHA256 del cuerpo, en hexadecimal. |
X-Pictulab-Event |
Nombre del evento. |
X-Pictulab-Delivery |
ID único de la entrega. Igual en todos los reintentos: úsalo para deduplicar. |
X-Pictulab-Timestamp |
Fecha de envío (ISO 8601). |
X-Pictulab-Attempt |
Número de intento (1, 2 o 3). |
Verificar la firma
Sección titulada «Verificar la firma»La firma es el HMAC-SHA256 del cuerpo crudo de la petición, usando como clave el secreto completo (whsec_…), codificado en hexadecimal en minúsculas, sin prefijo.
- Lee el cuerpo sin parsear (los bytes exactos recibidos).
- Calcula el HMAC-SHA256 con tu secreto.
- Compáralo con
X-Pictulab-Signatureen tiempo constante. - Si no coincide, responde
401y descarta el evento.
import crypto from 'node:crypto';import express from 'express';
const app = express();const SECRET = process.env.PICTULAB_WEBHOOK_SECRET; // whsec_...
app.post('/webhooks/pictulab', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.get('X-Pictulab-Signature') ?? ''; const expected = crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const valid = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8')); res.sendStatus(200); // responde rápido y procesa después
if (event.event === 'job.completed') { // guardar event.data.result.images... }});import hashlibimport hmacimport os
from flask import Flask, abort, request
app = Flask(__name__)SECRET = os.environ["PICTULAB_WEBHOOK_SECRET"].encode() # whsec_...
@app.post("/webhooks/pictulab")def pictulab_webhook(): body = request.get_data() # bytes sin parsear expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest() signature = request.headers.get("X-Pictulab-Signature", "") if not hmac.compare_digest(signature, expected): abort(401)
event = request.get_json() if event["event"] == "job.completed": pass # guardar event["data"]["result"]["images"]... return "", 200Reintentos
Sección titulada «Reintentos»- Una entrega es correcta si tu endpoint responde 2xx en menos de 10 segundos.
- Si respondes 5xx, hay un error de red o un timeout, se reintenta: 3 intentos en total, a los 0 s, 2 s y 10 s.
- Si respondes 4xx, no se reintenta.
- Puedes ver el historial de entregas de cada webhook en el panel.
Buenas prácticas
Sección titulada «Buenas prácticas»- Responde
2xxde inmediato y procesa el evento en segundo plano. - Haz el procesamiento idempotente usando
X-Pictulab-Delivery. - Usa HTTPS.
- Si tu endpoint estuvo caído, recupera los resultados con
GET /v2/jobs.