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

  1. Create an API key in Settings → API keys (Pro and up). The key is shown once — store it like a password.
  2. 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.
  3. 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

EndpointReturns
GET /api/v1/sitesEvery site this key's account can read — the site ids every other call takes.
GET /api/v1/usageYour allowances and what is used: API requests, citation calls, generation budget, tracked prompts.
GET /api/v1/sites/{siteId}/visibilityVisibility Score with its components, rank with its basis caveat, share of voice.
GET /api/v1/sites/{siteId}/citationsLatest per-query state: mentioned or not, cited URLs, sentiment, competitor domains.
GET /api/v1/sites/{siteId}/citations/historyPer-check rows over time, filterable by engine (days 1–90, limit 1–100).
GET /api/v1/sites/{siteId}/evidenceRaw answer excerpts, every cited URL and domain, in-answer position.
GET /api/v1/sites/{siteId}/sourcesDomains AI cites for your tracked queries, ranked and classified by type.
GET /api/v1/sites/{siteId}/geoLatest GEO readiness audit: grade, score, per-signal findings.
GET /api/v1/sites/{siteId}/geo/actionsTask statuses on the audit's fix list.
GET /api/v1/sites/{siteId}/opportunitiesOpen prompt opportunities with their scores.
GET /api/v1/sites/{siteId}/prompt-templatesPrompt templates and their dimension variants.
GET /api/v1/sites/{siteId}/scanDeep site scan: run lifecycle plus merged page-level findings.
GET /api/v1/sites/{siteId}/metricsAttributed 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:

401Missing or invalid API key.
402The account's plan has no API access — Pro and up.
403The key's account now holds a seat in another workspace — API keys are owner credentials; the workspace owner issues their own.
404Site not found, or not this account's.
429Per-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.

ToolScopeDoes
add_promptprompts:writeAdds a tracked prompt; plan pool and duplicate checks apply, same as the dashboard.
archive_promptprompts:writeArchives a tracked prompt — it stops running and frees its slot.
start_citation_runruns:writeStarts a full citation run; the monthly call allowance and burst gates apply.
start_deep_scanruns:writeStarts a deep site scan through the same gate ladder as the dashboard button.
generate_briefcontent:writeGenerates a content brief for a query; the generation budget applies.
update_action_statusactions:writeSets 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.