# Webhooks

> Recibe eventos firmados (fit.recommended, garment.published…) en tu servidor. Verificación de la firma en Node, Python y sin SDK; reintentos.

Source: https://docs.tryyoursize.com/es/guides/webhooks/

TryYourSize envía eventos a los endpoints (puntos de conexión) HTTPS que añadas en **Studio → Desarrolladores → Webhooks** (Developers → Webhooks). Cada endpoint pertenece a una tienda y a un modo (de prueba o real); hasta 16 por tienda. Cada endpoint se suscribe a una lista de tipos de evento, o a todos (`*`). Su **secreto de firma** (`whsec_…`) se muestra una sola vez.

## Tipos de evento

Los payloads nunca incluyen la identidad ni las medidas de quien compra.

| Tipo | `data` | Cuándo |
|---|---|---|
| `fit.recommended` | `tryon_id`, `product_sku`, `recommended_size`, `no_size_fits` | Una vez por resultado de tallaje, tienda y modo |
| `garment.published` | `garment_version_id`, `fit_group_id`, `version`, `product_skus` | Se publicó una versión de una prenda (se envía a los endpoints de prueba y reales) |
| `fit_check.completed` | `fit_check_id`, `garment_version_id`, `status` | Una prueba de tallaje de Studio terminó o falló |
| `profile.disconnected` | `connection_id`, `reason` | Una persona compradora desconectó tu tienda (solo en modo real) |
| `usage.threshold_reached` | | Próximamente: tu uso alcanzó un umbral de tu plan |
| `webhook.test` | | "Enviar evento de prueba" (Send test event) en Studio |

## El sobre

```json
{
  "id": "evt_…",
  "object": "event",
  "type": "fit.recommended",
  "api_version": "2026-10",
  "created": 1793000000,
  "mode": "live",
  "store_id": "…",
  "data": {
    "tryon_id": "…",
    "product_sku": "CREW-TSHIRT",
    "recommended_size": "M",
    "no_size_fits": false
  }
}
```

Cabeceras: `TYS-Signature`, `TYS-Event-Id`, `TYS-Event-Type`, `TYS-Delivery-Id`, `TYS-Delivery-Attempt`. El mismo evento puede llegar más de una vez: elimina duplicados con `TYS-Event-Id` (el `id` del sobre).

## Verifica la firma

```
TYS-Signature: t=1793000000,v1=5f2c…
```

`v1` es el HMAC-SHA256 en hexadecimal de `"<t>.<cuerpo bruto de la petición>"`, con el secreto `whsec_…` completo como clave. Mientras rotas un secreto, hay dos valores `v1` (uno por secreto) durante 24 horas: acepta la petición si **alguno** coincide. Rechaza las marcas de tiempo de hace más de **5 minutos**. Verifica siempre el cuerpo **bruto**, antes de cualquier parseo JSON.

**Node (SDK)**

```ts

const tys = new TryYourSize(process.env.TYS_SECRET_KEY!);
const app = express();

app.post("/webhooks/tryyoursize", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = tys.webhooks.constructEvent(req.body, req.header("TYS-Signature")!, process.env.TYS_WEBHOOK_SECRET!);
  } catch {
    return res.sendStatus(400);
  }
  if (event.type === "garment.published") {
    /* … */
  }
  res.sendStatus(200);
});
```

**Python (SDK)**

```python
from flask import Flask, request

app = Flask(__name__)

@app.post("/webhooks/tryyoursize")
def webhook():
    try:
        event = tryyoursize.Webhook.construct_event(
            request.get_data(), request.headers["TYS-Signature"], os.environ["TYS_WEBHOOK_SECRET"]
        )
    except Exception:
        return "", 400
    if event["type"] == "garment.published":
        ...
    return "", 200
```

**Node (sin SDK)**

```ts

  const parts = header.split(",").map((p) => p.split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  const sigs = parts.filter(([k]) => k === "v1").map(([, v]) => v);
  if (!t || !sigs.length) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  return sigs.some((s) => {
    const got = Buffer.from(s, "hex");
    return got.length === expected.length && timingSafeEqual(got, expected);
  });
}
```

**Python (sin SDK)**

```python

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    items = [p.split("=", 1) for p in header.split(",") if "=" in p]
    t = next((v for k, v in items if k == "t"), None)
    sigs = [v for k, v in items if k == "v1"]
    if t is None or not sigs or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in sigs)
```

En cualquier otro lenguaje: divide la cabecera por `,`, toma `t` y cada `v1`, calcula el HMAC-SHA256 de `t + "." + cuerpo bruto` con el secreto como clave, codifícalo en hexadecimal, compáralo en tiempo constante con cada `v1` y comprueba que `t` esté a menos de 5 minutos de la hora actual.

## Respuestas y reintentos

- Responde con cualquier **2xx** en menos de 10 segundos para confirmar la recepción. Haz el trabajo lento después de responder.
- No se siguen las redirecciones; el cuerpo de la respuesta se ignora.
- Ante otro código de estado, un timeout o un error de red, reintentamos: un intento inmediato y luego a los **1 min, 5 min, 30 min, 2 h, 5 h y 10 h**, y después cada **12 h**: 12 intentos en unas **72 horas**.
- Responde **410 Gone** para detener los reintentos de esa entrega.
- Desactivar o eliminar el endpoint detiene las entregas pendientes.

Studio muestra cada entrega con su payload y sus intentos, y permite **reenviarla**.

## Rotar el secreto

En Studio, rota el secreto de un endpoint: el secreto anterior sigue firmando (como segundo `v1`) durante 24 horas, para que puedas desplegar el nuevo sin perder eventos.
