---
title: Webhooks
description: Una notificación POST a su servidor cuando termina una generación — formato del cuerpo, verificación de la firma HMAC-SHA256, plazo de respuesta y calendario de reintentos de entrega.
icon: Webhook
---

Indique un `webhook_url` al enviar una generación y Clipia enviará un `POST` a esa dirección cuando termine el trabajo. No necesita consultar el estado. Cada entrega se firma con HMAC-SHA256 usando el secreto de su clave, y una entrega fallida se reintenta hasta 6 veces.

## Requisitos de la URL

- Solo `https://`. Las direcciones de redes internas o privadas se rechazan al enviar con `400 webhook_url_invalid`.
- No se siguen las redirecciones: una respuesta `3xx` cuenta como entrega fallida.

## Cuerpo

Si tiene éxito:

```json
{
  "request_id": "764cabcf-b745-4b3e-ae38-1200304cf45b",
  "status": "OK",
  "payload": {
    "model": "nano-banana-2",
    "output": { "images": [{ "url": "https://media.clipia.ai/....webp" }] },
    "cost": 12
  }
}
```

Si falla la generación:

```json
{
  "request_id": "764cabcf-b745-4b3e-ae38-1200304cf45b",
  "status": "ERROR",
  "error": {
    "code": "GENERATION_FAILED",
    "message": "Generation failed. Please retry or contact support."
  }
}
```

`output` depende del tipo de modelo: `images: [{ url }]`, `video: { url }` o `audio: { url }`.

<Callout type="warn" title="El cuerpo del webhook es más reducido que la respuesta REST">
Los webhooks no incluyen `original_url` ni `thumbnail_url`, y las imágenes llegan con un solo enlace. Obtenga el resultado completo con [`GET /v1/requests/:id`](/es/docs/api-reference/result).
</Callout>

<TypeTable
  type={{
    request_id: { type: 'string', description: 'El request_id de la respuesta de submit', required: true },
    status: { type: '"OK" | "ERROR"', description: 'Resultado de la generación', required: true },
    'payload.model': { type: 'string', description: 'Slug del modelo, solo con OK' },
    'payload.output': { type: 'object', description: 'Enlaces al resultado, solo con OK' },
    'payload.cost': { type: 'integer', description: 'Créditos cobrados, solo con OK' },
    error: { type: 'object', description: '{ code, message }, solo con ERROR' }
  }}
/>

## Cabeceras

```http
Content-Type: application/json
X-Clipia-Webhook-Id: 1f2e3d4c-5b6a-7890-abcd-ef0123456789
X-Clipia-Timestamp: 1760011200
X-Clipia-Signature: t=1760011200,v1=5257a869e7...
```

| Cabecera | Valor |
|----------|-------|
| `X-Clipia-Webhook-Id` | identificador de la entrega, el mismo en todos los reintentos |
| `X-Clipia-Timestamp` | hora Unix de este intento, en segundos |
| `X-Clipia-Signature` | `t=<timestamp>,v1=<hex HMAC-SHA256>` |

## Secreto de firma

Cada clave de API tiene su propio secreto, que se usa para firmar los webhooks de los trabajos enviados con esa clave. El secreto se muestra una sola vez, al crear la clave en la [Developer Console](/es/developer). La consola no tiene historial de entregas ni reenvío manual. Si pierde el secreto, cree una clave nueva y cambie a ella su integración: consulte [Gestión de claves](/es/docs/api-reference/keys).

## Verificación de la firma

La firma es `HMAC_SHA256(secret, "{timestamp}.{raw_body}")`. Verifíquela sobre el cuerpo **sin procesar** de la solicitud antes de analizar el JSON, compárela en tiempo constante y rechace las entregas con más de 5 minutos de antigüedad. Lo más sencillo es usar la función ya preparada del SDK.

<Tabs items={['TypeScript SDK', 'Python SDK', 'Node.js sin SDK']}>
<Tab value="TypeScript SDK">

```ts
import { verifyWebhookSignature } from 'clipia-ai/webhook';

// rawBody son los bytes originales del cuerpo, no JSON.stringify(req.body)
const ok = verifyWebhookSignature({
  secret: process.env.CLIPIA_WEBHOOK_SECRET!,
  headers: req.headers,
  body: rawBody,
  toleranceSeconds: 300,
});
if (!ok) return res.status(401).end();
```

</Tab>
<Tab value="Python SDK">

```python
import os
from clipia.webhooks import verify_signature

ok = verify_signature(
    os.environ["CLIPIA_WEBHOOK_SECRET"],
    request.headers,
    raw_body,  # los bytes originales del cuerpo
    tolerance_seconds=300,
)
if not ok:
    return Response(status=401)
```

</Tab>
<Tab value="Node.js sin SDK">

```js
import crypto from 'node:crypto';

function verify(headers, rawBody, secret) {
  const sig = headers['x-clipia-signature'] ?? '';
  const parts = Object.fromEntries(sig.split(',').map((p) => p.split('=')));
  if (!parts.t || !parts.v1) return false;
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(parts.v1, 'hex');
  const b = Buffer.from(expected, 'hex');
  return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

</Tab>
</Tabs>

Ambas funciones del SDK devuelven `true` o `false` y nunca lanzan una excepción ante cabeceras mal formadas.

## Entrega y reintentos

- Responda con `2xx` en un plazo de 10 segundos. Haga el procesamiento pesado después de responder.
- Una respuesta distinta de `2xx`, un `3xx` o un tiempo de espera agotado cuentan como fallo. La entrega se intenta hasta 6 veces en total.
- La pausa entre intentos se duplica, empezando en 30 segundos, con un máximo de una hora. Las entregas se despachan cada 30 segundos, así que el retraso real puede ser algo mayor.

<Callout type="info" title="Gestione las entregas de forma idempotente">
La misma entrega puede llegar más de una vez, por ejemplo si se perdió su respuesta. Elimine los duplicados por `X-Clipia-Webhook-Id` o `request_id` antes de ejecutar efectos secundarios.
</Callout>

En el [entorno de pruebas](/es/docs/getting-started/sandbox), los webhooks se entregan y se firman de la misma manera: una forma cómoda de probar su manejador.
