Set up a webhook on your site once and every future RankGoat post arrives there automatically. No copy-paste, no submitting URLs back. We POST the post, your endpoint publishes it, returns the URL, we verify the links - done.
BB_WEBHOOK_SECRET) so it can verify the HMAC signature we send.ping event. Your endpoint should respond 200.POST <your endpoint URL>
Content-Type: application/json
X-BB-Event: post.publish
X-BB-Timestamp: 1715900000
X-BB-Signature: sha256=<hex hmac of the raw request body, using your shared secret>
User-Agent: RankGoat/1.0 (+https://rankgoat.app)
{
"event": "post.publish",
"timestamp": 1715900000,
"post": {
"id": 42,
"site_id": 7,
"title": "Five Ways to Speed Up Your Static Site",
"slug": "five-ways-to-speed-up-your-static-site",
"meta_description": "Performance tricks that take a Hugo or Next.js site from B to A in Lighthouse.",
"body_html": "<p>...</p>",
"hero_image_url": "https://<our-host>/post-images/post-42.png",
"word_count": 1180
},
"outbound_links": [
{ "target_domain": "techreview.io", "anchor": "modern build pipelines", "href": "https://techreview.io" },
{ "target_domain": "saashub.dev", "anchor": "SaaS boilerplate kits", "href": "https://saashub.dev" }
]
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "published_url": "https://yoursite.com/blog/five-ways-to-speed-up-your-static-site" }
Compute the HMAC of the raw request body bytes using your shared secret. Compare with the value in X-BB-Signature after the sha256= prefix. Use a constant-time compare to avoid timing attacks. Reject if the signature doesn't match, or if X-BB-Timestamp is more than 5 minutes off.
published_url and immediately fetch it to verify the outbound links.import express from "express";
import crypto from "node:crypto";
const app = express();
// IMPORTANT: capture the raw body so HMAC matches the bytes we signed.
app.use(express.raw({ type: "application/json" }));
const SECRET = process.env.BB_WEBHOOK_SECRET;
app.post("/api/bb-publish", async (req, res) => {
const sigHeader = req.get("X-BB-Signature") || "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sigHeader), Buffer.from(expected))) {
return res.status(401).json({ error: "bad signature" });
}
const payload = JSON.parse(req.body.toString("utf8"));
if (payload.event === "ping") return res.json({ ok: true });
// Create the post in your CMS however you like.
const slug = payload.post.slug;
await db.posts.insert({
title: payload.post.title,
slug,
body_html: payload.post.body_html,
meta_description: payload.post.meta_description,
published_at: new Date(),
});
res.json({ published_url: `https://yoursite.com/blog/${slug}` });
});
app.listen(8080);
<?php
// mu-plugins/bb-publish.php - handles incoming RankGoat webhooks
add_action("rest_api_init", function () {
register_rest_route("bb/v1", "/publish", [
"methods" => "POST",
"callback" => "bb_handle_publish",
"permission_callback" => "__return_true",
]);
});
function bb_handle_publish(WP_REST_Request $req) {
$secret = getenv("BB_WEBHOOK_SECRET");
$body = $req->get_body();
$sig = $req->get_header("X-BB-Signature") ?: "";
$expected = "sha256=" . hash_hmac("sha256", $body, $secret);
if (!hash_equals($expected, $sig)) {
return new WP_REST_Response(["error" => "bad signature"], 401);
}
$payload = json_decode($body, true);
if ($payload["event"] === "ping") return ["ok" => true];
$post_id = wp_insert_post([
"post_title" => $payload["post"]["title"],
"post_name" => $payload["post"]["slug"],
"post_content" => $payload["post"]["body_html"],
"post_excerpt" => $payload["post"]["meta_description"],
"post_status" => "publish",
"post_type" => "post",
]);
return ["published_url" => get_permalink($post_id)];
}
import hmac, hashlib, os, json
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = os.environ["BB_WEBHOOK_SECRET"].encode()
@app.post("/api/bb-publish")
def bb_publish():
body = request.get_data()
sig = request.headers.get("X-BB-Signature", "")
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
return jsonify(error="bad signature"), 401
payload = json.loads(body)
if payload["event"] == "ping":
return jsonify(ok=True)
slug = payload["post"]["slug"]
your_cms.create_post(
title=payload["post"]["title"],
slug=slug,
body=payload["post"]["body_html"],
meta=payload["post"]["meta_description"],
)
return jsonify(published_url=f"https://yoursite.com/blog/{slug}")
Ghost ships with native webhook support but only as an outgoing notifier, not an HTTP-callable post creator. For Ghost you have two routes:
rel attributes on the embedded links. Our daily monitor will catch nofollow/sponsored/ugc additions, and repeated incidents suspend your account.