Help

Connect your own system (webhook)

Receive each article as signed JSON and publish it in any CMS.

Last updated October 7, 2026

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

  1. Enter your address

    In StrideRank, open Autopilot → Settings → Publishing, choose Webhook (JSON) and enter your Webhook address.

  2. 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.

  3. 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.

  4. Test the connection

    Click Test connection. We send a ping event; 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:

Body of article.publish
{
  "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
  }
}
FieldMeaning
eventarticle.publish for an article, ping for the connection test (then without article).
sentAtTime of sending (ISO 8601).
article.htmlThe finished article as HTML.
article.imagesImages with address, alt text and source; width and height when known.
article.schemaJsonLdStructured data for the article, ready to embed.
article.externalIdnull 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.

verify.js (Node.js 18+)
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);
}
server.js — minimal receiver
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 443

Using 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 as externalId when 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.