Webhook Integration
Receive published articles instantly on any endpoint — no polling required.
How it works
When an article is published in RivalRank (manually or via auto-publish), RivalRank immediately sends a POST request to your configured endpoint with the full article payload. Your server receives it, does whatever it needs — save to a database, trigger a static rebuild, call revalidatePath() — and responds with a 200.
The request is authenticated with a Bearer token you generate inside the Integrations page. Delivery is fire-and-forget: a failed delivery does not block or undo the publish.
Setting up
- Go to Dashboard → Integrations
- In the Webhook card, enter your endpoint URL (must be HTTPS and publicly reachable)
- Click Save endpoint
- A secret token is auto-generated and shown once — copy it and store it as
RIVALRANK_WEBHOOK_SECRETin your environment variables
If you need a new secret later, click Regenerate secret in the same card. Update your environment variable to match.
Verifying requests
Every request includes an Authorization header:
Authorization: Bearer <your-secret>
Always validate this before processing the payload. Reject requests that are missing or have a wrong token with a 401.
Payload
The request body is JSON with this shape:
{
"event_type": "publish_article",
"timestamp": "2026-04-29T08:00:00Z",
"data": {
"article": {
"id": "abc123",
"title": "Best Alternatives to Competitor X",
"slug": "best-alternatives-to-competitor-x",
"content_html": "<h1>...</h1>",
"meta_description": "...",
"created_at": "2026-04-28T10:00:00Z",
"published_at": "2026-04-29T08:00:00Z",
"published_url": "https://yoursite.com/best-alternatives...",
"image_url": "https://cdn.rivalrankai.com/...",
"tags": []
}
}
}| Field | Type | Description |
|---|---|---|
event_type | string | Always "publish_article" |
timestamp | ISO 8601 | When the publish completed |
data.article.id | string | RivalRank article ID |
data.article.title | string | Article title |
data.article.slug | string | URL-friendly slug |
data.article.content_html | string | Full article HTML |
data.article.meta_description | string | SEO meta description |
data.article.created_at | ISO 8601 | When the article was first created |
data.article.published_at | ISO 8601 | When this publish happened |
data.article.published_url | string | Live URL (WordPress or other), if available |
data.article.image_url | string | null | Hero image URL, or null if none |
data.article.tags | array | Tag strings (currently always empty, reserved for future use) |
Next.js example
Create a route handler in your Next.js app. This example validates the token, saves the article, and revalidates the relevant path so the page is updated immediately.
// app/api/rivalrank-webhook/route.js
import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
const TOKEN = process.env.RIVALRANK_WEBHOOK_SECRET;
export async function POST(req) {
// 1. Verify the bearer token
const auth = req.headers.get("authorization");
if (!auth || auth !== `Bearer ${TOKEN}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { event_type, data } = await req.json();
if (event_type === "publish_article") {
const { article } = data;
// 2. Save to your database
// await db.articles.upsert({ where: { id: article.id }, data: article });
// 3. Revalidate the page so visitors see the new content immediately
revalidatePath(`/blog/${article.slug}`);
revalidatePath("/blog"); // revalidate the listing page too
}
return NextResponse.json({ ok: true });
}Add your secret to .env.local (local) and your hosting environment (production):
RIVALRANK_WEBHOOK_SECRET=rrwh_your_secret_here
Plain Node.js / Express example
const express = require("express");
const app = express();
const TOKEN = process.env.RIVALRANK_WEBHOOK_SECRET;
app.use(express.json());
app.post("/webhooks/rivalrank", (req, res) => {
const auth = req.headers["authorization"];
if (!auth || auth !== `Bearer ${TOKEN}`) {
return res.status(401).json({ error: "Unauthorized" });
}
const { event_type, data } = req.body;
if (event_type === "publish_article") {
const { article } = data;
console.log("Published:", article.slug);
// handle your logic here
}
res.status(200).json({ ok: true });
});
app.listen(3000);Best practices
- Always verify the bearer token before processing — reject with
401if invalid - Respond quickly (within a few seconds). Offload slow work to a background queue if needed
- Return
200even if you choose to ignore a particular event — this prevents noise in server logs - Your endpoint must be publicly accessible over HTTPS. Local dev servers won't work — use a tunnelling tool like ngrok for testing
- If you change your secret, update
RIVALRANK_WEBHOOK_SECRETin your environment before saving the new secret in RivalRank
Troubleshooting
Endpoint not receiving requests
Confirm the URL is reachable from the internet (not localhost) and uses HTTPS. Check server logs and firewall rules.
401 errors
The token in RIVALRANK_WEBHOOK_SECRET must match exactly what is configured in Integrations. Check for trailing spaces or line breaks in your environment variable. Regenerate the secret if you are unsure.
500 errors from your server
Check your server logs for details. Common causes: JSON body not being parsed, unhandled promise rejections, or database errors.
Delivery is fire-and-forget
RivalRank does not retry failed deliveries. If your endpoint was down when an article was published, you can find the article data in the Articles section of your dashboard and re-publish from there.
Need help?
Contact us at hello@rivalrankai.com with your endpoint URL and the behaviour you're seeing.