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

# Conversion tracking

> Track purchases and leads to measure the impact of widget visualizations

# Conversion tracking

Selektable can attribute conversions to widget visualizations, so you can measure whether customers who use the widget are more likely to take action. A conversion can be a purchase, a form submission, an appointment booking, or any other goal.

For WooCommerce and Shopify stores, conversion tracking is handled automatically via platform webhooks. This guide covers **custom sites** that need to send conversion data manually.

## How it works

1. The embed script assigns each visitor a persistent **visitor ID** (stored in `localStorage`) and a **session ID** (stored in `sessionStorage`)
2. When a customer generates a visualization, it's recorded with their visitor and session IDs
3. When a conversion happens (purchase, form submission, etc.), your server sends a webhook to Selektable that includes the same IDs
4. Selektable's attribution engine matches the conversion to any recent visualizations within the attribution window (default: 7 days)

## Client-side tracking

If your site doesn't have a backend (static sites, no-code platforms, or client-rendered apps), you can track conversions directly from the browser using the embed script. No API key or server-side code needed.

<Info>
  For sites with server access, we recommend the [server-side webhook](#ecommerce-order-tracking) below. It's more secure because the API key is never exposed to the browser.
</Info>

### Prerequisites

* The Selektable embed script must be loaded on both your product pages **and** your thank-you or confirmation page
* You need your **store ID** from the [dashboard](https://app.selektable.com) (already in the embed script tag)

### Ecommerce order tracking

On your thank-you or order confirmation page, call `Selektable.track()` with the order details:

```javascript theme={null}
Selektable.track('$conversion', {
  orderId: 'order-123',
  totalCents: 4999,
  currency: 'EUR',
  items: [
    { productId: '456', quantity: 1, priceCents: 4999, totalCents: 4999 }
  ]
});
```

The `productId` in your items should match the `productId` you pass to `Selektable.open()` on your product pages.

### Lead gen / appointment tracking

For non-ecommerce conversions (form submissions, bookings), just pass an `orderId`:

```javascript theme={null}
Selektable.track('$conversion', {
  orderId: 'lead-' + Date.now()
});
```

Defaults to `totalCents: 0`, `currency: 'USD'`, `status: 'completed'`, and empty `items`.

### Property reference

| Property     | Type      | Required | Default       | Description                                                                       |
| ------------ | --------- | -------- | ------------- | --------------------------------------------------------------------------------- |
| `orderId`    | `string`  | Yes      | —             | Unique identifier for this conversion                                             |
| `totalCents` | `integer` | No       | `0`           | Order total in cents, or `0` for leads                                            |
| `currency`   | `string`  | No       | `'USD'`       | 3-letter currency code                                                            |
| `status`     | `string`  | No       | `'completed'` | One of: `pending`, `processing`, `on-hold`, `completed`, `refunded`, `cancelled`  |
| `items`      | `array`   | No       | `[]`          | Order items (see [webhook reference](#webhook-payload-reference) for item fields) |

### Deduplication

The SDK automatically prevents duplicate tracking when a customer refreshes the thank-you page. Each `orderId` is only sent once per browser session.

***

## Prerequisites

* The Selektable embed script must be loaded on your site, including pages with forms or checkout
* You need your **store ID**, **organization ID**, and **API key** from the [dashboard](https://app.selektable.com)

API keys start with `sel_` and are used as a Bearer token in the `Authorization` header of every webhook request.

<Warning>
  Your API key is a secret. Never expose it in client-side JavaScript. The webhook must always be sent from your server.
</Warning>

## How tracking IDs reach your server

`Selektable.getIdentity()` is a **client-side browser API** that reads from `localStorage` and `sessionStorage`. It is not available on your server.

To send the webhook from your backend, you need to pass the tracking IDs from the browser through your form using hidden fields. Add these to any form where you want to track conversions:

```html theme={null}
<form action="/your-endpoint" method="POST">
  <!-- Your form fields here -->

  <!-- Hidden tracking fields (populated by JavaScript) -->
  <input type="hidden" name="visitorId" id="sel-visitor-id" />
  <input type="hidden" name="sessionId" id="sel-session-id" />

  <button type="submit">Submit</button>
</form>

<script>
  var identity = Selektable.getIdentity();
  document.getElementById('sel-visitor-id').value = identity.visitorId;
  document.getElementById('sel-session-id').value = identity.sessionId;
</script>
```

Your server then reads `visitorId` and `sessionId` from the form submission and includes them in the webhook payload.

## Ecommerce order tracking

Track purchases by sending the order details along with the visitor's tracking IDs from your server.

**Step 1:** Add hidden tracking fields to your checkout form (see above).

**Step 2:** When the order is placed, send the webhook from your server.

<CodeGroup>
  ```javascript Node.js theme={null}
  app.post('/checkout/complete', async (req, res) => {
    const { visitorId, sessionId, order } = req.body;

    await fetch('https://app.selektable.com/api/webhooks/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer sel_your_api_key'
      },
      body: JSON.stringify({
        orderId: order.id,
        storeId: 'store_xxx',
        organizationId: 'org_xxx',
        visitorId: visitorId,
        sessionId: sessionId,
        totalCents: order.totalCents,
        currency: order.currency,
        platform: 'custom',
        status: 'completed',
        orderCreatedAt: new Date().toISOString(),
        items: order.items.map(item => ({
          productId: item.productId,
          quantity: item.quantity,
          priceCents: item.priceCents,
          totalCents: item.priceCents * item.quantity
        }))
      })
    });

    res.redirect('/thank-you');
  });
  ```

  ```php PHP theme={null}
  $visitor_id = $_POST['visitorId'];
  $session_id = $_POST['sessionId'];

  $payload = json_encode([
      'orderId' => $order->id,
      'storeId' => 'store_xxx',
      'organizationId' => 'org_xxx',
      'visitorId' => $visitor_id,
      'sessionId' => $session_id,
      'totalCents' => (int)($order->total * 100),
      'currency' => 'EUR',
      'platform' => 'custom',
      'status' => 'completed',
      'orderCreatedAt' => date('c'),
      'items' => array_map(function($item) {
          return [
              'productId' => (string)$item->product_id,
              'quantity' => $item->quantity,
              'priceCents' => (int)($item->price * 100),
              'totalCents' => (int)($item->price * $item->quantity * 100),
          ];
      }, $order->items),
  ]);

  $ch = curl_init('https://app.selektable.com/api/webhooks/orders');
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'Content-Type: application/json',
      'Authorization: Bearer sel_your_api_key',
  ]);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_exec($ch);
  curl_close($ch);
  ```
</CodeGroup>

<Warning>
  The `productId` in your items should match the `productId` you pass to `Selektable.open()` on your product pages. This is how Selektable links a purchase to a specific product visualization.
</Warning>

## Lead gen / appointment tracking

If your site uses a contact form, appointment booking, or any non-ecommerce conversion, you can track form submissions as conversions. Set `totalCents` to `0` and pass an empty `items` array.

**Step 1:** Add hidden tracking fields to your form.

```html theme={null}
<form action="/appointment/book" method="POST">
  <input type="text" name="name" placeholder="Name" />
  <input type="email" name="email" placeholder="Email" />
  <input type="tel" name="phone" placeholder="Phone" />

  <!-- Hidden tracking fields (populated by JavaScript) -->
  <input type="hidden" name="visitorId" id="sel-visitor-id" />
  <input type="hidden" name="sessionId" id="sel-session-id" />

  <button type="submit">Book appointment</button>
</form>

<script>
  var identity = Selektable.getIdentity();
  document.getElementById('sel-visitor-id').value = identity.visitorId;
  document.getElementById('sel-session-id').value = identity.sessionId;
</script>
```

**Step 2:** On your server, read the tracking IDs from the form submission and send the webhook.

<CodeGroup>
  ```javascript Node.js theme={null}
  app.post('/appointment/book', async (req, res) => {
    const { visitorId, sessionId, name, email, phone } = req.body;

    // Save the appointment in your system
    const appointment = await saveAppointment({ name, email, phone });

    // Track the conversion in Selektable
    await fetch('https://app.selektable.com/api/webhooks/orders', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer sel_your_api_key'
      },
      body: JSON.stringify({
        orderId: 'appt-' + appointment.id,
        storeId: 'store_xxx',
        organizationId: 'org_xxx',
        visitorId: visitorId,
        sessionId: sessionId,
        totalCents: 0,
        currency: 'USD',
        platform: 'custom',
        status: 'completed',
        orderCreatedAt: new Date().toISOString(),
        items: []
      })
    });

    res.redirect('/thank-you');
  });
  ```

  ```php PHP theme={null}
  $visitor_id = $_POST['visitorId'];
  $session_id = $_POST['sessionId'];

  // Save the appointment in your system
  $appointment_id = save_appointment($_POST);

  // Track the conversion in Selektable
  $payload = json_encode([
      'orderId' => 'appt-' . $appointment_id,
      'storeId' => 'store_xxx',
      'organizationId' => 'org_xxx',
      'visitorId' => $visitor_id,
      'sessionId' => $session_id,
      'totalCents' => 0,
      'currency' => 'USD',
      'platform' => 'custom',
      'status' => 'completed',
      'orderCreatedAt' => date('c'),
      'items' => [],
  ]);

  $ch = curl_init('https://app.selektable.com/api/webhooks/orders');
  curl_setopt($ch, CURLOPT_POST, true);
  curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
  curl_setopt($ch, CURLOPT_HTTPHEADER, [
      'Content-Type: application/json',
      'Authorization: Bearer sel_your_api_key',
  ]);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_exec($ch);
  curl_close($ch);
  ```
</CodeGroup>

## Webhook payload reference

```
POST https://app.selektable.com/api/webhooks/orders
Authorization: Bearer sel_your_api_key
Content-Type: application/json
```

| Field            | Type      | Required | Description                                                                                           |
| ---------------- | --------- | -------- | ----------------------------------------------------------------------------------------------------- |
| `orderId`        | `string`  | Yes      | Unique identifier for this conversion (order ID, form submission ID, etc.)                            |
| `storeId`        | `string`  | Yes      | Your Selektable store ID                                                                              |
| `organizationId` | `string`  | Yes      | Your Selektable organization ID                                                                       |
| `visitorId`      | `string`  | Yes      | From `Selektable.getIdentity().visitorId` on the client, passed to your server via hidden form fields |
| `sessionId`      | `string`  | Yes      | From `Selektable.getIdentity().sessionId` on the client, passed to your server via hidden form fields |
| `totalCents`     | `integer` | Yes      | Order total in cents, or `0` for lead gen                                                             |
| `currency`       | `string`  | Yes      | 3-letter currency code (e.g., `USD`, `EUR`)                                                           |
| `platform`       | `string`  | Yes      | Must be `"custom"`                                                                                    |
| `status`         | `string`  | Yes      | One of: `pending`, `processing`, `on-hold`, `completed`, `refunded`, `cancelled`                      |
| `orderCreatedAt` | `string`  | Yes      | ISO 8601 date string                                                                                  |
| `items`          | `array`   | Yes      | Array of order items, or `[]` for lead gen                                                            |

**Order item fields** (for ecommerce):

| Field        | Type      | Required | Description                                              |
| ------------ | --------- | -------- | -------------------------------------------------------- |
| `productId`  | `string`  | Yes      | Must match the `productId` passed to `Selektable.open()` |
| `quantity`   | `integer` | Yes      | Quantity purchased                                       |
| `priceCents` | `integer` | Yes      | Unit price in cents                                      |
| `totalCents` | `integer` | Yes      | Line total in cents (`priceCents * quantity`)            |

## Viewing conversion data

Once conversions are tracked, analytics appear in your dashboard:

1. Go to the [Selektable dashboard](https://app.selektable.com)
2. Open your store and select a widget
3. Navigate to the **Analytics** tab

You'll see:

* **Recent conversions** with IDs and attributed revenue (or \$0 for leads)
* **Conversion type** (direct or assisted)
* **Time to conversion** showing how long after a visualization the action occurred

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhook returns 401 Unauthorized">
    Make sure you're including the `Authorization` header with a valid API key. The format is `Bearer sel_your_api_key`. You can find your API key in the [dashboard](https://app.selektable.com).
  </Accordion>

  <Accordion title="Conversions aren't showing in the dashboard">
    Verify that:

    * The `Authorization` header includes a valid API key.
    * The `visitorId` and `sessionId` are not empty. Log `Selektable.getIdentity()` on the client to check the values before form submission.
    * The `storeId` and `organizationId` match your dashboard values.
    * The webhook returns `{ "success": true }`.
  </Accordion>

  <Accordion title="Visitor ID is different on the thank you page">
    The visitor ID is stored in `localStorage` on the same domain. If your form redirects to a different domain (e.g., a third-party payment page or booking tool), the visitor ID won't carry over. Make sure the form page is on the same domain as your product pages.
  </Accordion>

  <Accordion title="Session ID is empty after redirect">
    The session ID uses `sessionStorage`, which resets when a new tab opens. If your checkout or form flow opens a new tab or window, the session ID will be different. The visitor ID (which persists across sessions) is more reliable for attribution in these cases.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Identity tracking" icon="user" href="/guides/identity-tracking">
    Associate visualizations with logged-in customer accounts
  </Card>

  <Card title="Custom store setup" icon="code" href="/guides/custom-store">
    Full guide to integrating the widget on a custom site
  </Card>
</CardGroup>
