Webhooks

Push events to your stack.

Subscribe a URL to Storylayer events. Every delivery is HMAC-signed, retried with exponential backoff, and viewable in the dashboard.

Events

Five event types are published today. Subscribe to specific events or use "*" to fan out everything.

EventWhen
story.scheduledA story has a confirmed publish time.
story.publishedA story shipped successfully on at least one channel.
story.failedA scheduled story errored at publish time.
moment.detectedA detector fired against your data.
moment.auto_draftedA high-severity moment turned into a draft story.

Payload shape

All events share an envelope; the data field carries event-specific content.

POST <your-url>
content-type: application/json
x-storylayer-event: story.published
x-storylayer-event-id: evt_2T...
x-storylayer-timestamp: 1745948123
x-storylayer-signature: <hex>

{
  "id": "evt_2T...",
  "event": "story.published",
  "created_at": "2026-04-30T...",
  "data": {
    "story": {
      "id": "...",
      "title": "Snow report — 14 inches",
      "published_at": "2026-04-30T...",
      "published_channels": ["instagram","facebook"]
    }
  }
}

Signing

Each request carries an x-storylayer-signature header — a hex-encoded HMAC-SHA256 of the raw request body, keyed by the signing_secret returned when you create the endpoint. Always verify in constant time.

import crypto from "node:crypto";

const sig      = req.headers["x-storylayer-signature"];
const expected = crypto
  .createHmac("sha256", SIGNING_SECRET)   // whsec_...
  .update(rawBody)                        // raw bytes, NOT JSON.stringify(parsed)
  .digest("hex");

const ok = crypto.timingSafeEqual(
  Buffer.from(sig, "hex"),
  Buffer.from(expected, "hex"),
);

if (!ok) return res.status(401).end();

The signing_secret is shown once when you create an endpoint via POST /api/v1/webhooks. Store it securely; we never return it again. Use the dashboard or PATCH /api/v1/webhooks/:id to rotate it.

Retries

If your endpoint returns anything other than a 2xx within 10 seconds, we retry on this schedule:

  • 1 minute
  • 5 minutes
  • 15 minutes
  • 1 hour
  • 4 hours
  • 12 hours

After the sixth failure the delivery is marked permanent_failure and the endpoint's failure_count increments.

Health, alerts, and auto-disable

Each endpoint carries a health_status that reflects how the receiver is doing:

  • healthy — most recent delivery succeeded.
  • unhealthy — 3 consecutive deliveries failed; an in-app alert is raised.
  • auto_disabled — 10 consecutive deliveries failed; the endpoint is deactivated to avoid drowning your queue. You re-enable it once the receiver is back via the dashboard or POST /api/v1/webhooks/:id/reactivate.

The first successful delivery after a failure run resets the counter and clears the unhealthy state.

Replay

If your receiver was down for a window, you don't need to ask us to resend events — you can replay them yourself.

# single replay
curl -X POST https://app.storylayer.ai/api/v1/webhooks/wh_.../deliveries/dlv_.../replay \
  -H "Authorization: Bearer sl_pat_..."

# bulk replay (failed deliveries in a window)
curl -X POST https://app.storylayer.ai/api/v1/webhooks/wh_.../deliveries/replay \
  -H "Authorization: Bearer sl_pat_..." \
  -H "content-type: application/json" \
  -d '{
    "since": "2026-04-30T14:00:00Z",
    "until": "2026-04-30T16:00:00Z",
    "status": ["failed","permanent_failure"]
  }'

Replays are inserted as new deliveries with replay_of set to the original delivery id, so audit history is preserved.

Managing endpoints

From the dashboard at /dashboard/developers you can create endpoints, send a signed test ping, and inspect every delivery — including filtering by status and replaying individual or bulk deliveries with one click.

Or via the API:

# create
curl -X POST https://app.storylayer.ai/api/v1/webhooks \
  -H "Authorization: Bearer sl_pat_..." \
  -H "content-type: application/json" \
  -d '{
    "url": "https://hooks.acme.example/storylayer",
    "events": ["story.published","story.failed","moment.detected"],
    "description": "Production fan-out"
  }'

# test ping
curl -X POST https://app.storylayer.ai/api/v1/webhooks/wh_.../test \
  -H "Authorization: Bearer sl_pat_..."

# delivery history (filter with ?status=failed,permanent_failure)
curl https://app.storylayer.ai/api/v1/webhooks/wh_.../deliveries \
  -H "Authorization: Bearer sl_pat_..."

# alerts feed (in-app health events for your endpoints)
curl 'https://app.storylayer.ai/api/v1/webhooks/alerts?unread_only=true' \
  -H "Authorization: Bearer sl_pat_..."

# reactivate after auto-disable
curl -X POST https://app.storylayer.ai/api/v1/webhooks/wh_.../reactivate \
  -H "Authorization: Bearer sl_pat_..."

Best practices

  • Respond with 200 as soon as you've persisted the payload — do work asynchronously.
  • Verify the signature in constant time (timingSafeEqual), never ===.
  • De-duplicate using the x-storylayer-event-id header — retries and replays reuse the same id.
  • Allow at least one full minute of clock skew when checking x-storylayer-timestamp (rare, but helpful).
  • Subscribe to webhooks/alerts in your monitoring dashboard so a quietly-broken endpoint surfaces fast.