How it works
If your website runs on a system without a built-in connection, StrideRank can send every article to an address you run. We send a POST request with JSON, signed with a secret only you and StrideRank know. Your endpoint publishes the article and answers with its ID and public address.
- The address must start with
https://, use the standard port 443 and resolve to a public IP address. Private and local addresses are refused. - Redirects are not followed — enter the final address.
- Your endpoint has 20 seconds to answer; the answer may be at most 2 MB.
Set it up
Enter your address
In StrideRank, open Autopilot → Settings → Publishing, choose Webhook (JSON) and enter your Webhook address.
Save and copy the signing secret
Click Save connection. StrideRank creates a signing secret and shows it only once. Copy it into your server’s configuration right away.
Build the receiver
Verify the signature of every request and answer as described below. The example further down is a complete starting point in Node.js.
Test the connection
Click Test connection. We send a
pingevent; any 2xx status counts as success.
Lost the secret? It cannot be shown again. Click New signing secret — the old one stops working immediately, so update your server right after.
What we send
Headers: Content-Type: application/json and X-Seo-Autopilot-Signature. The body for a new article:
{
"event": "article.publish",
"sentAt": "2026-10-07T14:03:22.512Z",
"article": {
"title": "How often should you flush a water heater?",
"slug": "how-often-flush-water-heater",
"html": "<p>…</p>",
"metaTitle": "How Often to Flush a Water Heater",
"metaDescription": "A plumber’s guide to …",
"images": [
{ "url": "https://…", "alt": "…", "source": "…", "width": 1200, "height": 800 }
],
"schemaJsonLd": { "@context": "https://schema.org", "@type": "Article" },
"externalId": null
}
}| Field | Meaning |
|---|---|
event | article.publish for an article, ping for the connection test (then without article). |
sentAt | Time of sending (ISO 8601). |
article.html | The finished article as HTML. |
article.images | Images with address, alt text and source; width and height when known. |
article.schemaJsonLd | Structured data for the article, ready to embed. |
article.externalId | null for a new article. When we update an article you published before, it carries the id you returned — update that post instead of creating a new one. |
Verify the signature
The header looks like t=1791381802,v1=5f2b…. t is the Unix time in seconds, v1 is the hex HMAC-SHA256 of the text <t>.<raw body> with your signing secret as key. Compute it over the raw bytes you received — parsing and re-serializing the JSON changes them. Compare in constant time and reject old timestamps so a recorded request cannot be replayed.
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300; // reject requests older than 5 minutes
// rawBody: the request body exactly as received (a Buffer), before JSON.parse
export function verifySignature(rawBody, header, secret) {
if (typeof header !== 'string') return false;
const parts = Object.fromEntries(
header.split(',').map((part) => part.trim().split('=', 2)),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || !/^[0-9a-f]{64}$/.test(parts.v1 ?? '')) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = createHmac('sha256', secret)
.update(`${t}.`)
.update(rawBody)
.digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && timingSafeEqual(received, expected);
}import { createServer } from 'node:http';
import { verifySignature } from './verify.js';
const SECRET = process.env.STRIDERANK_SIGNING_SECRET;
createServer((req, res) => {
if (req.method !== 'POST') return res.writeHead(405).end();
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', async () => {
const raw = Buffer.concat(chunks);
if (!verifySignature(raw, req.headers['x-seo-autopilot-signature'], SECRET)) {
return res.writeHead(401).end();
}
const body = JSON.parse(raw.toString('utf8'));
if (body.event === 'ping') {
return res.writeHead(200, { 'Content-Type': 'application/json' }).end('{"ok":true}');
}
if (body.event === 'article.publish') {
// Your CMS: create the post, or update it when body.article.externalId is set.
const post = await savePost(body.article);
res.writeHead(200, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ id: post.id, url: post.url }));
}
res.writeHead(400).end();
});
}).listen(3000); // put it behind your https server or proxy on port 443Using Express? Read the body for this route with express.raw({ type: 'application/json' }) so req.body is the raw Buffer the signature was made over.
The response we expect
For article.publish, answer with a 2xx status and JSON like {"id": "123", "url": "https://example.com/blog/how-often-flush-water-heater"}.
id: string or number, 1–200 visible ASCII characters. We send it back asexternalIdwhen the article is updated.url: the public address of the article (http or https). It is shown in StrideRank as “View on your website”.- 401 or 403 marks the connection as failed — someone has to look at it.
- Temporary problems (timeouts, 429, 5xx) are retried automatically later. Other 4xx answers mark the article as failed with the status code.
Questions
Do I have to answer the ping with JSON?
No. The connection test only needs a 2xx status. Only article.publish needs the JSON answer with id and url.
Is the webhook the same for every article?
Yes. Every article goes to the one address you saved for this website, signed with the current secret.