AI Mentions API

Track how ChatGPT, Gemini, Google AI Mode and Perplexity mention your brand: named-in, cited and share of voice per market.

Track how AI assistants mention a business. Each prompt belongs to one of its markets, so it is asked in that market's language and from inside it. You create prompts for the providers to answer, then read back each prompt run, the aggregated prompt stats and the periodic visibility reports.

Providers: chatgpt, claude, perplexity, gemini, google_ai_overview, google_ai_mode, meta_ai, copilot, grok, tiktok.

WARNING

Each provider answer costs credits, and scheduled runs reserve theirs before they start, so a prompt can't overdraw your balance.

The prompt object

json
{
  "id": "prm_4k2Lx",
  "object": "prompt",
  "business_id": "biz_7hQm2",
  "market_id": "mkt_3xUs",
  "text": "best invisalign dentist near me",
  "intent": "commercial",
  "active": true,
  "providers": ["chatgpt", "google_ai_mode", "perplexity"],
  "monitor_interval": "dynamic",
  "tags": ["invisalign"],
  "created_at": 1761350400
}

List prompts

GET /v1/prompts

Parameter Type Description
business (required) string biz_ ID
market string Only this mkt_ market
tag string Only prompts with this tag
active true | false Only running or only paused prompts
limit, starting_after Pagination

Create a prompt

POST /v1/prompts

Parameter Type Description
business (required) string biz_ ID
market (required) string mkt_ ID of one of the business's markets. Write text in one of its languages.
text (required) string Question sent to each provider, worded the way locals ask. Max 1000 chars.
intent (required) "informational" | "transactional" | "commercial" What the asker wants
providers string[] Defaults to every provider. Each provider answers separately, and each answer costs credits.
monitor_interval "dynamic" | "weekly" | "monthly" Default "dynamic". See monitor intervals.
tags string[] Up to 10. Lowercase letters, digits, hyphens, max 64 chars each.
active boolean Default true. false saves it paused, so it doesn't run or spend credits.

Returns 201 with the prompt. Its first runs are queued right away; list them with GET /v1/prompt_runs?prompt=prm_….

sh
curl -X POST "https://spysay.com/v1/prompts" \
  -H "Content-Type: application/json" \
  -H "x-spysay-api-key: <API_KEY>" \
  -d '{"business":"biz_7hQm2","market":"mkt_3xUs","text":"best invisalign dentist near me","intent":"commercial","providers":["chatgpt","google_ai_mode"],"tags":["invisalign"]}'
ts
import { createClient } from "@spysay/sdk";

const spysay = createClient({ baseUrl: "https://spysay.com", token: process.env.SPYSAY_API_KEY! });

const prompt = await spysay.prompts.create({
  business: "biz_7hQm2",
  market: "mkt_3xUs",
  text: "best invisalign dentist near me",
  intent: "commercial",
  providers: ["chatgpt", "google_ai_mode"],
  tags: ["invisalign"],
});
Status Codes
400 parameter_invalid, parameter_unknown, parameter_missing
402 insufficient_credits: the balance can't cover the first runs
404 resource_missing: the business or market doesn't exist

Retrieve, update and delete

Endpoint Description
GET /v1/prompts/:id Retrieve a prompt
PATCH /v1/prompts/:id Change any create field except business. Reactivating a paused prompt with active: true needs enough credits for its next runs (402 insufficient_credits).
DELETE /v1/prompts/:id Returns { "id": "prm_…", "object": "prompt", "deleted": true }
sh
curl -X PATCH "https://spysay.com/v1/prompts/prm_4k2Lx" \
  -H "Content-Type: application/json" \
  -H "x-spysay-api-key: <API_KEY>" \
  -d '{"active":false,"tags":["invisalign","paused"]}'
ts
import { createClient } from "@spysay/sdk";

const spysay = createClient({ baseUrl: "https://spysay.com", token: process.env.SPYSAY_API_KEY! });

await spysay.prompts.update({ id: "prm_4k2Lx", active: false, tags: ["invisalign", "paused"] });

Check a prompt now

POST /v1/prompts/:id/check

Asks every engine the prompt again now, instead of waiting for its next scheduled check. The new answers replace today's runs. API engines answer within the call and cost their usual credits. Engines read in a browser answer within minutes: they're listed in waiting, so call again to collect them until waiting is empty.

A new check can be asked once an hour per prompt (429 rate_limited); calls while engines are still waiting only collect answers. A paused prompt can't be checked (400 parameter_invalid).

sh
curl -X POST "https://spysay.com/v1/prompts/prm_4k2Lx/check" \
  -H "x-spysay-api-key: <API_KEY>"
json
{ "object": "prompt_check", "prompt": "prm_4k2Lx", "requested_at": 1793005200, "waiting": ["gemini", "google_ai_mode"] }

Prompt stats

GET /v1/prompt_stats

Returns per-prompt performance and a business summary over a date range. Pass market for one market, as the overview's market tabs do.

Parameter Type Description
business (required) string biz_ ID
market string Only prompts in this mkt_ market
from YYYY-MM-DD Default: 30 days ago
to YYYY-MM-DD Default: today
provider string Only this provider
json
{
  "object": "prompt_stats",
  "business_id": "biz_7hQm2",
  "range": { "from": "2026-09-25", "to": "2026-10-25" },
  "data": [
    {
      "prompt_id": "prm_4k2Lx",
      "text": "best invisalign dentist near me",
      "last_run_at": 1761350400,
      "responses": 120,
      "mentions": 85,
      "visibility_percent": 71,
      "citation_percent": 65,
      "avg_position": 2.3,
      "avg_sentiment": 42,
      "difficulty": 87,
      "top_competitor_share_percent": 22,
      "top_competitors": [{ "name": "Acme", "url": "https://acme.com", "mentions": 15, "strength": 93 }]
    }
  ],
  "summary": {
    "responses": 120,
    "mentions": 85,
    "citations": 55,
    "visibility_percent": 71,
    "citation_percent": 65,
    "share_of_voice_percent": 68,
    "avg_position": 2.3,
    "avg_sentiment": 42,
    "top_competitor": { "name": "Acme", "url": "https://acme.com", "mentions": 15, "strength": 93 },
    "competitors": [{ "name": "Acme", "share_percent": 40 }, { "name": "Bright Dental", "share_percent": 18 }]
  }
}

competitors lists up to five rivals, ranked by the share of answers that name each one. An answer that names a rival several times counts once. Percentages run from 0 to 100, and so does difficulty, which stays null until enough runs exist. avg_sentiment runs from -100 to 100.

List prompt runs

GET /v1/prompt_runs

Returns each provider's answer to each prompt, along with how it was analyzed.

Parameter Type Description
business (required) string biz_ ID
market string Only runs of prompts in this mkt_ market
prompt string Only this prm_ prompt
provider string Only this provider
status string Only this status
from / to YYYY-MM-DD Date range
limit, starting_after Pagination
json
{
  "id": "prr_9dQe",
  "object": "prompt_run",
  "prompt_id": "prm_4k2Lx",
  "date": "2026-10-25",
  "provider": "chatgpt",
  "status": "analyzed",
  "response": "Popular options include…",
  "mention_position": 2,
  "citation_count": 1,
  "sentiment": 45,
  "analysis": "Brand mentioned second, positively…",
  "competitor_mentions": [{ "name": "Acme", "count": 3 }],
  "citation_domains": ["lumendental.example", "yelp.com"],
  "checked_from": { "country": "US", "city": "Austin", "method": "gps" },
  "created_at": 1761350400
}

checked_from records where the answer was really fetched from. gps means the market's device location was sent, which Google uses. ip means only the exit IP placed the request; ChatGPT and most other providers go by IP alone. It is null until the run is answered.

status moves through queued → running → answered (response received, analysis pending) → analyzed, or ends at failed.

Full answers are kept for 90 days. After that, response, analysis and fan_out_queries come back empty, and the rest of the run (mention_position, competitor_mentions, citation_domains) stays, so history and stats still cover every day.

Visibility reports

GET /v1/visibility_reports

Lists a business's archived reports, newest first. Takes business, limit and starting_after.

json
{
  "id": "rpt_5tVw",
  "object": "visibility_report",
  "business_id": "biz_7hQm2",
  "type": "rolling",
  "period_start": "2026-10-19",
  "period_end": "2026-10-25",
  "report": { "version": "v3", "quick_summary": "…", "action_items": [] },
  "markdown": null,
  "created_at": 1761350400
}

type is rolling for current reports. Older reports may be daily or weekly and carry markdown instead of a structured report.

GET /v1/visibility_reports/today

Returns the state of the current reporting period. Takes business.

json
{
  "object": "visibility_report_status",
  "business_id": "biz_7hQm2",
  "period_end": "2026-10-25",
  "report_id": null,
  "runs_complete": false
}

report_id is set once the period's report exists. runs_complete is true once no runs are still queued, running or awaiting analysis.

Monitor intervals

Interval Behavior
dynamic Adapts to how much answers change: stable prompts run less often, volatile ones daily or more. Default.
weekly Once every 7 days
monthly Once every 30 days