- Developers
- Guides
- Best Practices
Building an Agency Dashboard
How to use the API and webhooks to build a custom client-facing dashboard
Overview
Agencies can build custom dashboards on top of Ranked AI's data using the REST API for data retrieval and webhooks for real-time updates. This guide walks through the recommended architecture.
Architecture
Your dashboard pulls data from the API and caches it locally. Webhooks push updates so you don't need to poll.
API pulls data into your database
Your server calls the Ranked AI API to fetch keywords, audits, backlinks, and other data, then stores it in your database.
Webhooks push real-time updates
When keyword positions update or content changes, Ranked AI sends a webhook to your server with the event details.
Your dashboard reads from the cache
Your frontend reads from your local database instead of calling the API on every page load. Fast and always available.
Step 1: Discover projects
List all SEO projects for the authenticated user:
const response = await fetch('https://app.ranked.ai/api/v1/projects', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
const { data: projects } = await response.json();
// Store project IDs for subsequent calls
projects.forEach(project => {
db.upsert('projects', {
ranked_id: project.id,
name: project.name,
website: project.websiteUrl,
status: project.status,
});
});Step 2: Initial data sync
Pull the full dataset for each project:
async function syncProject(projectId) {
const headers = { 'Authorization': `Bearer ${apiKey}` };
const base = `https://app.ranked.ai/api/v1/projects/${projectId}`;
// Fetch all data in parallel
const [keywords, audits, backlinks, prompts, content] = await Promise.all([
fetch(`${base}/rankings/keywords?limit=1000`, { headers }).then(r => r.json()),
fetch(`${base}/audits/latest`, { headers }).then(r => r.json()),
fetch(`${base}/backlinks/summary`, { headers }).then(r => r.json()),
fetch(`${base}/prompts?limit=200`, { headers }).then(r => r.json()),
fetch(`${base}/content?limit=100`, { headers }).then(r => r.json()),
]);
// Store in your database
await db.upsertKeywords(projectId, keywords.data);
await db.upsertAudit(projectId, audits.data);
await db.upsertBacklinks(projectId, backlinks.data);
await db.upsertPrompts(projectId, prompts.data);
await db.upsertContent(projectId, content.data);
}Step 3: Set up webhooks for real-time updates
Instead of polling, subscribe to events:
// Create webhook for each project
for (const project of projects) {
await fetch('https://app.ranked.ai/api/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${writeApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: `Dashboard - ${project.name}`,
url: 'https://your-dashboard.com/webhooks/ranked',
project_id: project.id,
events: [
'keywords.updated',
'content.status_changed',
'content.created',
'audit.completed',
'prompts.updated',
],
}),
});
}Step 4: Handle webhook events
app.post('/webhooks/ranked', async (req, res) => {
// Verify signature first (see Webhook Security docs)
if (!verifySignature(req)) return res.status(401).send();
const { event, project_id, data } = req.body;
switch (event) {
case 'keywords.updated':
// Re-sync keyword positions from the API
const keywords = await fetch(
`https://app.ranked.ai/api/v1/projects/${project_id}/rankings/keywords?limit=1000`,
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
).then(r => r.json());
await db.upsertKeywords(project_id, keywords.data);
break;
case 'audit.completed':
// Fetch the latest audit results
const audit = await fetch(
`https://app.ranked.ai/api/v1/projects/${project_id}/audits/latest`,
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
).then(r => r.json());
await db.upsertAudit(project_id, audit.data);
break;
case 'content.status_changed':
// Update content status in your DB
await db.updateContentStatus(data.content_id, data.new_status);
break;
case 'prompts.updated':
// Refresh AI visibility data for this prompt
const prompt = await fetch(
`https://app.ranked.ai/api/v1/projects/${project_id}/prompts/${data.prompt_id}`,
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
).then(r => r.json());
await db.upsertPrompt(project_id, prompt.data);
break;
}
res.status(200).send('OK');
});Recommended sync strategy
| Data | Strategy | Frequency |
|---|---|---|
| Keyword positions | Webhook keywords.updated triggers API pull | Daily (after scan completes) |
| AI visibility | Webhook prompts.updated triggers API pull | Monthly (after analysis) |
| Audit results | Webhook audit.completed triggers API pull | Monthly (after scan) |
| Content status | Webhook content.status_changed for real-time | Real-time |
| Backlinks | Scheduled API pull (no webhook yet) | Weekly cron job |
| Reports | On-demand API call | As needed |
Tips
- Use Read Only API keys for data fetching and a separate Read + Write key for webhook management
- Cache API responses in your database rather than calling the API on every page load
- The
keywords.updatedwebhook fires once per daily scan, not per keyword -- use it as a signal to re-sync - Implement retry logic for your API calls with exponential backoff
This page is also available as markdown for AI agents, and the whole reference as an OpenAPI spec. Questions? Developer support.