← Back to Docs

Webhooks

Get notified when events happen on your site. Requires Publisher plan or above. Configure URL, secret, and events in Dashboard → Site → Webhooks.

How It Works

  1. Register a URL endpoint that accepts HTTP POST
  2. Threadline sends a JSON payload when subscribed events occur
  3. Your server returns a 2xx status code
  4. Non-2xx or timeout (10s) triggers up to 3 retries with exponential backoff (30s → 2m → 10m)
  5. Delivery attempts appear in the site Webhooks delivery log

Available Events

EventDescription
comment.createdNew comment posted (including pending moderation and guests)
comment.updatedComment edited by author
comment.deletedComment deleted
comment.flaggedVisitor flagged a comment
comment.approvedComment approved by moderator
comment.rejectedComment rejected / removed by moderator
moderation.actionAny moderation action with action metadata
user.createdFirst-time commenter on your site
user.bannedUser banned (platform or site moderation)
subscription.changedSite owner subscription created, updated, or canceled

Payload Format

POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-Threadline-Signature: sha256=abc123...
X-Threadline-Event: comment.created
X-Threadline-Delivery: del_xyz789

{
  "event": "comment.created",
  "site_id": "uuid-of-your-site",
  "timestamp": "2025-01-15T10:30:00Z",
  "data": {
    "comment": {
      "id": "cmt_abc123",
      "thread_url": "https://example.com/post-1",
      "author_name": "Jane",
      "content": "Great article!",
      "status": "published"
    }
  }
}

Signature Verification

Compute HMAC-SHA256 of the raw request body with your webhook secret, prefix with sha256=, and compare to X-Threadline-Signature using a timing-safe compare.

import crypto from 'crypto';

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Example receivers

Node (Express)

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.THREADLINE_WEBHOOK_SECRET;

app.post(
  '/webhooks/threadline',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.get('X-Threadline-Signature') || '';
    const rawBody = req.body; // Buffer

    const expected =
      'sha256=' +
      crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex');

    const sigBuf = Buffer.from(signature);
    const expBuf = Buffer.from(expected);
    if (
      sigBuf.length !== expBuf.length ||
      !crypto.timingSafeEqual(sigBuf, expBuf)
    ) {
      return res.status(401).send('Invalid signature');
    }

    const payload = JSON.parse(rawBody.toString('utf8'));

    if (payload.event === 'comment.created') {
      const comment = payload.data && payload.data.comment;
      console.log('New comment', payload.site_id, comment && comment.id);
    }

    res.status(200).json({ received: true });
  }
);

app.listen(3000);

Python (Flask)

import hmac
import hashlib
import os
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["THREADLINE_WEBHOOK_SECRET"]

def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

@app.post("/webhooks/threadline")
def threadline_webhook():
    raw_body = request.get_data()
    signature = request.headers.get("X-Threadline-Signature", "")

    if not verify_signature(raw_body, signature, WEBHOOK_SECRET):
        return ("Invalid signature", 401)

    payload = request.get_json(force=True)
    print(payload.get("event"), payload.get("site_id"))
    return jsonify({"received": True})

Management API

Update webhook settings with authenticated PATCH /api/sites/:id:

{
  "webhook_url": "https://your-server.com/webhooks/threadline",
  "webhook_secret": "whsec_your_secret",
  "webhook_events": ["comment.created", "comment.flagged", "moderation.action"]
}

Delivery log: GET /api/sites/:id/webhooks/deliveries