Publish to Your Website

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

HeaderDescription
X-TrendScribbr-Eventarticle.published when you publish, ping for test events
X-TrendScribbr-TimestampUnix time the request was signed
X-TrendScribbr-Signaturesha256= followed by the hex HMAC-SHA256 of {timestamp}.{raw request body}, keyed with your signing secret
X-TrendScribbr-DeliveryUnique 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.

json
{ "event": "article.published", "delivery_id": "5f0c9f7e-2b8f-4f7e-9a51-0d3c1e7f6b21", "created_at": "2026-10-30T09:00:00+00:00", "data": { "article": { "id": 482, "title": "How Pay-As-You-Go Solar Is Changing Off-Grid Power", "slug": "how-pay-as-you-go-solar-is-changing-off-grid-power", "status": "published", "excerpt": "Access to dependable electricity remains a daily challenge...", "html": "<h2>Unlocking affordable solar</h2><p>...</p>", "markdown": "# How Pay-As-You-Go Solar...\n\n## Unlocking affordable solar...", "content_type": "article", "word_count": 1450, "reading_time_minutes": 7, "brand": {"id": 12, "name": "Acme Solar"}, "created_at": "2026-10-29T14:12:03+00:00", "updated_at": "2026-10-30T08:55:41+00:00" } } }

Option 2: Content API (pull)

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.

ParameterTypeRequiredDescription
statusstringNodraft, published or archived
updated_sinceISO 8601 datetimeNoOnly articles updated after this time, e.g. 2026-10-01T00:00:00Z
pageintegerNoPage number, starting at 1
page_sizeintegerNoResults per page, 1–50 (default 20)
bash
curl "https://www.trendscribbr.com/agents/api/v1/articles/?status=draft" \ -H "X-API-Key: your-api-key-here"
200 OK
{ "count": 42, "page": 1, "page_size": 20, "has_more": true, "results": [ { "id": 482, "title": "...", "slug": "...", "status": "draft", "html": "...", "markdown": "...", ... } ] }
GET /v1/articles/{id}/

Returns one article with the same fields as the webhook payload's article.

POST /v1/articles/{id}/mark-published/

Marks the article as published in TrendScribbr once it's live on your site, so it stops appearing in ?status=draft.

Rate limits

600 requests per hour per API key. Exceeding it returns 429.