Promvia
Visibilitas pencarian AI: lihat apakah ChatGPT, Perplexity, Gemini, Claude, dan Google AI mengutip situs Anda, sumber apa yang mereka kutip, dan berapa nilai lalu lintas yang dirujuk AI. 19 alat; masuk OAuth (paket Pro atau Agency), alat tulis opsional.
Server MCP Terhosting
npx add-mcp 'https://promvia.app/api/mcp'Terpasang ke Claude Code, Codex, Cursor, dan lainnya
Dokumentasi
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: · · · · · · · · · ·
Connect from Claude
Claude (on the web, desktop and mobile) connects to Promvia with OAuth: you paste one URL and sign in. No API key to copy.
- In Claude, open Customize → Connectors and choose Add custom connector.
- Enter the URL below and leave the advanced OAuth client fields empty.
- A Promvia sign-in window opens. Sign in, check which account and access are being granted, and choose Allow access. Read access is included; each kind of change (adding questions and writing their FAQ answers, starting checks, generating briefs, updating action items) stays off unless you tick it.
- To disconnect, use Settings → Integrations → Connected apps in Promvia, or remove the connector in Claude.
https://promvia.app/api/mcp
Needs a Pro or Agency plan, the same as API keys. Tool calls count against the same monthly API pool.
Other MCP clients that support OAuth sign-in, such as Claude Code, use the same URL. Clients that can only send a fixed header use an API key instead, described below.
Step by step, with what Claude can read and change, and how the other AI visibility tools with an MCP server compare: AI visibility tool with an MCP server for Claude
Connect from ChatGPT
ChatGPT adds Promvia as a custom MCP server: you paste the URL below and sign in with OAuth.
- In ChatGPT, open Plugins (chatgpt.com/plugins), select the + button and choose Add custom MCP server.
- Name it Promvia, enter the URL below as the server URL and choose OAuth as the authentication. Leave any client ID and secret fields empty: ChatGPT identifies itself to Promvia on its own.
- Confirm ChatGPT's warning about custom servers, then choose Create as a plugin. A Promvia sign-in window opens: sign in, check which account and access are being granted, and choose Allow access. Changes stay off unless you tick them.
- To disconnect, use Settings → Integrations → Connected apps in Promvia, or remove the plugin in ChatGPT.
https://promvia.app/api/mcp
Whether you can add custom MCP servers depends on your ChatGPT plan and, in a Business or Enterprise workspace, on what its admin allows. Older ChatGPT versions keep the same steps under Settings → Apps & Connectors, after you turn on Developer mode in its Advanced settings.
Connect from Cursor
Cursor installs Promvia from one link and signs in with OAuth.
- With Cursor installed, choose Add to Cursor. Cursor opens and shows the Promvia server it is about to add; confirm it.
- Or add it by hand: put the snippet below in.cursor/mcp.json in your project, or in ~/.cursor/mcp.json for every project.
- The first time Cursor connects, it asks you to sign in: a Promvia window opens in your browser. Sign in and choose Allow access.
{
"mcpServers": {
"promvia": {
"url": "https://promvia.app/api/mcp"
}
}
}
If your Cursor version cannot finish the OAuth sign-in, send an API key in a header instead, as shown under MCP server below.
Connect from Perplexity
Perplexity adds Promvia as a custom remote connector and signs in with OAuth. Custom connectors need a Perplexity Pro, Max or Enterprise plan.
- In Perplexity, open Account settings → Connectors, choose + Custom connector, then Remote.
- Name it Promvia, enter the URL below as the MCP server URL and choose OAuth as the authentication. Leave Client ID and Client Secret empty: Perplexity registers itself with Promvia.
- Choose Add, then select the Promvia connector card. A Promvia sign-in window opens: sign in and choose Allow access. Turn the connector on in a thread when you want Perplexity to use it.
https://promvia.app/api/mcp
In an Enterprise workspace, an admin first turns on Allow members to add custom connectors.
How sign-in works
Promvia is its own OAuth 2.1 authorization server, and every sign-in uses PKCE (S256). A client identifies itself with a Client ID Metadata Document (CIMD) or registers through Dynamic Client Registration (DCR), so there is never a client ID or secret for you to copy. Before you allow access, you see which client is asking, which account it gets and what it may change.
Whichever client you use, MCP access needs a Pro or Agency plan on Promvia, and tool calls count against the same monthly API pool.
Stuck connecting? Email with the client you are using and the message it shows.
Quickstart
- Create an API key in Settings → Integrations → 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 → Integrations → 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:
| 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, the check budget (fullChecks.left — full checks left on demand this month, on the engines in fullChecks.engines), generation budget, tracked questions. fullChecks.breakdown says where the month went, in the same full checks: weekly deep checks, daily checks, the share held for automatic checks later this month, checks you started, automatic extras (fact-checking, sentiment, CDN log intake, how-often bands) and what is left, adding up to total (null on a legacy period). engines.selected is the engines your scheduled checks run on (your choice, or your plan's default), engines.daily the ones on the frequent check this month, and engines.sites how many days apart that check runs for each of your sites (checkedEveryDays). |
GET /api/v1/sites/{siteId}/visibility | Visibility Score with its components, rank with its basis caveat, share of voice, and the competitor ranking (rivals): each business's share with its sample and 95% interval, and a tie wherever two intervals overlap. |
GET /api/v1/sites/{siteId}/citations | Latest per-question state: mentioned or not, cited URLs, sentiment, the other domains in the answer, and demandBand — the question's search demand as a band (rare, occasional, frequent), null when not measured. |
GET /api/v1/sites/{siteId}/citations/history | Per-check rows over time, newest first, filterable by engine (days 1–90, limit 1–100 per page); pass nextCursor back as?cursor= to read the rest of the window. scope says which check wrote the row (full, daily or sweep). observedQueries is the web searches that engine ran for the answer: null where the engine does not expose them, [] when it ran none. |
GET /api/v1/sites/{siteId}/evidence | Raw answer excerpts, every cited URL and domain, in-answer position, and observedSearches: the distinct web searches the engines ran for each question, with which engines ran each. |
GET /api/v1/sites/{siteId}/sources | Domains AI cites for your tracked questions, ranked and classified by type. |
GET /api/v1/sites/{siteId}/geo | Latest GEO readiness audit: grade, score, per-signal findings. It re-runs automatically (weekly on paid plans, monthly on Free), so the latest audit may come from the automatic run. signals.consistency holds the consistency check: six rule checks of the scanned pages against each other, on every plan, not part of the score. |
GET /api/v1/sites/{siteId}/geo/actions | The site's Fix list, open items first, in its ranked order: actions, each with taskKey, title, kind (technical, content or offsite), priority, effort and status; at most 50 items, with totalActions, openActions and omitted. otherStatuses lists your in-progress, done and dismissed records on items the current plan no longer holds. |
GET /api/v1/sites/{siteId}/opportunities | Open question opportunities with their scores. |
GET /api/v1/sites/{siteId}/prompt-templates | Prompt templates and their dimension variants. |
GET /api/v1/sites/{siteId}/prompts | Your tracked questions with their FAQ answers, verbatim, and answerHash — the SHA-256 of the answer's UTF-8 bytes — so you can check your page holds the same text; 100 per page with nextCursor,?includeArchived=true for questions you stopped tracking. replacesQueryId and replacedByQueryId link a reworded question to the one it replaced. Answers are written over MCP (set_prompt_answer, sync_prompt_answers), never here. |
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": 58,
"grade": "C",
"components": [
{ "key": "coverage", "value": 0.44, "weight": 0.3 },
{ "key": "sov", "value": 0.4444444444444444, "weight": 0.25 },
{ "key": "engines", "value": 0.8, "weight": 0.2 },
{ "key": "geo", "value": 0.81, "weight": 0.15 }
],
"modelVersion": 6,
"thinBasis": null
},
"rank": {
"rank": 4,
"of": 14,
"basis": "businesses cited across your discovery questions — you plus every other brand that took an answer slot. Forums, reference sites, press, directories and social are excluded, and questions naming your own brand are not counted"
},
"scoreBasis": "A weighted index (0–100) over up to five components: coverage 0.3, share of voice 0.25, engines 0.2, GEO readiness 0.15, sentiment 0.1. …",
"shareOfVoicePct": 10,
"shareOfVoiceSample": {
"successes": 4,
"n": 40,
"interval95Pct": { "low": 4, "high": 23 },
"tooThin": false,
"questions": 25,
"earlyReading": true,
"note": "4 of 40 answer slots = 10%, over 25 questions. 95% Wilson score interval 4-23%. Early reading: fewer than 32 measured questions, so the figure firms up as more questions are measured. One answer fills many slots, so the sample size is the questions, not the slots."
},
"rivals": [
{
"domain": "rival-a.example", "you": false, "rank": 1, "shareOfVoicePct": 23, "versusYou": "tied",
"shareOfVoiceSample": { "successes": 9, "n": 40, "interval95Pct": { "low": 12, "high": 38 }, "tooThin": false, "questions": 25, "earlyReading": true, "note": "9 of 40 answer slots = 23%, over 25 questions. 95% Wilson score interval 12-38%. Early reading: fewer than 32 measured questions, so the figure firms up as more questions are measured. One answer fills many slots, so the sample size is the questions, not the slots." },
"interval95PctFine": { "low": 12.32, "high": 37.5 }
},
{
"domain": "rival-b.example", "you": false, "rank": 2, "shareOfVoicePct": 18, "versusYou": "tied",
"shareOfVoiceSample": { "successes": 7, "n": 40, "interval95Pct": { "low": 9, "high": 32 }, "tooThin": false, "questions": 25, "earlyReading": true, "note": "7 of 40 answer slots = 18%, over 25 questions. 95% Wilson score interval 9-32%. Early reading: fewer than 32 measured questions, so the figure firms up as more questions are measured. One answer fills many slots, so the sample size is the questions, not the slots." },
"interval95PctFine": { "low": 8.75, "high": 31.95 }
},
{
"domain": "rival-c.example", "you": false, "rank": 3, "shareOfVoicePct": 13, "versusYou": "tied",
"shareOfVoiceSample": { "successes": 5, "n": 40, "interval95Pct": { "low": 5, "high": 26 }, "tooThin": false, "questions": 25, "earlyReading": true, "note": "5 of 40 answer slots = 13%, over 25 questions. 95% Wilson score interval 5-26%. Early reading: fewer than 32 measured questions, so the figure firms up as more questions are measured. One answer fills many slots, so the sample size is the questions, not the slots." },
"interval95PctFine": { "low": 5.46, "high": 26.11 }
},
{
"domain": "acme.com", "name": "Acme", "you": true, "rank": 4, "shareOfVoicePct": 10, "versusYou": null,
"shareOfVoiceSample": { "successes": 4, "n": 40, "interval95Pct": { "low": 4, "high": 23 }, "tooThin": false, "questions": 25, "earlyReading": true, "note": "4 of 40 answer slots = 10%, over 25 questions. 95% Wilson score interval 4-23%. Early reading: fewer than 32 measured questions, so the figure firms up as more questions are measured. One answer fills many slots, so the sample size is the questions, not the slots." },
"interval95PctFine": { "low": 3.96, "high": 23.05 }
},
{
"domain": "rival-d.example", "you": false, "rank": 5, "shareOfVoicePct": 8, "versusYou": "tied",
"shareOfVoiceSample": { "successes": 3, "n": 40, "interval95Pct": { "low": 3, "high": 20 }, "tooThin": false, "questions": 25, "earlyReading": true, "note": "3 of 40 answer slots = 8%, over 25 questions. 95% Wilson score interval 3-20%. Early reading: fewer than 32 measured questions, so the figure firms up as more questions are measured. One answer fills many slots, so the sample size is the questions, not the slots." },
"interval95PctFine": { "low": 2.58, "high": 19.86 }
}
],
"rivalsBasis": "The top five businesses by share of voice, ordered by share, with your own row (you: true) always included: appended with its real rank when it is not in the five. Each share is that business's answer slots out of every answer slot a business occupied across your discovery questions, the same denominator as shareOfVoicePct and rank. Forums, reference sites, press, directories and social are excluded, and questions naming your own brand are not counted. Equal counts share a rank. When two businesses' 95% intervals overlap, the data shows no measured lead between them: call it a tie (versusYou: 'tied'), never 'ahead'. versusYou is read on interval95PctFine, the same interval to two decimals: at shares of a few percent, whole-percent bounds can touch after rounding while the intervals are clear of each other. A competitor known only by a name you listed has domain null and that name in name. Your own rank is null while you hold no answer slot, as the top-level rank is. Every share's sample carries questions, the measured discovery questions behind the slots: one answer fills many slots, so the sample size is questions, not n. tooThin is true under 10 questions; earlyReading is true from 10 up to 32, where the share is shown but firms up as more questions are measured. Empty when no completed check has ranked anyone."
}
Every rate ships the sample it was measured on. A field ending Pct that is a proportion has a …Sample sibling holding the counts behind it, a 95% Wilson score interval in the same unit, and tooThin — true when n is under 32, the sample size below which a 95% interval can be wider than a third of the scale. It reads n alone, so every rate measured on the same n carries the same tooThin. Share of voice is the one clustered rate: its n counts answer slots and one answer names many businesses, so its sample also carries questions (the measured discovery questions), tooThin reads that instead — true when questions is under 10 —, earlyReading is true from 10 up to 32 (the share is shown but firms up as more questions are measured), and its note says “over N questions”. At that size the rate cannot be read off the sample. 2 of 10 and 200 of 1,000 are both “20%” and are not the same finding. Numbers that are not proportions — the Visibility Score, GEO readiness, an in-answer rank, the visitor-quality lift, money — carry no interval on purpose and say so in a Basis or note field; inventing one would be precision we made up. These fields are additions: nothing named in v1 has been renamed or removed.
Changed on 15 October 2026: tooThin (and the CSV export's _sample_too_small columns) now reads n alone and is true when n is under 32. Before that date it read the width of the observed interval, so more rates report tooThin: true than before.
Errors
Every error is JSON with an error field. The statuses you will meet:
400 | A cursor the endpoint did not return for this site (/prompts, /citations/history). Pass back the previous page's nextCursor, or omit cursor for the first page. |
|---|---|
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 — or no such endpoint (the body points at openapi.json). |
405 | Any method but GET. The REST API is read-only; writes live on the MCP server. |
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. |
503 | The key's account membership could not be verified just now, so the request was refused rather than assumed and nothing was read. Retry shortly. |
MCP server
Point any MCP-capable assistant at the endpoint below. With an API key the connection is read-only (15 read tools: sites, allowances, the connection's own capabilities, visibility score, citations, history, evidence, sources, GEO audit and actions, opportunities, templates, tracked questions with their FAQ answers, deep scan, traffic metrics); the 8 write tools — add or replace questions, set or sync FAQ answers, 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 15 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 — except get_capabilities, which describes the connection itself (its scopes, its plan, which tools it may call and why not) and has no REST twin. An API key covers all of them.
list_sites · get_usage · get_capabilities · get_visibility_score · get_citations · get_citation_history · get_evidence · get_sources_report · get_geo_audit · get_geo_actions · get_opportunities · get_prompt_templates · list_prompts · get_deep_scan · get_metrics
The 8 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 question and returns its id (the existing one's id when it is already tracked); plan pool and duplicate checks apply, same as the dashboard. With replacesQueryId it replaces a reworded question: the old one is archived in the same step, keeps its results and is linked to the new one. |
archive_prompt | prompts:write | Archives a tracked question — it stops running and frees its slot. |
set_prompt_answer | prompts:write | Sets a tracked question's FAQ answer, stored as sent (line endings normalised, ends trimmed); over 600 characters is refused, never cut. Returns its answerHash; sending the stored text again changes nothing and records nothing. With expectedHash, a stale copy cannot overwrite a newer answer: it is refused as a conflict. |
sync_prompt_answers | prompts:write | Sets up to 100 FAQ answers in one transaction, for a release that keeps its answers in a repository: a dry run says exactly what would change and writes nothing, one invalid answer refuses the whole batch, and an idempotency key per batch makes a retried batch return the first result instead of writing twice (once every item applied). |
start_deep_scan | runs:write | Starts a deep site scan through the same gate ladder as the dashboard button. |
start_citation_run | runs:write | Starts a full citation run; it uses checks from this month's allowance, and the burst gates apply. |
generate_brief | content:write | Generates a content brief for a question; the generation budget applies. |
update_action_status | actions:write | Sets the status of a GEO action-plan task — one of the keys get_geo_actions lists; any other key is refused. |
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 (https:// on the standard port 443 only — a URL naming any other port is refused when you save it and at every delivery): run.completed after every full citation run (with status, won/lost questions, and the visibility score current at delivery time) and citations.changed when questions 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": 58, "grade": "C", "modelVersion": 6, "thinBasis": null, "basis": "current at delivery time" }
}
queriesChecked counts the questions at least one AI answer engine answered in the run; queriesFailed counts the rest. A question only the Bing search proxy returned results for counts as failed — a results page is not an AI answer (since 2026-09-29; before that it counted as checked). The won and lost lists follow the same rule.
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"));
}
run.completed covers the weekly and on-demand checks; the daily check sends citations.changed only when something changed, and the trend check sends nothing — 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.