PendiumDocs

Trigger a Scan

Start an AI visibility scan for any brand across ChatGPT, Claude, Gemini, AI Overviews, and, on Pro, Perplexity.

POST /api/visibility/scan

Authentication: Required. An API key whose account is in read-only mode gets a 403 with code: "READ_ONLY" — scans are a write + spend, so they're refused. A free-plan key gets a 403 with code: "PLAN_REQUIRED" before anything else is checked (plan required). A paid key rescanning a group-shared brand whose owner is on the free plan gets a 403 with code: "OWNER_PLAN_UPGRADE_REQUIRED": the scan bills the brand's owner, so the owner needs to upgrade.

Start a visibility scan that checks how AI platforms perceive and recommend a brand. You can scan a new URL (which creates a brand agent if one doesn't exist) or re-scan an existing agent by ID.

The scan runs in the background — this endpoint returns immediately. Use Poll Scan Status to check when results are ready.

Request body

{
  "url": "https://example.com",
  "mode": "batch",
  "maxQueries": 30
}
FieldTypeRequiredDefaultDescription
urlstringOne of url or syntheticId—Website URL to scan. If no agent exists for this domain yet, one is created automatically.
syntheticIdintegerOne of url or syntheticId—ID of an existing brand agent to re-scan. Use Account to find your agent IDs.
modestringNo"batch"Both "batch" and "full" scan every platform on your plan with fast, cost-effective models. (Premium-model runs are an advanced opt-in, not selected by "full" on its own.)
maxQueriesintegerNoyour plan's capHow many queries to run across all platforms. Capped at the brand owner's plan: 100 on Starter, 160 on Pro, 200 on Managed or with an enterprise plan. Leave it out to run the plan's full cap. 20–40 is the sweet spot for coverage vs. cost.
platformConfigobjectNo—Per-platform overrides, e.g. { "Gemini": { "enabled": false } }. Your plan's platforms are on by default: ChatGPT, Claude, Gemini, and AI Overviews on Starter, plus Perplexity on Pro. Grok, DeepSeek, Qwen, MiniMax, Nemotron, and Llama require Managed or the enterprise plan. See Platforms by plan.
publishToBrandIndexbooleanNofalseSet to true to create or update a public Brand Index page for this brand at /brands/{slug}. Leaving it off doesn't create a page, but a brand that already has one keeps showing its newest scan there.
webhookUrlstringNo—HTTPS URL we POST the results to when the scan finishes, so you don't have to poll. Max 2048 chars; must be HTTPS and public (private/internal addresses are rejected). The response echoes webhookRegistered: true when accepted. See Webhook callback.

Platforms by plan

PlanPlatforms each scan runs
Free (web app only)Gemini, AI Overviews
StarterChatGPT, Claude, Gemini, AI Overviews
ProChatGPT, Claude, Gemini, AI Overviews, Perplexity
ManagedThe Pro five, plus Grok, DeepSeek, Qwen, MiniMax, Nemotron, and Llama when you enable them in platformConfig
EnterpriseYour plan's platforms (at least the Starter four), plus Grok, DeepSeek, Qwen, MiniMax, Nemotron, and Llama when you enable them in platformConfig (e.g. { "Grok": { "enabled": true } })

The platforms and the query cap follow the plan of the account that owns the brand, whoever calls the API. If you request a platform your plan doesn't include, it's dropped and the scan still runs on your plan's platforms. scanScope.droppedEnterprisePlatforms lists what was skipped (the field name predates Perplexity moving to Pro), scanScope.enterpriseNotice says which plan each one needs, and scanScope.enabledPlatforms lists what the scan runs on.

How it works

Scanning an existing agent (syntheticId):

  1. Verifies you own the agent
  2. If a scan is already running, returns its progress instead of starting a new one
  3. Otherwise, kicks off the scan and returns immediately

Scanning a new URL (url):

  1. Extracts the domain from your URL
  2. Checks if you already have an agent for that domain — if so, re-scans it
  3. If not, creates a new brand agent (scrapes the site, builds a knowledge base) and starts the scan
  4. If your account has its own team, adds the new agent to it, so your teammates can see it and poll its scans

A public Brand Index page is only created when you send publishToBrandIndex: true. The reportUrl link works for anyone who has it, whatever this setting.

On a brand whose owner is on the free plan (a teammate's brand your paid key can read), polling and the report return a partial report (score plus the top 3 findings) once the scan finishes.

Example

curl -X POST https://pendium.ai/api/visibility/scan \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_xxxxxxxxxxxx" \
  -d '{"url": "https://example.com", "mode": "batch", "maxQueries": 30}'

Response

{
  "success": true,
  "syntheticId": 1234,
  "agentName": "Example",
  "scanTriggered": true,
  "isNewAgent": false,
  "mode": "batch",
  "maxQueries": 30,
  "message": "Scan triggered. Poll GET /api/visibility/scan?syntheticId=1234 for results."
}

If a scan is already running:

{
  "success": true,
  "scanAlreadyRunning": true,
  "syntheticId": 1234,
  "progress": {
    "completedQueries": 12,
    "totalQueries": 30
  }
}

If scans are paused for maintenance: during a platform upgrade we occasionally pause all new scans. New triggers return 503 with code: "SCANS_PAUSED" and a short message — retry once it lifts (existing scans and polling/reports are unaffected):

{
  "error": "Visibility scans are temporarily paused for maintenance. Please try again shortly.",
  "code": "SCANS_PAUSED"
}

If the account is at its brand limit: a url that doesn't match one of your brands creates a new brand. If your plan caps how many brands an account can own and you're at that cap, the request returns 403 with code: "BRAND_LIMIT_REACHED" and nothing is created. The body also carries error (a message naming your plan and what to do next), brandLimit, plan and upgradeUrl, plus the limit fields below. Rescanning a brand you already have never counts toward the cap.

If the brand's owner has used this month's on-demand scans: on a plan with a monthly allowance of on-demand scans, a rescan past it returns 429 with code: "SCAN_LIMIT_REACHED", used, limit and resetsAt (when the count resets, the first of next month in UTC), plus the limit fields below. No scan starts and nothing is billed. The brand's latest results stay readable with GET.

Limit fields. Every limit refusal carries the same fields, next to the ones above:

FieldDescription
errorTypebrand_limit_reached or scan_limit_reached.
limitKindbrands or on_demand_scans.
messageWhat hit the limit, whose allowance it is, when it resets, and the next step. Safe to show as-is.
resetsAtWhen the count resets (ISO timestamp), or null for the brand limit.
allowanceOwneryou when the key's owner owns the allowance, owner when it belongs to the brand's (or team account's) owner.
limitSourceWhat set the limit: plan, enterprise (an Agency account), or override (a limit set for the account).
ctaLabel, ctaUrlThe next step as a link (Upgrade to Pro, or talk to our team), or null when only the owner can act on it. The link opens in the app, so the person following it signs in first.
{
  "error": "You've used 10 of 10 on-demand scans this month on your Pro plan. They reset on November 1. This brand still gets its weekly scan. Talk to our team about Agency for more scans.",
  "code": "SCAN_LIMIT_REACHED",
  "used": 10,
  "limit": 10,
  "resetsAt": "2026-11-01T00:00:00.000Z",
  "errorType": "scan_limit_reached",
  "limitKind": "on_demand_scans",
  "allowanceOwner": "you",
  "limitSource": "plan",
  "ctaLabel": "Talk to our team about Agency",
  "ctaUrl": "https://pendium.ai/talk-to-us"
}

Webhook callback

If you passed webhookUrl, we POST the finished results there when the scan completes, so you don't have to poll. The payload describes the scan that just ran:

{
  "success": true,
  "syntheticId": 1234,
  "scanId": "8f3c…",
  "overallScore": 62,
  "visibilityLevel": "moderate",
  "summary": "…",
  "platformScores": [],
  "topCompetitors": [],
  "recommendations": [],
  "reportUrl": "https://pendium.ai/r/abc12345"
}

success reflects the real outcome of that run. If the scan couldn't reach any AI engine (a transient provider/network failure), success is false and overallScore is 0 — don't ingest the zero as a real result; trigger another scan. The same condition surfaces as scanFailed: true when you poll.

Per-platform honesty follows the same rule: a platformScores entry for a platform the scan didn't query (a scoped or single-engine run) comes back with queried: false and a null score, not a 0.

On this page