How to Track Your Brand in ChatGPT, Perplexity, Copilot and Gemini (With an API)
To track your brand in ChatGPT, Perplexity, Microsoft Copilot and Google Gemini, ask each engine the same fixed set of buyer questions several times, keep every answer with its citations, and report three numbers per engine: how often the answer names you (mention rate), how much of what it cites is yours (citation share), and how early you are named (position of first mention). This tutorial builds that tracker in Python on Searlo's AI visibility API and prices it per engine.
Searlo is independent and is not affiliated with, endorsed by or sponsored by OpenAI, Microsoft, Google or Perplexity; ChatGPT, Copilot, Gemini and Perplexity are trademarks of their owners.
Why one answer tells you nothing
Ask ChatGPT for the best CRM for a small business twice and you can get two different lists of brands, backed by two different sets of sources. The answers come from each engine's consumer interface, the one people actually use, and they are non-deterministic: wording, brands and citations move between runs of the same prompt. A single screenshot is an anecdote. A rate over many samples is a measurement.
Each engine also has to be measured on its own. In a study by Ahrefs of 15,000 long-tail prompts (published August 11, 2025; checked 2026-09-29), on average only 12% of the links cited by ChatGPT, Gemini and Copilot appeared in Google's top 10 for the same prompt, against 28.6% for Perplexity. Your Google rankings do not tell you what the answer engines say, and one engine tells you little about another.
What you need
• A Searlo API key. New accounts get 3,000 free credits with no card: enough for 375 ChatGPT answers or 1,500 Perplexity answers.
• Python 3.9 or later with the requests package.
• One endpoint, identical for all four engines: GET https://api.searlo.tech/api/v1/search/ai/{engine}?q=...&gl=us&hl=en, with your key in the x-api-key header. {engine} is chatgpt, perplexity, copilot or gemini.
Every response has the same shape; Listing 1 shows one from ChatGPT, with real keys, illustrative values and placeholder brands. answer is the text; citations lists each cited source as {index, url, title, domain}; sources is the list of unique domains the answer cited, which is what share of voice is counted from. Copilot adds citationPills, the source chips it shows inside the answer. complete is false when the capture stopped before the answer finished, and cached says whether the answer was served from cache. The engine pages for ChatGPT, Perplexity, Copilot and Gemini cover what differs between them.
The code is split into five numbered listings; each Python and shell listing starts with a comment that names its file.
json{ "success": true, "searchParameters": { "q": "best crm for a small business", "type": "ai", "engine": "chatgpt", "mode": "ui", "gl": "us", "hl": "en" }, "answer": "For a small team, YourBrand and Competitor A are the usual starting points. Competitor B suits teams that need…", "answerMarkdown": "For a small team, YourBrand and Competitor A are the usual starting points. Competitor B suits teams that need…", "citations": [ { "index": 1, "url": "https://www.example-reviews.com/best-crm", "title": "The best CRM software", "domain": "example-reviews.com" }, { "index": 2, "url": "https://help.yourbrand.com/pricing", "title": "YourBrand pricing", "domain": "help.yourbrand.com" } ], "sources": ["example-reviews.com", "help.yourbrand.com"], "related": [], "model": "", "threadUrl": null, "complete": true, "fetchedAt": "2026-09-29T09:41:12.087Z", "source": "searlo.tech(chatgpt)", "cached": false }
Step 1: Build a prompt set that sounds like your buyers
The prompt set decides whether the numbers mean anything. Twenty to two hundred prompts is a common size. Write them the way a buyer types, not the way your homepage talks, and mix five kinds:
• Category: "best crm for a small business"
• Use case: "crm for a five-person sales team that hates data entry"
• Comparison: "competitor a vs competitor b for a startup"
• Alternatives: "alternatives to competitor a"
• Problem: "how do I stop sales leads falling through the cracks"
Seed the list from real search behaviour. The Google Autocomplete API returns the completions Google suggests for a stem at 0.5 credits a call, and the People Also Ask API returns the questions Google shows under a keyword. Tag each prompt with its kind so you can report per topic, and freeze the wording: an edited prompt is a new measurement, so retire the old ID and add a new one.
Then list your brand and each competitor with every name an engine might use (the product name, a short form, a former name) and the domains each one owns. Listing 2 holds both, plus the sampling plan.
python# Listing 2: config.py - the prompt set, the brands to look for, the sampling plan PROMPTS = [ # (id, kind, prompt): never edit a prompt in place; retire it and add a new id ("p01", "category", "best crm for a small business"), ("p02", "use case", "crm for a five-person sales team that hates data entry"), ("p03", "comparison", "competitor a vs competitor b for a startup"), ("p04", "alternatives", "alternatives to competitor a"), ("p05", "problem", "how do I stop sales leads falling through the cracks"), ] # Every name an engine might use for each brand, and the domains it owns BRANDS = { "YourBrand": {"aliases": ["yourbrand", "your brand"], "domains": ["yourbrand.com"]}, "Competitor A": {"aliases": ["competitor a"], "domains": ["competitor-a.com"]}, "Competitor B": {"aliases": ["competitor b"], "domains": ["competitor-b.com"]}, } ENGINES = ["chatgpt", "perplexity", "copilot", "gemini"] SAMPLES = 3 # answers vary between runs, so ask every prompt several times GL, HL = "us", "en" # market and language
Step 2: Sample every engine several times
Three to five answers per prompt and engine is a sensible range: enough to see a rate, cheap enough to run every week. Two API behaviours decide how you collect them:
• Cache. A repeat of the same engine, prompt, gl and hl within an hour is served from cache, marked cached: true, and billed like a live answer. For sampling that buys you the same answer twice, so send cache=false on every request; each one is then a fresh, billed run.
• In-flight merging. Identical requests in flight at the same moment can be merged into one run, and each is still billed. Send the samples of one prompt one after another. Different prompts and engines can run side by side.
A live answer typically takes 10 to 30 seconds, because Searlo asks the engine in a real browser session and waits for it to finish writing. Treat collection as a background job: give each request a long timeout (the listings use 180 seconds) and run a few prompt and engine pairs in parallel. Listing 3 uses four workers, so its 60 answers (5 prompts, 4 engines, 3 samples) take a few minutes.
A busy engine answers HTTP 429 with a Retry-After header, and an upstream failure answers 503. Neither is charged: requests that fail, or return success: false, are refunded automatically. Listing 3 waits and retries on both, and appends each answer to a JSON Lines file with the day, engine, prompt and sample number, so you never pay to collect the same run twice.
python# Listing 3: collect.py - ask every engine every prompt SAMPLES times, keep every answer import json import time from concurrent.futures import ThreadPoolExecutor from datetime import date import requests from config import ENGINES, GL, HL, PROMPTS, SAMPLES API = "https://api.searlo.tech/api/v1/search/ai" HEADERS = {"x-api-key": "YOUR_API_KEY"} def ask(engine, prompt, attempts=4): """One fresh answer as a dict, or None if the engine kept refusing.""" for _ in range(attempts): try: r = requests.get( f"{API}/{engine}", headers=HEADERS, params={"q": prompt, "gl": GL, "hl": HL, "cache": "false"}, timeout=180, # a live answer typically takes 10-30 s ) except requests.RequestException: time.sleep(10) continue if r.status_code in (429, 503): # engine busy or upstream failure: not charged time.sleep(int(r.headers.get("retry-after", 30))) continue r.raise_for_status() # anything else, such as 402 (out of credits), stops the run data = r.json() return data if data.get("success") else None return None def sample(job): """Every sample of one prompt on one engine, sent one after another: identical requests in flight at the same moment are merged into one run.""" engine, (prompt_id, kind, prompt) = job rows = [] for i in range(SAMPLES): data = ask(engine, prompt) if data is None: continue rows.append({ "day": date.today().isoformat(), "engine": engine, "prompt_id": prompt_id, "kind": kind, "prompt": prompt, "sample": i, "answer": data["answer"], "citations": data["citations"], "sources": data["sources"], "complete": data["complete"], "cached": data["cached"], }) return rows jobs = [(engine, p) for engine in ENGINES for p in PROMPTS] with ThreadPoolExecutor(max_workers=4) as pool, open("answers.jsonl", "a", encoding="utf-8") as out: for rows in pool.map(sample, jobs): # different engines and prompts run side by side for row in rows: print(json.dumps(row), file=out)
Step 3: Compute mention rate, citation share and first-mention position
Listing 4 turns the answers into a table per engine, one row per tracked brand:
• Mention rate: the share of answers whose text names the brand, matched on whole words and case-insensitively against every alias in the config. It reads the plain-text answer field.
• Citation rate and citation share, two views of the citations. The citation rate is the share of answers whose sources include one of the brand's domains: how often you are cited at all. Citation share is the brand's fraction of every entry in citations across those answers: how much of the evidence the engine shows points to you. Count subdomains, since sources holds hostnames such as help.yourbrand.com with only www. removed.
• Position of first mention: in each answer that names you, your place in the order the tracked brands are first named, where 1 means you came before every competitor. The listing reports the median, and the share of all answers in which you are named first, which is the number that moves when you become the default recommendation.
In the printed table these are the mentioned, cited, share, first and median pos columns.
In AI search, share of voice usually means one of two ratios over a fixed prompt set: your mentions divided by all tracked brands' mentions, or your citations divided by all citations. Choose one, write down which, and keep it; a trend is only a trend if the definition stays still.
Three habits keep the numbers honest. Keep engines apart: an average across ChatGPT and Perplexity hides an engine that never cites you, and the fix for each engine is different. Drop answers with complete: false, since the capture ended before the answer did. And expect some answers from ChatGPT and Gemini with no citations at all: they cite only when they search the web, so a prompt they answer from general knowledge can return a confident answer with an empty citations list. It shows up as a mention without a citation, which is a finding in its own right.
python# Listing 4: metrics.py - mention rate, citation share and first-mention position per engine import json import re import sys from collections import defaultdict from statistics import median from config import BRANDS DAY = sys.argv[1] if len(sys.argv) > 1 else None # a run's day, e.g. 2026-09-29 def first_offset(text, aliases): """Character offset of the brand's first mention in the answer, or None.""" low = text.lower() offsets = [] for alias in aliases: pattern = "(?<![a-z0-9])" + re.escape(alias.lower()) + "(?![a-z0-9])" match = re.search(pattern, low) if match: offsets.append(match.start()) return min(offsets) if offsets else None def owns(domain, domains): """Hostnames arrive with only www. removed, so count subdomains too.""" return any(domain == d or domain.endswith("." + d) for d in domains) rows = [json.loads(line) for line in open("answers.jsonl", encoding="utf-8")] DAY = DAY or max(r["day"] for r in rows) # default: the latest run rows = [r for r in rows if r["complete"] and r["day"] == DAY] # skip captures cut short by_engine = defaultdict(list) for r in rows: by_engine[r["engine"]].append(r) for engine, answers in sorted(by_engine.items()): n = len(answers) all_citations = sum(len(a["citations"]) for a in answers) print() print(f"{engine}: {n} answers, {all_citations} citations") print(f" {'brand':<14}{'mentioned':>10}{'cited':>7}{'share':>7}{'first':>7}{'median pos':>12}") for brand, cfg in BRANDS.items(): named = cited = linked = first = 0 positions = [] for a in answers: offsets = {b: first_offset(a["answer"], c["aliases"]) for b, c in BRANDS.items()} if offsets[brand] is not None: named += 1 order = [b for _, b in sorted((o, b) for b, o in offsets.items() if o is not None)] positions.append(order.index(brand) + 1) first += order[0] == brand cited += any(owns(d, cfg["domains"]) for d in a["sources"]) linked += sum(owns(c.get("domain") or "", cfg["domains"]) for c in a["citations"]) pos = f"{median(positions):g}" if positions else "-" print( f" {brand:<14}{named / n:>10.0%}{cited / n:>7.0%}" f"{linked / max(all_citations, 1):>7.0%}{first / n:>7.0%}{pos:>12}" )
Step 4: Run it weekly and read the trend, not the week
Schedule the collector with cron (Listing 5); the metrics script reports on the latest run by default, or on any day you pass it. Weekly is typical, and daily helps during a launch or a PR push. Read changes against the noise you should expect. With 150 answers per engine (50 prompts, 3 samples) and a mention rate near 30%, the standard error of that rate is about 3.7 points, so a 5-point move between two weeks is within the noise, while a move that holds for three or four runs is not. Samples of the same prompt are correlated, so treat that figure as the least noise you will see, not the most.
Two cuts are worth adding once the basics run. By prompt kind: a brand often wins category prompts and vanishes from comparison prompts, or the reverse. And by cited domain: the domains cited most often in answers that name a competitor but not you are your outreach and content list. Fetch those pages with the web scraping API to see what the engines are quoting.
bash# Listing 5: crontab - collect every Monday at 06:00, then append that run's report 0 6 * * 1 cd /opt/ai-visibility && python3 collect.py && python3 metrics.py >> report.txt
What AI visibility tracking costs, per engine
Credits are charged per answer at a different rate for each engine, because each costs a different amount to run live. In dollars at $0.30 per 1,000 credits, the Scale pack rate ($74.99 for 250,000 credits):
Worked examples, all on the four engines:
• The run in this tutorial, 5 prompts with 3 samples, is 330 credits: about 10 cents.
• 50 prompts with 3 samples, weekly, is 3,300 credits a run: $0.99, or $12.87 for a 13-week quarter.
• 50 prompts with 3 samples, daily, is 99,000 credits over 30 days: $29.70.
• An agency tracking 10 clients at 100 prompts and 5 samples each, weekly, spends 110,000 credits a week: $33.00 at $0.30 per 1,000, or $22.00 at the Enterprise pack rate of $0.20.
Credits come in one-time packs from $3.99 with no subscription (see pricing), and new users' credits are valid for 90 days, so buy a pack sized to about 90 days of runs. If the budget is tight, cut prompts before you cut samples: one sample per prompt is the anecdote this tutorial is built to avoid.
Honest limits
• Logged-out answers. Each engine is asked the way an anonymous visitor asks it: logged-out chatgpt.com, Copilot on Bing, the signed-out Gemini web app and perplexity.ai. A signed-in user with history and memory can see a different answer.
• Country is best effort. gl routes the request through an exit in that country when one is available, so read per-country results as indicative.
• Latency. 10 to 30 seconds per live answer, sometimes longer. This is a batch job, not something to call while a user waits.
• Capacity. A busy engine answers 429 with Retry-After. Build the retry in, as Listing 3 does, rather than treating it as an outage.
• Live interfaces change. Every answer is read from a consumer product that changes without notice. If every brand's numbers drop at once on one engine, check the data before you report a visibility change.
Where to go next
• Google's side of the same question: AI Overviews sit on the results page, not in these four engines. How to monitor Google AI Overviews for your keywords builds the matching tracker on GET /serp, and the Google AI Overview API page lists the fields.
• Organic rankings for the same topics: the Rank Tracking API.
• A dashboard instead of code: Best AI visibility tools in 2026 compares eight tools with verified pricing.
• Every parameter: the docs and the API reference.
Frequently Asked Questions
How do I track my brand in ChatGPT?
Write a fixed set of prompts your buyers ask, send each one to ChatGPT several times with cache=false so every answer is a fresh run, and store the answer text and its citations. Your mention rate is the share of answers that name you, and your citation rate is the share whose cited domains include yours. On Searlo that is GET /search/ai/chatgpt at 8 credits per answer, or $2.40 per 1,000 answers at $0.30 per 1,000 credits.
How many times should I ask each prompt?
Three to five times per engine per run. The answers are non-deterministic, so one answer per prompt cannot separate a real change from chance. At 50 prompts and 3 samples you have 150 answers per engine, and a mention rate near 30% then carries a standard error of about 4 points.
How much does it cost to monitor a brand across ChatGPT, Perplexity, Copilot and Gemini?
Per answer, Perplexity is 2 credits, Gemini 4, ChatGPT 8 and Copilot 8, so one prompt on all four engines is 22 credits. At $0.30 per 1,000 credits, 50 prompts with 3 samples on all four engines costs $0.99 per run. New accounts get 3,000 free credits, and packs are one-time purchases with no subscription.
What is share of voice in AI search?
The part of a fixed prompt set's answers that goes to your brand, measured either as mentions (your mentions divided by all tracked brands' mentions) or as citations (your cited links divided by all cited links). Measure it per engine and per prompt topic, and keep the definition the same from report to report.
Why do I get the same answer twice in a row?
A repeat of the same engine, prompt, gl and hl within an hour is served from cache and marked cached: true, and it is billed like a live answer. For sampling, pass cache=false, and send the samples of one prompt one after another, because identical requests in flight at the same time can be merged into one run.
Why does ChatGPT or Gemini sometimes return no citations?
They cite only when they search the web. A prompt they answer from general knowledge can return a full answer with an empty citations list, which shows up in your data as a mention without a citation.
Is this the official ChatGPT or Gemini API?
No. A model API such as OpenAI's or Google's gives your code a model's reply. Searlo returns the answer each public consumer product shows a logged-out visitor, which is what brand tracking needs to measure.