# Server-side recommendations

> Get the shopper's size on your server with a sealed handle from the browser. Sizes only; measurements never leave TryYourSize.

Source: https://docs.tryyoursize.com/guides/server-side-recommendations/

Use this when your server needs the shopper's size: a headless storefront, server-rendered product pages, a size shown in a cart email, or your own recommendation logic.

## How it works

1. **In the browser**, TryYourSize.js asks our origin for a **recommendation handle** for the signed-in shopper.

   ```ts
   const tys = TryYourSize("pk_live_…");
   const h = await tys.recommendationHandle();   // { handle, expiresAt } | null
   ```

   `null` means the shopper is not signed in to TryYourSize on this site, has no body yet, or is in your [A/B holdout](https://docs.tryyoursize.com/guides/ab-holdout.md). `expiresAt` is an ISO date-time one hour ahead. In React, call it on the client from `useTryYourSize()`.

2. **Your page sends `h.handle` to your own server**, like any other value (a form field, a fetch to your API).

3. **Your server** calls `POST /v1/recommendations` with a secret key (or a restricted key with `fit_checks:read`) and up to 25 products.

4. **The answer** is one entry per product, in the order asked: sizes in your labels, never measurements.

**Node.js**

```ts
const tys = new TryYourSize(process.env.TYS_SECRET_KEY!);

const { mode, data } = await tys.recommendations.create({
  handle: req.body.tysHandle,
  products: ["CREW-TSHIRT", "STRAIGHT-JEANS"],
});
for (const r of data) {
  if (r.status === "ready") console.log(r.product, r.size);   // "CREW-TSHIRT", "Medium"
}
```

**Python**

```python
tys = tryyoursize.TryYourSize(os.environ["TYS_SECRET_KEY"])

res = tys.recommendations.create(
    handle=request.form["tys_handle"],
    products=["CREW-TSHIRT", "STRAIGHT-JEANS"],
)
for r in res["data"]:
    if r["status"] == "ready":
        print(r["product"], r["size"])
```

**cURL**

```bash
curl https://api.tryyoursize.com/v1/recommendations \
  -H "Authorization: Bearer $TYS_SECRET_KEY" \
  -H "TYS-Version: 2026-10-25" \
  -H "Content-Type: application/json" \
  -d '{"handle":"tys_rh_…","products":["CREW-TSHIRT","STRAIGHT-JEANS"]}'
```

## The answer

```json
{
  "mode": "live",
  "data": [
    {
      "product": "CREW-TSHIRT",
      "status": "ready",
      "size": "Medium",
      "studio_size": "M",
      "wearable_sizes": ["Medium", "Large"]
    },
    {
      "product": "STRAIGHT-JEANS",
      "status": "unavailable",
      "size": null,
      "studio_size": null,
      "wearable_sizes": []
    }
  ]
}
```

| Field | |
|---|---|
| `status` | `ready` (a size), `computing` (a simulation the shopper started is running: ask again later), `none` (simulated, no size is wearable), `unavailable` (no published garment, not simulated on this body yet, or the plan has no fit checks left). There is no `signed_out`: a handle always belongs to a signed-in shopper. |
| `size` | The recommended size in your label (the product's size map), or `null`. |
| `studio_size` | The same size as entered in Studio, or `null`. |
| `wearable_sizes` | Sizes wearable on this body, in your labels. |
| `method` | A coarse, informational label of how the size was obtained. Do not build logic on its values. |
| `tryon_id` | The try-on that produced the size, when there is one. Send it back with orders for attribution. |

## Rules

- **It never starts a simulation.** Like a page view, it answers only with a size that is already available (many garments answer in under a second); otherwise the status is `unavailable` until the shopper asks for the try-on in our window.
- **A delivered size counts as a fit check**, exactly as in the browser (once per shopper and product per 30 days, live mode only).
- **Raw measurements are never accepted or returned** by the server API.

## The handle

- It starts with `tys_rh_` and is up to 4 KB. Treat it as opaque.
- It is **encrypted and sealed** to one store, one mode and one shopper. Neither your page nor your server can read anything from it; it never carries the shopper's email.
- It **expires after 1 hour**. After that the API answers `401` with code [`handle_expired`](https://docs.tryyoursize.com/api/errors.md#handle_expired): ask the browser for a new one.
- It is refused with `401` [`handle_invalid`](https://docs.tryyoursize.com/api/errors.md#handle_invalid) after the shopper deletes their account or disconnects your store, and when it was issued for another store.
- Its **mode must match your key**: a test handle with a live key (or the reverse) is `403` [`handle_mode_mismatch`](https://docs.tryyoursize.com/api/errors.md#handle_mode_mismatch).

> **Tip**
>
> Ask for a new handle on each page load rather than storing one: it is cheap, and a fresh handle reflects the shopper's current consent.
