> ## Documentation Index
> Fetch the complete documentation index at: https://docs.genlook.app/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive HMAC-signed HTTP requests on your own endpoint whenever a shopper tries on your products.

Point Genlook at any HTTPS endpoint and receive a signed POST request for every try-on, every completed try-on session, and every attributed order. Wire it into your backend, a Zapier-style automation, or an internal data pipeline.

<Note>
  Webhooks only fire for **identified** shoppers: they're logged in, or they submitted their email through the widget. When an anonymous visitor identifies themselves, their earlier try-ons are replayed to your endpoint at that moment.
</Note>

## Set up

<Steps>
  <Step title="Open Integrations">
    **Shopify:** In the Genlook app, go to **Integrations**.

    **WooCommerce, PrestaShop, and other platforms:** Sign in at [app.genlook.app](https://app.genlook.app), open your site, then go to **Integrations**.
  </Step>

  <Step title="Add your endpoint URL">
    Find **Webhook**, enter the HTTPS URL Genlook should call, and save.
  </Step>

  <Step title="Store the signing secret">
    Saving generates a signing secret and shows it **once**. Store it now; later visits only confirm that a secret exists. If you lose it, rotate to get a new one.
  </Step>
</Steps>

## Events

| Event                     | Fires                                                                                                         |
| ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `product_tried_on`        | Once per try-on, within seconds. One shopper, one product.                                                    |
| `try_on_session`          | Once per browsing session, 30 minutes after the shopper's last try-on. Contains every product of the session. |
| `order_try_on_attributed` | When an order is attributed to a shopper who tried on before buying.                                          |

`try_on_session` intentionally duplicates the `product_tried_on` events of the same session: consume whichever granularity fits your pipeline and ignore the other. Its `sessionId` is stable, so re-deliveries of the same session can be deduplicated on it.

## Request format

Every delivery is a POST with a JSON body and these headers:

| Header                  | Value                                                                 |
| ----------------------- | --------------------------------------------------------------------- |
| `x-genlook-event`       | The event type, one of the values above                               |
| `x-genlook-signature`   | `sha256=<hex>`, HMAC-SHA256 of the raw request body using your secret |
| `x-genlook-store-id`    | Your Genlook store id                                                 |
| `x-genlook-merchant-id` | Your store's own id, e.g. the myshopify domain (when known)           |

The body is always the same envelope; only `data` changes per event:

```json theme={null}
{
  "event": "try_on_session",
  "storeId": "9f3c1a2e-6b4d-4e8f-a1b2-c3d4e5f60789",
  "externalStoreId": "your-store.myshopify.com",
  "occurredAt": "2026-05-01T10:42:00.000Z",
  "customer": {
    "email": "shopper@example.com",
    "externalCustomerId": "gid://shopify/Customer/123456789",
    "acquisitionSource": "email_capture",
    "provisionedCustomer": false
  },
  "data": { }
}
```

| Field                          | Notes                                                                                                        |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| `customer.email`               | Present when the shopper shared or has an email                                                              |
| `customer.externalCustomerId`  | The shopper's customer id in your store, when they were logged in or matched                                 |
| `customer.acquisitionSource`   | `email_capture` (typed into the widget) or `logged_in`; `null` for shoppers recorded before this was tracked |
| `customer.provisionedCustomer` | `true` when Genlook created the customer record in your store rather than matching an existing one           |

## Product fields

Products in `product_tried_on` and `try_on_session` payloads carry the ids plus every catalog detail Genlook could resolve. A field that can't be resolved is omitted, never sent as `null` or empty.

| Field          | Example                                         | Notes                                              |
| -------------- | ----------------------------------------------- | -------------------------------------------------- |
| `productId`    | `123456789`                                     | Always present                                     |
| `variantId`    | `987654`                                        | When a specific variant was tried on               |
| `title`        | `Linen Shirt`                                   | Product title                                      |
| `vendor`       | `Example Brand`                                 | Product vendor (brand)                             |
| `variantTitle` | `M / Blue`                                      | Variant label; omitted for single-variant products |
| `sku`          | `LIN-SHIRT-M`                                   | Variant SKU                                        |
| `price`        | `49.99`                                         | Variant price                                      |
| `currency`     | `EUR`                                           | Currency of `price`                                |
| `productUrl`   | `https://shop.example.com/products/linen-shirt` | Storefront link                                    |
| `imageUrl`     | `https://cdn.example.com/linen-shirt.jpg`       | Variant image, falling back to the product image   |
| `categories`   | `["Shirts"]`                                    | Coarse merchandising categories, when known        |

## Payloads by event

### `product_tried_on`

```json theme={null}
"data": {
  "productId": "123456789",
  "variantId": "987654",
  "title": "Linen Shirt",
  "vendor": "Example Brand",
  "variantTitle": "M",
  "sku": "LIN-SHIRT-M",
  "price": 49.99,
  "currency": "EUR",
  "productUrl": "https://shop.example.com/products/linen-shirt",
  "imageUrl": "https://cdn.example.com/linen-shirt.jpg",
  "categories": ["Shirts"]
}
```

### `try_on_session`

Distinct products, most-tried first, each with the product fields above plus per-product counts:

```json theme={null}
"data": {
  "sessionId": "abc123_1748772000000",
  "startedAt": "2026-05-01T10:00:00.000Z",
  "endedAt": "2026-05-01T10:42:00.000Z",
  "tryOnCount": 6,
  "productCount": 4,
  "products": [
    {
      "productId": "123456789",
      "variantId": "987654",
      "title": "Linen Shirt",
      "vendor": "Example Brand",
      "variantTitle": "M",
      "sku": "LIN-SHIRT-M",
      "price": 49.99,
      "currency": "EUR",
      "productUrl": "https://shop.example.com/products/linen-shirt",
      "imageUrl": "https://cdn.example.com/linen-shirt.jpg",
      "tryOnCount": 3,
      "firstTriedAt": "2026-05-01T10:00:00.000Z",
      "lastTriedAt": "2026-05-01T10:39:00.000Z"
    }
  ]
}
```

### `order_try_on_attributed`

```json theme={null}
"data": {
  "orderId": "gid://shopify/Order/5551234567",
  "tryOnCount": 6,
  "productMatch": true
}
```

`tryOnCount` is the shopper's try-on count in the attribution window at order time, and `productMatch` is `true` when the order contains a product they tried on.

## Verify the signature

Compute HMAC-SHA256 of the **raw** request body with your secret and compare it, timing-safe, against the hex after `sha256=`. Reject the request on mismatch.

```javascript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function isGenuine(rawBody, signatureHeader, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const received = signatureHeader.replace("sha256=", "");
  return (
    expected.length === received.length &&
    timingSafeEqual(Buffer.from(expected), Buffer.from(received))
  );
}
```

Parse the JSON only after the signature checks out, and compute the HMAC over the raw bytes, not a re-serialized object.

## Delivery and retries

* Respond with a `2xx` quickly; Genlook times out after 5 seconds.
* Failed deliveries (timeouts, `408`, `429`, and `5xx`) are retried up to 3 times with exponential backoff, then dropped. Webhooks are best-effort: treat your own systems as the source of truth for anything critical.
* Deliveries can arrive out of order; use `occurredAt` and `sessionId` rather than arrival time.
