RankPine docs
Integrations

Webhook / REST

Send each finished article as a JSON payload to any HTTPS endpoint you control — for custom systems, static sites, or any CMS RankPine doesn't natively support.

The webhook integration is the most flexible option: when an article is ready, RankPine sends it as a JSON payload via HTTP POST to an endpoint you control. Use it to push articles into a custom CMS, trigger a static-site rebuild, drop files in a repo, or anything else you can script.

This is the developer-oriented integration. If your blog runs on WordPress, Ghost, Webflow, or Notion, the dedicated guides for those are simpler. Reach for the webhook when nothing else fits.

What you'll need

  • A public HTTPS endpoint that accepts POST requests and responds with a 2xx status

Connect in RankPine

Go to Integrations, click Connect on Webhook, and fill in the dialog:

The Connect a webhook dialog in RankPine

FieldWhat to enter
Endpoint URLA public HTTPS URL, e.g. https://api.yoursite.com/hooks/rankpinerequired
Signing secret(optional) Used to HMAC-sign each request so you can verify it came from RankPine
Auth header(optional) A custom header name to send, e.g. Authorization
Header value(optional) The value for that header, e.g. Bearer your-token
Include MarkdownOn by default — include the article body as Markdown
Include HTMLOn by default — include the article body as HTML

When you connect, RankPine sends a verification ping to your endpoint. It must respond 2xx or the connection won't save.

Verification ping

{
  "event": "verification",
  "sentAt": "2026-06-29T09:00:00.000Z"
}

Respond with any 2xx status to confirm the endpoint is reachable.

The publish payload

When an article publishes, RankPine sends a POST with this body:

{
  "event": "article.published",
  "publishedAt": "2026-06-29T09:00:00.000Z",
  "site": {
    "name": "Acme",
    "url": "https://acme.com",
    "language": "en"
  },
  "article": {
    "title": "How to choose a CRM",
    "slug": "how-to-choose-a-crm",
    "metaDescription": "A practical guide to picking the right CRM…",
    "language": "en",
    "translationGroupId": null,
    "wordCount": 1840,
    "seoScore": 86,
    "sources": [{ "url": "https://example.com/study", "title": "Industry study" }],
    "markdown": "# How to choose a CRM\n\n…",
    "html": "<h1>How to choose a CRM</h1>…"
  },
  "images": [{ "url": "https://cdn.rankpine.com/…/cover.png", "alt": "CRM dashboard" }]
}

Notes:

  • article.markdown is included only if Include Markdown is on; article.html only if Include HTML is on.
  • images are hosted on RankPine's CDN — the first is the intended featured image.
  • Inline images inside html/markdown already point at those same CDN URLs.
  • article.language is the language this article is written in. site.language is your site's main language — on a multi-language site they differ.
  • article.translationGroupId is null unless the article has translations. See below.

Multi-language sites

If you've set additional languages, each version of an article is published separately — one POST per language, each with its own slug, its own language, and its own id/url in your response. There is no single request containing all of them.

Every version of the same article shares one translationGroupId, so you can tell which posts belong together and wire up hreflang yourself:

// three separate POSTs
{ "article": { "slug": "how-to-choose-a-crm",    "language": "en", "translationGroupId": "a1b2c3…" } }
{ "article": { "slug": "crm-auswahl",            "language": "de", "translationGroupId": "a1b2c3…" } }
{ "article": { "slug": "como-elegir-un-crm",     "language": "es", "translationGroupId": "a1b2c3…" } }

Storing translationGroupId pays off later

Keep it alongside each post. It's the only thing tying the versions together — the slugs differ by design, and translations can arrive minutes apart, so you can't rely on timing to group them.

The group id is the original article's own id, so the original arrives with translationGroupId equal to its own identity, and every translation carries the same value. A site with no additional languages always sends null.

Request headers

HeaderValue
Content-Typeapplication/json
Acceptapplication/json
X-RankPine-Eventarticle.published
X-RankPine-Signaturesha256=<hex> — present only if you set a signing secret
your custom headerthe auth header name/value you configured, if any

Your response

Respond 2xx to acknowledge. Optionally return JSON to tell RankPine where the article landed:

{ "id": "post_123", "url": "https://acme.com/blog/how-to-choose-a-crm" }

If you return an id and url, RankPine records them against the article. If you don't, that's fine — a 2xx is all that's required.

Verifying the signature

If you set a signing secret, every request includes an X-RankPine-Signature header. It's an HMAC-SHA256 of the raw request body, hex-encoded, prefixed with sha256=. Recompute it with your secret and compare — reject the request if it doesn't match.

import crypto from "node:crypto";

function isValid(rawBody, signatureHeader, secret) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  // Compare the raw bytes you received, before JSON parsing.
  return signatureHeader === expected;
}

Hash the raw body

Compute the HMAC over the exact bytes you received, before any JSON parsing or re-serialisation — re-encoding can change the bytes and break the comparison.

Troubleshooting

On this page