← 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
- Register a URL endpoint that accepts HTTP POST
- Threadline sends a JSON payload when subscribed events occur
- Your server returns a
2xxstatus code - Non-2xx or timeout (10s) triggers up to 3 retries with exponential backoff (30s → 2m → 10m)
- Delivery attempts appear in the site Webhooks delivery log
Available Events
| Event | Description |
|---|---|
comment.created | New comment posted (including pending moderation and guests) |
comment.updated | Comment edited by author |
comment.deleted | Comment deleted |
comment.flagged | Visitor flagged a comment |
comment.approved | Comment approved by moderator |
comment.rejected | Comment rejected / removed by moderator |
moderation.action | Any moderation action with action metadata |
user.created | First-time commenter on your site |
user.banned | User banned (platform or site moderation) |
subscription.changed | Site 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