Gareth Wilson Gareth Wilson

Guide to Storylane Webhooks: Features and Best Practices

Published


Storylane is a platform for building interactive product demos that prospects click through on a website, in an email or in a sales follow-up. Its demo alerts can post to a webhook when someone views a demo, carrying the session's engagement figures, the lead if one was captured, and buyer-reveal data about the visitor's company. Sales and marketing teams use these to push warm leads into a CRM, alert a rep in Slack, or score intent.

This guide covers setting up a demo alert with a webhook destination, reading the demo_viewed payload, verifying the base64 x-storylane-signature, and the gaps in Storylane's delivery documentation that a receiver has to allow for.

What are Storylane webhooks?

Storylane webhooks are one destination of a demo alert, configured under Settings > Alerts on the Starter plan and above. When the alert's conditions are met for a demo session, Storylane sends a JSON summary of that session to your URL. You can add custom headers to every request, and a Test webhook button sends a sample before you save.

Only one event is documented, demo_viewed, and it covers both known visitors (where lead is an object) and anonymous ones (where lead is null). Storylane doesn't describe timeouts, retries or which status codes count as success.

Storylane webhook features

FeatureDetails
Signaturebase64 HMAC-SHA256
Signature headerx-storylane-signature
BodyJSON
Event routingBody field event

Events and payloads

EventTriggered WhenCommon Use Cases
demo_viewedA demo session is viewedPush leads to a CRM or Slack, score intent, enrich accounts

The event name is in the top-level event field, which comes last in the body. Route on it anyway, so that any future alert type falls through to a default branch instead of being treated as a view.

Event Payload Structure

Each body describes one demo session:

  • Session: id (the session ID), viewed_at, time_spent, completion, checklist_completed, cta_opened and intent_level.
  • Demo: name, link, link_name, host_url and analytics_url, which opens the session in Storylane.
  • Visitor: lead (with email, names, lead_source and tracking IDs, or null), location, referers and utm_params.
  • Company: buyer_reveal, with company name, domain, revenue and headcount ranges and a confidence_score.

Treat buyer_reveal as an estimate and check confidence_score before writing it into account records. first_name and last_name can be empty strings even when lead is present, as in the example below.

Published payload example

This is the demo_viewed sample from the webhook samples library, taken from Storylane's documentation:

{
  "id": "a61a8af4-8a33-4b4d-9861-d8542bcb4d36",
  "analytics_url": "https://app.storylane.io/audience/sessions/a61a8af4-8a33-4b4d-9861-d8542bcb4d36",
  "buyer_reveal": {
    "company_name": "Acme Inc",
    "company_domain": "example.com",
    "company_annual_revenue_range": "$1B-$10B",
    "company_employee_range": "1K-5K",
    "confidence_score": "low"
  },
  "checklist_completed": false,
  "completion": 100,
  "cta_opened": null,
  "host_url": "https://app.storylane.io/share/lcmplr8qd0rj",
  "intent_level": "high",
  "lead": {
    "id": "dcc7997a-c811-4778-a065-cc009ad6b46e",
    "client_source": "none",
    "client_tracking_id": null,
    "email": "webhooktest@test.com",
    "first_name": "",
    "last_name": "",
    "lead_source": "Nurtured"
  },
  "link": "https://app.storylane.io/share/lcmplr8qd0rj",
  "link_name": "Default",
  "location": {
    "city": "Pune",
    "country": "India",
    "state": "MH"
  },
  "name": "test for Pardot",
  "referers": [
    "https://app.storylane.io/share/lcmplr8qd0rj?email=webhooktest@test.com"
  ],
  "time_spent": 10,
  "utm_params": {},
  "viewed_at": "2026-05-20T03:27:29.246-07:00",
  "event": "demo_viewed"
}

Setting up Storylane webhooks

Prerequisites

  • A Storylane account on the Starter plan or above
  • Your endpoint URL (use npx hookdeck-cli listen 3000 storylane --path /webhooks/storylane locally)
  • The Webhook Verification Secret for your workspace or alert

Register Your Endpoint

  1. In Storylane, open Settings > Alerts and create or edit a demo alert.
  2. Turn on Webhooks and paste your endpoint URL.
  3. Add any custom headers your receiver needs, such as a routing key.
  4. Click Test webhook to send a sample demo_viewed delivery, then Save.

Storylane's docs name the Webhook Verification Secret without saying where it appears in the app or whether each alert has its own. Find it before you go live, and if you run several alerts, test each one against the secret you've stored.

Test a delivery

Start a local receiver and forward requests with the Hookdeck CLI:

npx hookdeck-cli listen 3000 storylane --path /webhooks/storylane

Paste the CLI URL into the alert and press Test webhook. Then view one of your demos as a known lead and as an anonymous visitor, and compare the two bodies.

Securing Storylane webhooks

Compute HMAC-SHA256 over the raw request body with the Webhook Verification Secret, base64-encode the result with the standard alphabet, and compare it with x-storylane-signature. Use a constant-time comparison: Storylane's own Node sample uses ===, but its Ruby and Python samples get this right. Nothing in the signature covers a time, so replays can't be detected from the signature alone. See how to verify the webhook signature.

Node.js verification

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-storylane-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler below verifies the signature, then upserts the session by id. Upserting rather than skipping repeats means a resent or updated alert for the same session replaces the stored copy instead of creating a duplicate lead. Anonymous sessions are stored without any CRM sync.

const express = require('express');

function createStorylaneApp({ upsertSession, syncLead }) {
  const app = express();
  app.post('/webhooks/storylane', express.raw({ type: '*/*' }), async (req, res) => {
    const secret = process.env.WEBHOOK_SECRET;
    if (!secret) return res.sendStatus(500);
    if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
    let session;
    try {
      session = JSON.parse(req.body.toString('utf8'));
    } catch {
      return res.sendStatus(400);
    }
    if (!session || typeof session !== 'object' || Array.isArray(session)) {
      return res.sendStatus(400);
    }
    if (!session.id || typeof session.event !== 'string') return res.sendStatus(400);
    if (session.event !== 'demo_viewed') {
      console.log('Unhandled Storylane event', session.event);
      return res.sendStatus(200);
    }
    try {
      const isNewSession = await upsertSession(session.id, session);
      if (isNewSession && session.lead?.email) await syncLead(session.lead, session);
    } catch {
      return res.sendStatus(503);
    }
    return res.sendStatus(200);
  });
  return app;
}

Whether Storylane retries a 503 isn't documented, so treat the response code as a signal for your own monitoring rather than a guarantee of redelivery.

A Hookdeck Storylane source performs the same base64 HMAC-SHA256 check on the body. It replies 200 to Storylane straight away, and a request that fails the check is kept as a failed verification instead of being delivered. There's no timestamp to enforce.

Catch every demo view. Hookdeck Event Gateway verifies Storylane's signature, stores each demo_viewed delivery and retries it to your CRM sync until it succeeds, so a busy sales day doesn't lose leads.

Storylane webhook limitations and pain points

No documented delivery guarantees

The Problem: You don't know whether a failed delivery will be tried again.

Why It Happens: Storylane documents no timeout, success code or retry policy for webhooks.

Workarounds:

  • Respond quickly and keep processing off the request path.
  • Reconcile against Storylane's analytics periodically to catch sessions your system missed.

How Hookdeck Can Help: Hookdeck acknowledges Storylane immediately and takes on retries to your service, so delivery to your code no longer depends on Storylane's undocumented behaviour.

One documented event

The Problem: You can't plan for other alert types or know when a session is "finished".

Why It Happens: The docs show only demo_viewed and give no event list.

Workarounds:

  • Route on event with a default branch that logs and acknowledges unknown values.
  • Upsert by session id so later alerts for the same session update it.

How Hookdeck Can Help: Hookdeck stores every request, so a new event value is visible in the request log the first time it appears.

Unclear secret scope

The Problem: Verification can fail for one alert and pass for another.

Why It Happens: The docs don't say where the Webhook Verification Secret is shown or whether it is per alert or per account.

Workarounds:

  • Test each alert with Test webhook after setting up verification.
  • If secrets turn out to differ, give each alert its own receiving URL.

How Hookdeck Can Help: One Hookdeck source per alert keeps each secret separate, and failed verifications are listed with their headers for debugging.

No replay protection

The Problem: A captured request can be resent and will still verify.

Why It Happens: Only the body is signed, with no timestamp or nonce.

Workarounds:

  • Upsert by session id, so a replayed body has no extra effect.
  • Use a custom header with a shared token as a second check if your threat model needs it.

How Hookdeck Can Help: Hookdeck's deduplication can drop exact repeats before they reach your service.

Best practices

Separate known and anonymous sessions

Only sync to the CRM when lead has an email. Anonymous sessions are still useful for account-level intent if buyer_reveal has a reasonable confidence_score.

Keep lead data out of logs

The body includes email addresses, names and location. Log the session id and event and keep the rest in your datastore.

Store analytics_url with the lead so reps can open the session in Storylane's analytics from the CRM record.

Re-test after alert changes

Use Test webhook whenever you change an alert's URL or headers, and confirm the sample verifies before saving.

Conclusion

Storylane's webhook gives you a detailed picture of each demo session, including lead and company data, in a single signed demo_viewed event. What it doesn't give you is any documented delivery behaviour, a list of other events or a clear home for the verification secret, so receivers should upsert by session, acknowledge fast and watch for unknown event values.

Hookdeck Event Gateway adds stored deliveries, retries and deduplication to Storylane's alerts.

Get started with Hookdeck for free and handle Storylane webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.