# Webhooks

You can send demo events to the Webhook URL.

{% hint style="warning" %}

### **Identifying Leads**

There are a few ways in which you can capture or identify leads. You can refer to the article on [Lead Tracking](https://docs.storylane.io/analytics-and-performance/capture-leads) to learn more
{% endhint %}

***

### **How to Integrate**

* Click the "Settings" on the left-hand menu, and then click on the "Integration" tab.&#x20;
* Choose Webhook integration and click on the "Connect" button.&#x20;

<div align="left"><figure><img src="https://2431356420-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FN8hWd9k2Pbb6YSWO4pUQ%2Fuploads%2FsZYWdyFZaKGgkShqMY0C%2FScreenshot%202025-11-21%20at%2015.48.39.png?alt=media&#x26;token=82935544-a8d5-4ddf-80e6-f2ac64b1adfe" alt="" width="563"><figcaption></figcaption></figure></div>

* Paste the webhook URL (This is the URL where you want to receive webhooks)
* Lastly, decide whether you want to send webhooks for all demo sessions (unknown and known), or only when the lead or account is known.

***

### How do I verify the webhook signature? <a href="#how-do-i-verify-the-webhook-signature" id="how-do-i-verify-the-webhook-signature"></a>

To ensure that webhook requests are coming from Storylane and have not been tampered with, we recommend verifying the signature included in the request headers.

Every webhook request includes an `x-storylane-signature` header. This signature is an HMAC-SHA256 hash of the raw request body, generated using your Webhook Verification Secret and then Base64 encoded.

#### 1. Retrieve your Verification Secret

You can find your Verification Secret in the Storylane dashboard under **Settings > Integrations > Webhook**. Keep this secret secure; do not share it or commit it to public repositories.

#### 2. Verification Logic

To verify the signature:

1. Retrieve the `x-storylane-signature` header from the request.
2. Get the raw body of the request (unparsed string).
3. Compute the HMAC-SHA256 hash of the raw body using your Verification Secret as the key.
4. Base64 encode the resulting hash.
5. Compare your generated string with the value in the `x-storylane-signature` header.

#### 3. Implementation Examples

**Node.js (Express)**

When using Express, ensure you use the raw body for the HMAC calculation.

```js
const crypto = require('crypto');

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-storylane-signature'];
  const secret = process.env.STORYLANE_WEBHOOK_SECRET;

  // 'req.rawBody' should be the unparsed string of the request body
  const hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(req.rawBody).digest('base64');

  if (signature === digest) {
    // Valid signature
    console.log('Webhook verified');
    res.status(200).send('Verified');
  } else {
    // Invalid signature
    res.status(401).send('Unauthorized');
  }
});
```

**Ruby**

```ruby
require 'openssl'
require 'base64'

def verify_signature(payload, secret, received_signature)
  hmac = OpenSSL::HMAC.digest(
    'sha256',
    secret.to_s,
    payload
  )

  expected_signature = Base64.strict_encode64(hmac)

  # Use secure comparison to prevent timing attacks
  Rack::Utils.secure_compare(expected_signature, received_signature)
end
```

**Python (Flask)**

```python
import hmac
import hashlib
import base64
from flask import request

@app.route('/webhook', methods=['POST'])
def handle_webhook():
    secret = "your_verification_secret"
    received_signature = request.headers.get('x-storylane-signature')

    # Get raw data
    payload = request.get_data()

    # Create hash
    digest = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).digest()

    expected_signature = base64.b64encode(digest).decode()

    if hmac.compare_digest(expected_signature, received_signature):
        return "OK", 200
    else:
        return "Unauthorized", 401
```

***

### Webhook Example&#x20;

To help you understand what kind of data you can expect from our webhooks, here's a sample response payload for unknown and known demo sessions.

#### **Known demo sessions (known lead data)**

```json
{
  "id": "2f37f8ce-6ecb-49a2-be12-36ae9c063042",
  "buyer_reveal": {
    "company_name": "Acme Inc",
    "company_domain": "example.com",
    "company_annual_revenue_range": "1M-10M",
    "company_employee_range": "11-50"
  },
  "lead": {
    "id": "f3694255-aed4-4514-8ce6-ac55506a67fe",
    "client_source": "none",
    "client_tracking_id": null,
    "email": "john.doe@example.com",
    "first_name": "John",
    "last_name": "Doe"
  },
  "name": "Product Demo",
  "link": "https://app.storylane.io/share/aejr8lqfghlp",
  "viewed_at": "2024-05-17T10:58:05.337+00:00",
  "completion": 30,
  "time_spent": 125,
  "checklist_completed": false,
  "cta_opened": "https://example.com",
  "analytics_url": "https://app.storylane.io/analytics/activity/2f37f8ce-6ecb-49a2-be12-36ae9c063042",
  "utm_params": {
    "gclid": "123xyz",
    "utm_source": "source",
    "utm_medium": "medium",
    "utm_campaign": "campaign",
    "utm_term": "term",
    "utm_content": "content"
  }
}
```

#### &#x20;**Unknown demo sessions (unknown lead data)**

```json
{
  "id": "2f37f8ce-6ecb-49a2-be12-36ae9c063042",
  "buyer_reveal": null,
  "lead": null,
  "name": "Product Demo",
  "link": "https://app.storylane.io/share/aejr8lqfghlp",
  "viewed_at": "2024-05-17T10:58:05.337+00:00",
  "completion": 30,
  "time_spent": 125,
  "checklist_completed": false,
  "cta_opened": "https://example.com",
  "analytics_url": "https://app.storylane.io/analytics/activity/2f37f8ce-6ecb-49a2-be12-36ae9c063042",
  "utm_params": {
    "gclid": "123xyz",
    "utm_source": "source",
    "utm_medium": "medium",
    "utm_campaign": "campaign",
    "utm_term": "term",
    "utm_content": "content"
  }
}
```
