Developers
Your data, over REST and MCP
Everything the dashboard shows is readable by machine: a REST API for scripts and BI, and an MCP server so an AI assistant can query (and, with scoped OAuth, act on) your visibility data. Both surfaces return byte-identical payloads — including the honesty caveats.
On this page: Quickstart · Authentication · Limits, stated out loud · REST endpoints · Errors · MCP server · Webhooks
Quickstart
- Create an API key in Settings → API keys (Pro and up). The key is shown once — store it like a password.
- Call any endpoint with the key as a bearer token; the first call to make is /v1/sites, which returns the site ids everything else takes.
- Or skip REST entirely: point an MCP client at the server below — the same key covers every read tool.
Authentication
Create an API key in Settings → API keys (Pro and Agency). Send it as a Bearer header — never in a URL. Keys are account-owner credentials: the owner can see, rotate and revoke every key.
curl https://promvia.app/api/v1/sites \
-H "Authorization: Bearer pv_live_..."Limits, stated out loud
One monthly pool covers REST requests and MCP tool calls together: 10,000 requests/month on Pro, 50,000 on Agency, plus a 120 requests/minute burst limit. The pool resets on the 1st (UTC). Checks, scans and generation keep their own product budgets — an API call never spends those; only the actions that actually run them do.
Successful responses and monthly-pool 429s carry X-RateLimit-Limit / -Remaining / -Reset headers, and GET /api/v1/usage returns every allowance on the account in one read. Over the pool you get a 429 that names the limit and the reset date; the separate per-minute burst 429 just asks you to retry shortly.
REST endpoints
Read-only by design (writes live on MCP, behind OAuth scopes). Machine-readable description: openapi.json
| Endpoint | Returns |
|---|---|
GET /api/v1/sites | Every site this key's account can read — the site ids every other call takes. |
GET /api/v1/usage | Your allowances and what is used: API requests, citation calls, generation budget, tracked prompts. |
GET /api/v1/sites/{siteId}/visibility | Visibility Score with its components, rank with its basis caveat, share of voice. |
GET /api/v1/sites/{siteId}/citations | Latest per-query state: mentioned or not, cited URLs, sentiment, competitor domains. |
GET /api/v1/sites/{siteId}/citations/history | Per-check rows over time, filterable by engine (days 1–90, limit 1–100). |
GET /api/v1/sites/{siteId}/evidence | Raw answer excerpts, every cited URL and domain, in-answer position. |
GET /api/v1/sites/{siteId}/sources | Domains AI cites for your tracked queries, ranked and classified by type. |
GET /api/v1/sites/{siteId}/geo | Latest GEO readiness audit: grade, score, per-signal findings. |
GET /api/v1/sites/{siteId}/geo/actions | Task statuses on the audit's fix list. |
GET /api/v1/sites/{siteId}/opportunities | Open prompt opportunities with their scores. |
GET /api/v1/sites/{siteId}/prompt-templates | Prompt templates and their dimension variants. |
GET /api/v1/sites/{siteId}/scan | Deep site scan: run lifecycle plus merged page-level findings. |
GET /api/v1/sites/{siteId}/metrics | Attributed AI visits and verified revenue, by currency and source. |
GET /v1/sites/{siteId}/visibility — payloads carry their caveats with them (the rank's basis field is part of the data, not a footnote):
{
"site": { "id": "site_1a2b3c", "name": "Acme", "domain": "acme.com" },
"score": {
"score": 62,
"grade": "C",
"components": [
{ "key": "coverage", "value": 0.44, "weight": 0.3 },
{ "key": "sov", "value": 0.18, "weight": 0.25 },
{ "key": "engines", "value": 0.67, "weight": 0.2 },
{ "key": "geo", "value": 0.81, "weight": 0.15 }
]
},
"rank": {
"rank": 4,
"of": 23,
"basis": "domains cited across your tracked queries — forums, reference sites and press are counted alongside rivals"
},
"shareOfVoicePct": 18
}Errors
Every error is JSON with an error field. The statuses you will meet:
401 | Missing or invalid API key. |
402 | The account's plan has no API access — Pro and up. |
403 | The key's account now holds a seat in another workspace — API keys are owner credentials; the workspace owner issues their own. |
404 | Site not found, or not this account's. |
429 | Per-minute burst limit or the monthly pool exhausted. A monthly-pool 429 names the limit and reset date in the body and X-RateLimit headers; the burst 429 just asks you to retry shortly. |
MCP server
Point any MCP-capable assistant at the endpoint below. With an API key the connection is read-only (12 read tools: sites, visibility score, citations, history, evidence, sources, GEO audit and actions, opportunities, templates, deep scan, traffic metrics); the 6 write tools — add prompts, start runs and scans, generate briefs — require an OAuth grant with the matching scope, so a leaked key can read but never act.
{
"mcpServers": {
"promvia": {
"url": "https://promvia.app/api/mcp",
"headers": { "Authorization": "Bearer pv_live_..." }
}
}
}MCP tools
The 12 read tools return the same payloads as the REST endpoints above — both surfaces are built on one shared read model, so they cannot drift apart. An API key covers all of them.
list_sites · get_visibility_score · get_citations · get_citation_history · get_evidence · get_sources_report · get_geo_audit · get_geo_actions · get_opportunities · get_prompt_templates · get_deep_scan · get_metrics
The 6 write tools pass through the app's own gate ladders (plan pools, budgets, verification) and ask for an OAuth grant with the named scope — an API key alone stays read-only on purpose.
| Tool | Scope | Does |
|---|---|---|
add_prompt | prompts:write | Adds a tracked prompt; plan pool and duplicate checks apply, same as the dashboard. |
archive_prompt | prompts:write | Archives a tracked prompt — it stops running and frees its slot. |
start_citation_run | runs:write | Starts a full citation run; the monthly call allowance and burst gates apply. |
start_deep_scan | runs:write | Starts a deep site scan through the same gate ladder as the dashboard button. |
generate_brief | content:write | Generates a content brief for a query; the generation budget applies. |
update_action_status | actions:write | Sets the status of a GEO action-plan task. |
Write scopes: prompts:write, runs:write, content:write, actions:write. Every write runs the same gate ladder as the dashboard button it mirrors — quotas, verification and plan limits included — so nothing is purchasable by API that is not purchasable by click.
Webhooks
Instead of polling, register an HTTPS endpoint in Settings and Promvia POSTs events to it: run.completed after every full citation run (with status, won/lost queries, and the visibility score current at delivery time) and citations.changed when queries were won or lost. Deliveries retry with exponential backoff for up to 8 attempts; an endpoint that only fails is disabled automatically and can be re-enabled in Settings.
What a delivery looks like on the wire:
{
"event": "run.completed",
"deliveryId": "7f3c9e2a-…",
"sentAt": "2026-09-01T13:05:12.000Z",
"site": { "id": "site_1a2b3c", "name": "Acme", "domain": "acme.com" },
"run": {
"id": "b41d…", "status": "completed",
"queriesChecked": 14, "queriesFailed": 0,
"startedAt": "2026-09-01T13:00:02.000Z"
},
"citations": { "won": ["best crm for startups"], "lost": [] },
"visibilityScore": { "score": 62, "grade": "C", "basis": "current at delivery time" }
}Every delivery carries an X-Promvia-Signature header: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your endpoint secret>. Verify the MAC with a constant-time compare and reject timestamps older than 5 minutes — that is what makes a replayed capture detectable. A webhook.test event with the same signature can be fired from Settings.
// Node.js — verify X-Promvia-Signature
const { createHmac, timingSafeEqual } = require("node:crypto");
function verify(secret, rawBody, header) {
const m = header.match(/^t=(\d+),v1=([0-9a-f]{64})$/);
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const mac = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest();
return timingSafeEqual(mac, Buffer.from(m[2], "hex"));
}Webhook events cover FULL citation runs only; the daily trend lane is deliberately excluded — a webhook that fires on every calendar day, changed or not, is noise. The visibilityScore field says "current at delivery time": a retried delivery reports the score as of the retry, never a reconstruction.
What the numbers mean
Payloads carry their own caveats (what a rank is measured against, which pages a scan counted but never fetched) because an assistant reads fields raw and repeats them to your customer. If a number cannot be measured cleanly, the field is null and says so — same rule as the product.