Try the RankNibbler SEO API

Webhooks

Webhooks send RankNibbler events to your own systems as they happen. When a monitor goes down, a scan finishes, a watched page changes or a keyword moves, RankNibbler posts a signed JSON request to your https endpoint, or a readable message straight into Slack or Discord.

Webhooks live in Settings › Webhooks in the app. Anyone with the Integrations permission can view them. Adding, editing or deleting endpoints, sending tests and seeing signing secrets needs the workspace owner, an admin, or the Manage API keys permission.

Setting up an endpoint

  1. Open Settings › Webhooks and click Add webhook.
  2. Give the endpoint a name so you can recognise it later.
  3. Paste the URL. It must use https. Private, local and cloud-metadata addresses are refused.
  4. Choose the events to send, or pick All events, which also includes events added in future.
  5. Save, then copy the signing secret. It starts with whsec_ and your server uses it to verify requests.
  6. Click Send test to post a ping event, then check the delivery log to see how your endpoint replied.

You can add up to 10 endpoints per workspace. Each one can be switched off and back on without deleting it. You can reveal the signing secret again, or replace it, from the endpoint's details. A replaced secret takes effect immediately, so update your server at the same time.

Events

EventSent when
uptime.downAn uptime monitor stops responding.
uptime.upA monitor that was down responds again.
scan.completedA site scan completes or fails.
change.detectedA page watched by Change Monitor changes. Not sent for the first capture.
rank.changedA tracked keyword's position moves, or it enters or leaves the top 100. Not sent on a keyword's first check.
pingOnly sent by the Send test button.

Request format

Each event is sent as a POST request with a JSON body. These headers are included:

HeaderValue
Content-Typeapplication/json
User-AgentRankNibbler-Webhooks/1.0 (+https://www.ranknibbler.com/docs/webhooks)
X-RankNibbler-EventThe event type, for example uptime.down.
X-RankNibbler-DeliveryA unique delivery id such as whd_…, for de-duplication. A resend gets a new delivery id but keeps the same event id.
X-RankNibbler-Signaturet=<unix seconds>,v1=<hex signature>. See verifying signatures.

Every JSON body uses the same envelope. id is the event id, type matches the event header, created is an ISO 8601 time in UTC, and data depends on the event.

Envelope
  1. {
  2. "id": "evt_…",
  3. "type": "uptime.down",
  4. "created": "2026-09-17T15:04:05.000Z",
  5. "data": { … }
  6. }

Example payloads

These show the data object for each event. Every link opens the matching page in the RankNibbler app.

uptime.down

data: uptime.down
  1. {
  2. "monitor": {
  3. "id": "mon_…",
  4. "name": "Shop",
  5. "url": "https://example.com",
  6. "status": "down",
  7. "statusCode": 503,
  8. "responseMs": 1200,
  9. "region": "default",
  10. "link": "https://www.ranknibbler.com/app/uptime-monitoring/mon_…"
  11. },
  12. "reason": "HTTP 503",
  13. "downForSeconds": null
  14. }

uptime.up

The same monitor object, with status set to up, no reason, and how long the monitor was down.

data: uptime.up
  1. {
  2. "monitor": {
  3. "id": "mon_…",
  4. "name": "Shop",
  5. "url": "https://example.com",
  6. "status": "up",
  7. "statusCode": 200,
  8. "responseMs": 310,
  9. "region": "default",
  10. "link": "https://www.ranknibbler.com/app/uptime-monitoring/mon_…"
  11. },
  12. "reason": null,
  13. "downForSeconds": 840
  14. }

scan.completed

status is the scan's final state, for example done or failed.

data: scan.completed
  1. {
  2. "scan": {
  3. "id": "…",
  4. "name": "…",
  5. "host": "example.com",
  6. "status": "done",
  7. "pagesCrawled": 250,
  8. "averageScore": 87,
  9. "brokenPages": 3,
  10. "link": "https://www.ranknibbler.com/app/scans/…"
  11. }
  12. }

change.detected

changes holds short labels for what changed. aiMatch is filled in when the monitor has a "watching for" prompt.

data: change.detected
  1. {
  2. "monitor": {
  3. "id": "watch_…",
  4. "name": "Pricing page",
  5. "url": "https://example.com/pricing",
  6. "link": "https://www.ranknibbler.com/app/change-monitor/watch_…"
  7. },
  8. "snapshotId": 12345,
  9. "changes": ["Title", "H1", "Price"],
  10. "changeCount": 3,
  11. "aiMatch": null
  12. }

rank.changed

position or previousPosition is null when the keyword isn't in the top 100. change is positive when the keyword moved up, and null if either position is null.

data: rank.changed
  1. {
  2. "keyword": {
  3. "id": 123,
  4. "keyword": "seo checker",
  5. "location": 2826,
  6. "language": "en",
  7. "device": "desktop"
  8. },
  9. "project": { "id": "pj…", "host": "example.com" },
  10. "position": 4,
  11. "previousPosition": 9,
  12. "change": 5,
  13. "url": "https://example.com/seo-checker",
  14. "link": "https://www.ranknibbler.com/app/projects/pj…/keyword-rankings"
  15. }

ping

data: ping
  1. {
  2. "message": "Test event from RankNibbler.",
  3. "webhook": { "id": "whk_…", "name": "…" }
  4. }

Verifying signatures

Check the signature on every request so you only act on events RankNibbler sent. The X-RankNibbler-Signature header looks like this:

Signature header
  1. X-RankNibbler-Signature: t=1789657445,v1=5f2b…c9a1

v1 is the hex HMAC-SHA256 of <t>.<raw body>, using the endpoint's signing secret as the key. To verify it:

  1. Split the header into t and v1.
  2. Compute the HMAC over the timestamp, a full stop and the raw request body bytes. Don't parse and re-serialise the JSON first, as that changes the bytes and the signature won't match.
  3. Compare your result with v1 using a constant-time comparison.
  4. Reject the request if t is more than 5 minutes away from your server's clock.
Node.js
  1. const crypto = require('crypto');
  2.  
  3. function verify(rawBody, header, secret) {
  4. const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  5. const expected = crypto.createHmac('sha256', secret).update(parts.t + '.' + rawBody).digest('hex');
  6. const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  7. return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  8. }

With Express, read the body as raw bytes on the webhook route so the signature can be checked before you parse it. timingSafeEqual throws if the two values differ in length, so treat an error as a failed check.

Node.js: Express route
  1. app.post('/webhooks/ranknibbler', express.raw({ type: 'application/json' }), (req, res) => {
  2. let ok = false;
  3. try {
  4. ok = verify(req.body, req.get('X-RankNibbler-Signature') || '', process.env.RANKNIBBLER_WEBHOOK_SECRET);
  5. } catch (e) {}
  6. if (!ok) return res.status(400).end();
  7.  
  8. const event = JSON.parse(req.body);
  9. res.status(200).end();
  10. // Handle event.type and event.data here
  11. });
Python
  1. import hashlib, hmac, time
  2.  
  3. def verify(raw_body: bytes, header: str, secret: str) -> bool:
  4. parts = dict(p.split("=", 1) for p in header.split(","))
  5. signed = parts["t"].encode() + b"." + raw_body
  6. expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
  7. fresh = abs(time.time() - int(parts["t"])) < 300
  8. return fresh and hmac.compare_digest(expected, parts["v1"])

In Flask, pass request.get_data() as the raw body. In Django, use request.body.

Retries and failures

Slack and Discord

You don't need any code to get alerts into a chat channel. Paste one of these as the endpoint URL and RankNibbler sends a readable message instead of JSON:

The format is detected from the URL, so there's nothing else to set. Every other URL gets the JSON body described above.

Zapier and Make

RankNibbler doesn't have its own Zapier or Make app, but both services can receive webhooks, which lets you pass events to thousands of other apps:

Paste that URL as the endpoint URL in RankNibbler, then click Send test so the service can see a sample event. Fields from the JSON body, such as data.monitor.name, are then available to the later steps in your workflow.

Frequently asked questions

Which events can RankNibbler send to a webhook?

uptime.down when a monitor stops responding, uptime.up when it responds again, scan.completed when a site scan completes or fails, change.detected when a watched page changes, and rank.changed when a tracked keyword moves or enters or leaves the top 100. A ping event is sent by the Send test button.

Can I send RankNibbler alerts to Slack or Discord?

Yes. Paste a Slack incoming webhook URL or a Discord webhook URL as the endpoint. RankNibbler recognises it and posts a readable chat message instead of JSON.

How do I check a webhook really came from RankNibbler?

Every request has an X-RankNibbler-Signature header containing a timestamp and an HMAC-SHA256 of the timestamp and the raw request body, made with the endpoint's signing secret. Compute the same HMAC, compare it in constant time and reject timestamps more than 5 minutes old.

What happens if my endpoint is down?

RankNibbler retries after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, for 7 attempts in total. 4xx replies other than 408, 425 and 429 are not retried. An endpoint that fails 25 attempts in a row, or replies 410 Gone, is switched off until you turn it back on.

Can I use RankNibbler webhooks with Zapier or Make?

Yes. Create a Zap with a catch hook trigger, or a Make scenario with a custom webhook trigger, and use the URL it gives you as the endpoint URL in RankNibbler. The JSON fields are then available to the rest of the workflow.

How many webhook endpoints can I add?

Up to 10 per workspace. Each endpoint has its own URL, events and signing secret, and can be switched off and on independently.

Get alerts where you work

Create a free account, set up a monitor, scan or tracked keyword, then add a webhook under Settings › Webhooks.

Create a free account   or sign in