Webhooks
Push job completions over HTTP to your endpoint — instead of polling.
Webhooks are the push path of the Rankion API: instead of building a polling loop after each 202 Accepted, you register an HTTPS endpoint and Rankion delivers job completions via POST as soon as they finish. Ideal for integrations with Zapier/Make, your own backends, or Slack notifications. Webhooks are complementary to Automation (internal pipelines) — they leave the platform.
Status: outbound webhooks are in planning. Today only an inbound webhook exists for external review sources:
POST /api/v1/webhooks/reviews/{source}(key-based, no Sanctum, see Review Sources). A general UI under/settings/webhooksfor subscription management and HMAC signing is not rolled out yet. The event list, payload envelope, and HMAC verification described below are the planned convention — they may still change before release. For immediate job-status updates: use REST polling on the relevant detail endpoint.
Configuration (UI) — planned
Webhook endpoints will (after release) be created in the frontend under /settings/webhooks:
- Enter the endpoint URL (HTTPS required)
- Subscribe to event types (multi-select)
- Copy the signing secret (shown only once)
- Click "Send test payload" — the endpoint must respond with
2xxwithin 5 seconds, otherwise the test counts as failed
Event types
| Event | Trigger |
|---|---|
article.generated |
POST /v1/articles/{id}/generate job done |
article.optimized |
POST /v1/articles/{id}/optimize job done |
article.published |
CMS publish completed (or failed) |
tracking.run.completed |
Tracking run completed across all LLM platforms |
tracking.report.ready |
generate-report job done |
content_audit.completed |
content-audits crawl finished |
content_optimizer.completed |
Optimizer analysis done |
competitor_analysis.completed |
Competitor analysis done |
backlinks.refreshed |
Delta pull or full pull completed |
humanize.batch.completed |
Humanizer batch done |
bulk_generation.completed |
Bulk article generation done |
pipeline.stage.completed |
Pipeline stage completed (for stage-by-stage reactions) |
keyword.expansion.completed |
A→Z expansion done |
Payload structure
All events share the same envelope:
{
"id": "evt_01HW9X8KRJV4M5PXAM",
"type": "article.generated",
"created_at": "2026-04-30T12:14:22Z",
"team_id": 3,
"data": {
"article_id": 88,
"project_id": 12,
"status": "ready",
"credits_used": 5,
"url": "https://rankion.ai/api/v1/articles/88"
}
}
The data object varies by event type. For details, fetch the full resource via data.url through the REST API — webhook payloads stay deliberately small to respect body limits in receivers (Slack, Zapier).
Example tracking.run.completed
{
"id": "evt_01HW9XBQS...",
"type": "tracking.run.completed",
"created_at": "2026-04-30T15:00:11Z",
"team_id": 3,
"data": {
"tracking_project_id": 7,
"run_id": 142,
"platforms": ["chatgpt","perplexity","claude"],
"avi_score": 64,
"delta_vs_previous": "+8",
"url": "https://rankion.ai/api/v1/tracking-projects/7/avi"
}
}
HMAC signature
Each request includes an X-Rankion-Signature header — an HMAC-SHA256 signature of the raw body using the signing secret. Always verify it before trusting the payload.
import hmac, hashlib
def verify(body: bytes, signature_header: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header.replace("sha256=", ""))
function rankion_verify_webhook(string $rawBody, string $sigHeader, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, str_replace('sha256=', '', $sigHeader));
}
Header format: X-Rankion-Signature: sha256=<hex>. Additionally: X-Rankion-Event (event type) and X-Rankion-Delivery (unique delivery ID — ideal for idempotency checks).
Idempotency
Because of retries the same event can be delivered multiple times. Persist incoming X-Rankion-Delivery IDs for at least 24 h and ignore repeats.
Retry logic
If your endpoint does not respond with 2xx within 5 seconds, Rankion retries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | +30 s |
| 3 | +5 min |
| 4 | +30 min |
| 5 | +2 h |
| 6 | +6 h |
After the 6th failed attempt the subscription is automatically deactivated and you receive an email notification. Reactivate via the UI after fixing it.
4xx responses (e.g. 401, 403, 410) count as permanent failures → no retry, immediate deactivation. 5xx and timeouts trigger the retry loop.
Example: Express receiver
import express from "express";
import crypto from "crypto";
const app = express();
const SECRET = process.env.RANKION_WEBHOOK_SECRET;
app.post("/webhooks/rankion",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["x-rankion-signature"]?.replace("sha256=","");
const expected = crypto.createHmac("sha256", SECRET)
.update(req.body).digest("hex");
if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString());
console.log(`Got ${event.type}:`, event.data);
// ack first, process async
res.status(200).send("ok");
handleEvent(event).catch(console.error);
}
);
Notes & pitfalls
- Respond fast, process async. Heavy work in the receiver blocks the 5 s timer. Return
200first, then queue. - HTTPS only. HTTP URLs are rejected directly in the UI.
- Local testing: ngrok / cloudflared tunnel onto a local port → enter the URL into the webhook UI.
- No order guarantee. If you trigger
article.generatedandarticle.optimizedfor the same article in quick succession, they can arrive unsorted. Sort bycreated_atin the receiver. - The webhooks section is still young. If an event type you need is missing → the same effect can be achieved internally via Automation pipelines, or file a feature request via support.
Related: API overview · Automation · Credits.