Use these if your site isn't on WordPress, Ghost or Wix. There are two ways to get articles into it: TrendScribbr pushes each article to a webhook on your site when you click Publish, or your site pulls articles with the Content API.
Option 1: Webhook (push)
In the app, go to Integrations → Custom Website, enter your webhook URL and save. You'll get a signing secret; store it on your site as TRENDSCRIBBR_SECRET. Then use Send test event to check the connection.
When you publish an article to Custom Website, TrendScribbr sends a POST request with a JSON body to your URL. Any 2xx response counts as success. If you reply with {"url": "https://..."}, the app links to the live post.
Headers
Header
Description
X-TrendScribbr-Event
article.published when you publish, ping for test events
X-TrendScribbr-Timestamp
Unix time the request was signed
X-TrendScribbr-Signature
sha256= followed by the hex HMAC-SHA256 of {timestamp}.{raw request body}, keyed with your signing secret
X-TrendScribbr-Delivery
Unique ID for this delivery
Verifying requests
Recompute the signature over the raw request body (before parsing JSON), compare it in constant time, and reject timestamps more than 5 minutes old. Each receiver below does this and is ready to paste into your project:
Node / Express
import express from "express";
import crypto from "node:crypto";
const app = express();
const SECRET = process.env.TRENDSCRIBBR_SECRET;
// Use the raw body: the signature is computed over the exact bytes sent.
app.post("/api/trendscribbr", express.raw({ type: "application/json" }), async (req, res) => {
const timestamp = req.get("X-TrendScribbr-Timestamp") || "";
const signature = req.get("X-TrendScribbr-Signature") || "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET)
.update(`${timestamp}.`).update(req.body).digest("hex");
const valid = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
if (event.event === "article.published") {
const article = event.data.article;
// TODO: create or update your post, keyed by article.id
// (article.title, article.slug, article.html, article.excerpt, article.status)
return res.json({ url: `https://example.com/blog/${article.slug}` });
}
res.sendStatus(200); // "ping" test events
});
app.listen(3000);
Next.js
// app/api/trendscribbr/route.ts
import crypto from "node:crypto";
export async function POST(req: Request) {
const body = await req.text();
const timestamp = req.headers.get("x-trendscribbr-timestamp") ?? "";
const signature = req.headers.get("x-trendscribbr-signature") ?? "";
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.TRENDSCRIBBR_SECRET!)
.update(`${timestamp}.${body}`)
.digest("hex");
const valid = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!valid || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(body);
if (event.event === "article.published") {
const { article } = event.data;
// TODO: save to your CMS or database, keyed by article.id
return Response.json({ url: `https://example.com/blog/${article.slug}` });
}
return new Response("ok"); // "ping" test events
}
PHP
<?php
// e.g. public/trendscribbr-webhook.php
$secret = getenv('TRENDSCRIBBR_SECRET');
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TRENDSCRIBBR_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_TRENDSCRIBBR_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expected, $signature) || abs(time() - (int) $timestamp) > 300) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($body, true);
if ($event['event'] === 'article.published') {
$article = $event['data']['article'];
// TODO: insert or update your post, keyed by $article['id'], using $article['html']
header('Content-Type: application/json');
echo json_encode(['url' => 'https://example.com/blog/' . $article['slug']]);
exit;
}
http_response_code(200); // "ping" test events
Python / Flask
import hashlib
import hmac
import os
import time
from flask import Flask, abort, jsonify, request
app = Flask(__name__)
SECRET = os.environ["TRENDSCRIBBR_SECRET"].encode()
@app.post("/api/trendscribbr")
def trendscribbr_webhook():
body = request.get_data() # raw bytes, before any JSON parsing
timestamp = request.headers.get("X-TrendScribbr-Timestamp", "")
signature = request.headers.get("X-TrendScribbr-Signature", "")
expected = "sha256=" + hmac.new(SECRET, f"{timestamp}.".encode() + body, hashlib.sha256).hexdigest()
if (not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300
or not hmac.compare_digest(expected, signature)):
abort(401)
event = request.get_json()
if event["event"] == "article.published":
article = event["data"]["article"]
# TODO: create or update your post, keyed by article["id"], using article["html"]
return jsonify(url=f"https://example.com/blog/{article['slug']}")
return "", 200 # "ping" test events
Payload
Publishing the same article again sends the same id, so treat each delivery as create-or-update. html is sanitised and ready to render, and markdown is included if you prefer to render it yourself. status is published or draft, depending on what was chosen when publishing.
Fetch articles from your site, a build step or a scheduled job. Authenticate with an API key in the X-API-Key header (see Authentication). A key created for a specific brand only returns that brand's articles; an account-wide key returns all of yours.
GET/v1/articles/
Lists articles, most recently updated first.
Parameter
Type
Required
Description
status
string
No
draft, published or archived
updated_since
ISO 8601 datetime
No
Only articles updated after this time, e.g. 2026-10-01T00:00:00Z