Agent skill
Storylane Webhooks Skill
Receive and verify Storylane webhooks. Use when setting up Storylane demo alert webhook handlers, debugging x-storylane-signature verification, or handling demo_viewed events with session, lead and buyer-reveal data.
Install this skill
npx skills add hookdeck/webhook-skills --skill storylane-webhooks
When to Use This Skill
- How do I receive Storylane webhooks?
- How do I verify the
x-storylane-signatureheader on a Storylane webhook? - How do I handle a
demo_viewedevent 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:
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):
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/, examples/nextjs/, 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
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
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 storylane --path /webhooks/storylane
Reference Materials
- references/overview.md - Storylane webhook concepts and the
demo_viewedpayload - references/setup.md - Configure a webhook alert and find the verification secret
- references/verification.md - Signature verification details and gotchas