# Storylane Webhooks

## When to Use This Skill

* How do I receive Storylane webhooks?
* How do I verify the `x-storylane-signature` header on a Storylane webhook?
* How do I handle a `demo_viewed` event with lead and buyer-reveal data?
* Why is my Storylane webhook signature verification failing?

## Verification (core)

Storylane sends `x-storylane-signature: <base64>`: the **standard padded base64 HMAC-SHA256 of the
raw request body**, keyed with the Webhook Verification Secret (its UTF-8 bytes, used as-is). There
is no prefix, and no timestamp or id is signed, so there is no replay window to check. Verify over
the **raw bytes before parsing**, validate the 44-character shape, then compare the exact strings in
constant time (decoding alone would accept non-canonical base64 that Hookdeck rejects).

Node:

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

function verifyStorylaneSignature(rawBody, signatureHeader, secret) {
  if (typeof signatureHeader !== 'string' || !/^[A-Za-z0-9+/]{43}=$/.test(signatureHeader)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64');
  return crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}

```

Python (compare bytes so a non-ASCII header yields 401, not a 500):

```python
import base64, hashlib, hmac, re

def verify_storylane_signature(raw_body: bytes, signature_header: str | None, secret: str) -> bool:
    if not signature_header or not re.fullmatch(r"[A-Za-z0-9+/]{43}=", signature_header, re.ASCII):
        return False
    digest = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).digest()
    return hmac.compare_digest(signature_header.encode("ascii"), base64.b64encode(digest))

```

Respond `401` for a missing, empty, malformed or mismatched signature. Express handlers must read the
body with `express.raw({ type: '*/*' })` so any content type verifies (Next.js:
`await request.arrayBuffer()`; FastAPI: `await request.body()`).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/storylane-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/storylane-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/storylane-webhooks/examples/fastapi/).

## Events

Dispatch on the top-level `event` field of the JSON body.

| Event | Triggered When |
| --- | --- |
| `demo_viewed` | A demo session is viewed (alert fires with session, lead and buyer-reveal data) |

Only `demo_viewed` is documented. Known and unknown visitors share it: `lead` is an object for a
known visitor and `null` otherwise. Log other event values rather than failing. Use the session `id`
as your idempotency key. Storylane's docs don't describe timeouts or retries.

## Environment Variables

```bash
STORYLANE_WEBHOOK_SECRET=your_webhook_verification_secret   # Webhook Verification Secret from Storylane

```

## Setup with Hookdeck

Paste the Webhook Verification Secret into the Hookdeck source's webhook verification settings
(Storylane source: HMAC-SHA256, header `x-storylane-signature`, base64). Hookdeck then rejects
unsigned or invalid deliveries before they reach your endpoint.

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 storylane --path /webhooks/storylane

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/storylane-webhooks/references/overview.md) - Storylane webhook concepts and the `demo_viewed` payload
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/storylane-webhooks/references/setup.md) - Configure a webhook alert and find the verification secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/storylane-webhooks/references/verification.md) - Signature verification details and gotchas