Skip to main content

Documentation Index

Fetch the complete documentation index at: https://dev.ranked.ai/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Set up a webhook that sends Slack messages when your keyword positions update. You’ll receive a daily summary after each scan completes.

Prerequisites

Step 1: Create a webhook endpoint

Build a simple server that receives Ranked AI webhooks and forwards to Slack:
const express = require('express');
const crypto = require('crypto');
const app = express();

const RANKED_WEBHOOK_SECRET = process.env.RANKED_WEBHOOK_SECRET;
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
const RANKED_API_KEY = process.env.RANKED_API_KEY;

function verifySignature(payload, signature) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', RANKED_WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

app.post('/webhooks/ranked', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  if (!verifySignature(req.body.toString(), signature)) {
    return res.status(401).send();
  }

  const event = JSON.parse(req.body);

  if (event.event === 'keywords.updated') {
    await handleKeywordsUpdated(event);
  }

  res.status(200).send('OK');
});

async function handleKeywordsUpdated(event) {
  // Fetch latest keyword data
  const response = await fetch(
    `https://app.ranked.ai/api/v1/projects/${event.project_id}/rankings/keywords?limit=1000`,
    { headers: { 'Authorization': `Bearer ${RANKED_API_KEY}` } }
  );
  const { data: keywords } = await response.json();

  // Find significant changes
  const improved = keywords.filter(kw => kw.net_change > 0);
  const declined = keywords.filter(kw => kw.net_change < 0);

  if (improved.length === 0 && declined.length === 0) return;

  // Build Slack message
  const blocks = [
    {
      type: 'header',
      text: { type: 'plain_text', text: 'Keyword Rankings Update' }
    }
  ];

  if (improved.length > 0) {
    const top5 = improved.sort((a, b) => b.net_change - a.net_change).slice(0, 5);
    blocks.push({
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Improved (${improved.length})*\n` +
          top5.map(kw => `${kw.keyword}: +${kw.net_change} positions`).join('\n')
      }
    });
  }

  if (declined.length > 0) {
    const top5 = declined.sort((a, b) => a.net_change - b.net_change).slice(0, 5);
    blocks.push({
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Declined (${declined.length})*\n` +
          top5.map(kw => `${kw.keyword}: ${kw.net_change} positions`).join('\n')
      }
    });
  }

  // Send to Slack
  await fetch(SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ blocks })
  });
}

app.listen(3001, () => console.log('Webhook server running on port 3001'));

Step 2: Register the webhook

curl -X POST https://app.ranked.ai/api/v1/webhooks \
  -H "Authorization: Bearer rk_live_your_write_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Slack Alerts",
    "url": "https://your-server.com/webhooks/ranked",
    "project_id": "your-project-id",
    "events": ["keywords.updated"]
  }'
Save the secret from the response as your RANKED_WEBHOOK_SECRET environment variable.

Result

You’ll receive a Slack message once per day after the keyword scan completes, showing which keywords improved or declined.