# Webhooks

> Receive signed events (fit.recommended, garment.published…) on your server. Signature verification in Node, Python and plain code; retries.

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

TryYourSize sends events to HTTPS endpoints you add in **Studio → Developers → Webhooks**. Endpoints belong to one store and one mode (test or live); up to 16 per store. Each endpoint subscribes to a list of event types, or to all (`*`). Its **signing secret** (`whsec_…`) is shown once.

## Event types

Payloads never carry shopper identity or measurements.

| Type | `data` | When |
|---|---|---|
| `fit.recommended` | `tryon_id`, `product_sku`, `recommended_size`, `no_size_fits` | Once per fit result, store and mode |
| `garment.published` | `garment_version_id`, `fit_group_id`, `version`, `product_skus` | A garment version was published (sent to test and live endpoints) |
| `fit_check.completed` | `fit_check_id`, `garment_version_id`, `status` | A Studio fit check finished or failed |
| `profile.disconnected` | `connection_id`, `reason` | A shopper disconnected your store (live mode only) |
| `usage.threshold_reached` | | Coming soon: your usage reached a threshold of your plan |
| `webhook.test` | | "Send test event" in Studio |

## The envelope

```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
  }
}
```

Headers: `TYS-Signature`, `TYS-Event-Id`, `TYS-Event-Type`, `TYS-Delivery-Id`, `TYS-Delivery-Attempt`. The same event can arrive more than once: deduplicate on `TYS-Event-Id` (the envelope's `id`).

## Verify the signature

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

`v1` is the hex HMAC-SHA256 of `"<t>.<raw request body>"`, keyed with the whole `whsec_…` secret. While you roll a secret, there are two `v1` values (one per secret) for 24 hours: accept the request if **any** matches. Reject timestamps older than **5 minutes**. Always verify the **raw** body, before any JSON parsing.

**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 (plain)**

```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 (plain)**

```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)
```

In any other language: split the header on `,`, take `t` and every `v1`, compute HMAC-SHA256 over `t + "." + raw body` with the secret as the key, hex-encode it, compare in constant time against each `v1`, and check that `t` is within 5 minutes of now.

## Responding and retries

- Answer any **2xx** within 10 seconds to acknowledge. Do slow work after you answer.
- Redirects are not followed; the response body is ignored.
- On another status, a timeout or a network error, we retry: an attempt now, then after **1 min, 5 min, 30 min, 2 h, 5 h and 10 h**, then every **12 h**: 12 attempts over about **72 hours**.
- Answer **410 Gone** to stop the retries of that delivery.
- Disabling or deleting the endpoint stops pending deliveries.

Studio shows every delivery with its payload and attempts, and can **resend** one.

## Rolling the secret

In Studio, roll an endpoint's secret: the previous secret keeps signing (as a second `v1`) for 24 hours, so you can deploy the new one without dropping events.
