Track brand mentions in Copilot answers
To track brand mentions in Copilot answers, send a fixed set of buyer prompts to Microsoft Copilot on a schedule, per market, and store what comes back: the answer and the pages it cites. From each answer you read whether Copilot named your brand and whether it cited your domain, and how early. Over time that tells you whether and how a brand, page or competitor appears.
The fields are documented on the Copilot engine page. If you are comparing ways to collect Copilot answers rather than measuring them, see best Copilot scrapers. For Microsoft’s own developer products (Microsoft 365 Copilot, Copilot Studio, Foundry), see Microsoft Copilot API options.
Why Copilot needs its own measurement
Microsoft’s own documentation says Copilot is grounded in web search results for information-seeking conversations and “provides hyperlinked citations following generated text responses” (Transparency Note for Copilot, checked 2026-09-17). The response below returns those citations as fields.
Copilot’s answers are grounded in Bing rather than Google Search, so the cited pages and recommended products can differ from ChatGPT, Perplexity or Gemini on the same prompt. Whether they differ for your category is an empirical question.
The endpoint and request fields
POST https://api.answerline.dev/v1/monitor/copilot, authenticated with Authorization: Bearer <key> (authentication).
| Field | Required | What it does |
|---|---|---|
prompt |
yes | The prompt sent to Copilot (up to 2,000 characters). |
country |
yes | Any country code, for the localized answer. GET /v1/countries?model=copilot lists supported codes. |
state |
no | A US state code; implies country US. Copilot answers for the whole US (states). |
include.markdown |
no | Adds result.markdown. |
include.rawResponse |
no | Adds result.rawResponse, the answer as normalized stream events; include.rawWire returns null on Copilot. |
Both include flags default to false. include.html is not available on Copilot: a request that sets it is refused with a 400 and not charged.
Step 1: make a synchronous call
curl
curl -X POST https://api.answerline.dev/v1/monitor/copilot \
-H "Authorization: Bearer $ANSWERLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"prompt": "best accounting software for freelancers",
"country": "US",
"include": { "markdown": true }
}'
Python (requests)
import os
import requests
API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['ANSWERLINE_API_KEY']}"}
resp = requests.post(
f"{API}/v1/monitor/copilot",
headers=HEADERS,
json={"prompt": "best accounting software for freelancers", "country": "US", "include": {"markdown": True}},
timeout=660, # a synchronous call can wait up to ten minutes
)
resp.raise_for_status()
result = resp.json()["result"]
print(result["text"][:300])
for source in result.get("sources", []):
print(source["position"], source["url"], source.get("label"))
print("charged:", resp.headers.get("X-Credits-Charged"))
TypeScript (@answerline/sdk)
import { Client } from "@answerline/sdk";
interface CopilotResult {
text?: string;
markdown?: string;
sources?: { position: number; url: string; label?: string; description?: string }[];
citationPills?: { citationPillId: number; position: number; url: string; domain: string }[];
}
const client = new Client({ apiKey: process.env.ANSWERLINE_API_KEY! });
const { result } = await client.monitor.copilot<CopilotResult>({
prompt: "best accounting software for freelancers",
country: "US",
});
for (const s of result.sources ?? []) console.log(s.position, s.url);
The TypeScript SDK is on npm as @answerline/sdk; the Python example calls the HTTP endpoint with requests.
Step 2: read every field in the response
The body is { "success": true, "result": { ... } }. The fields under result:
| Field | Type | Meaning |
|---|---|---|
text |
string | Copilot’s answer as plain text. |
markdown |
string | The answer in Markdown, with include.markdown. |
links[] |
array | The links in the answer text, in order. |
sources[] |
array | Cited sources: position, url, label, description. |
citationPills[] |
array | Inline citation chips: one entry per chip and source, with citationPillId, position (into sources), url and domain. |
rawResponse[] |
array | The answer as normalized stream events, with include.rawResponse. result.rawWire (with include.rawWire) is null on Copilot. |
Copilot results do not carry the searches Copilot ran, product cards or map results. For product and local prompts, measure mentions in text and the pages in sources. The cited pages are described on Copilot sources.
rawResponse
rawResponse follows Copilot’s own format, which can change without notice. Use it for debugging, not as a data source for metrics.
Step 3: turn a response into brand metrics
Three numbers per prompt and market cover most reporting:
- Mentioned: your brand name or an alias appears in
text. - Cited: a URL on your domain appears in
sources, and at whichposition. - In a chip: your domain sits behind one of the inline
citationPills.
import re
from urllib.parse import urlsplit
def on_domain(url: str, domain: str) -> bool:
host = (urlsplit(url).hostname or "").lower().removeprefix("www.")
return host == domain or host.endswith("." + domain)
def copilot_metrics(result: dict, aliases: list[str], domain: str) -> dict:
brand = re.compile(r"\b(" + "|".join(re.escape(a) for a in aliases) + r")\b", re.IGNORECASE)
cited = [s["position"] for s in result.get("sources", []) if on_domain(s["url"], domain)]
return {
"mentioned": bool(brand.search(result.get("text", ""))),
"best_source_position": min(cited) if cited else None,
"in_chip": any(on_domain(p["url"], domain) for p in result.get("citationPills", [])),
}
Roll these up into a mention rate, a citation rate and an average best position across the prompt set. Mentions and citations are separate events with separate fixes; mentions vs citations explains why to track both. For share-of-voice formulas, see the AI share of voice framework, and for how many prompts a rate needs, how to measure brand mentions.
Step 4: schedule it with async tasks
For anything recurring, submit tasks instead of holding synchronous connections. A task takes the same body as payload, with taskType: "COPILOT":
curl -X POST https://api.answerline.dev/v1/async/task/batch \
-H "Authorization: Bearer $ANSWERLINE_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{
"taskType": "COPILOT",
"payload": { "prompt": "best accounting software for freelancers", "country": "US" },
"idempotencyKey": "copilot-accounting-freelancers-US-2026-11-11",
"webhook": { "url": "https://example.com/hooks/answers" }
},
{
"taskType": "COPILOT",
"payload": { "prompt": "best accounting software for freelancers", "country": "GB" },
"idempotencyKey": "copilot-accounting-freelancers-GB-2026-11-11",
"webhook": { "url": "https://example.com/hooks/answers" }
}
]'
import hashlib
def copilot_tasks(prompts: list[str], countries: list[str], day: str, hook: str) -> list[dict]:
tasks = []
for prompt in prompts:
slug = hashlib.sha1(prompt.encode()).hexdigest()[:12]
for country in countries:
tasks.append({
"taskType": "COPILOT",
"payload": {"prompt": prompt, "country": country},
"idempotencyKey": f"copilot-{slug}-{country}-{day}",
"webhook": {"url": hook},
})
return tasks
tasks = copilot_tasks(prompts, ["US", "GB"], "2026-11-11", "https://example.com/hooks/answers")
for start in range(0, len(tasks), 500):
batch = requests.post(f"{API}/v1/async/task/batch", headers=HEADERS, json=tasks[start:start + 500], timeout=660).json()
for item in batch["results"]:
if not item["success"]:
print(tasks[start + item["index"]]["idempotencyKey"], item["error"]["code"])
A batch holds 1 to 500 tasks, and each item succeeds or fails on its own (VALIDATION_ERROR, RESOURCE_ALREADY_EXISTS or INSUFFICIENT_CREDITS). The webhook body is the same JSON as GET /v1/async/task/{id}, with the Copilot result under response.result. Verify the Webhook-Signature header before storing anything (verify webhook signatures), and dedupe by task.id, since deliveries can repeat or arrive out of order.
The key is derived from prompt, market and day, so re-sending the same submission after a timeout never creates a task twice; idempotency keys explained covers the pattern.
What it costs
Per billing, at the Starter tier:
| Request | Price |
|---|---|
| Copilot request | $0.00274 |
include.markdown or include.rawResponse |
+0 |
A worked example: 50 prompts in 2 countries is 50 x 2 x $0.00274 = $0.274 per run, or $1.10 a month at a weekly cadence. A new account’s 100 free requests cover one run; after that you top up your balance (see pricing). For budgeting across engines and cadences, see estimating your monitoring bill.
A task reserves its maximum cost in dollars (credits.creditsToCharge) and is charged the actual cost (credits.creditsCharged) only on success. Failed tasks are not charged.
Sync or async: decision criteria
| Situation | Use |
|---|---|
| A person is waiting for the answer (a dashboard “check now” button, debugging a prompt) | Synchronous call |
| A scheduled prompt set, any size | Async batch with a webhook |
| Fewer than a handful of tasks, no public endpoint to receive webhooks | Async tasks and polling GET /v1/async/task/{id} |
The trade-offs in more depth are in sync calls, async tasks or webhooks.
Pitfalls
- Treating
sourcesas always populated. An answer can cite nothing; read it with a default. - Expecting product cards or map results from Copilot. They are not returned; measure product and local presence through
textandsources. - Matching brand names loosely. A plain substring test matches “Acme” inside “Acmeology”; use word boundaries and an alias list.
- Retrying synchronous calls blindly. A 500 or 502 from a synchronous call is final (the server already retried) and is not charged. Retry only when the request never reached the server; details in handling API errors.
- Reporting a Copilot answer sent with
stateas a state result. Copilot acceptsstatebut answers for the whole US. - Reading one run as the truth. Answers vary between runs of the same prompt. Trend several runs before acting; answer volatility and sampling vs census explain how many.
- Random idempotency keys. A key generated per attempt defeats its purpose. Derive it from what the task means.
Where Copilot fits in a monitoring program
Run the same prompt set on Copilot, ChatGPT and Gemini, then compare citation and mention rates engine by engine. A brand that is cited on one engine and absent on another points to a retrieval-layer gap you can investigate through the sources of each. Prompt design matters more than engine count; start with designing a prompt set, and see the AI visibility tracking and competitor analysis use cases.
Field list and a live request example are on the Copilot engine page; the quickstart gets you to a first call.
Questions
How do I track brand mentions in Copilot?
Run a fixed set of buyer prompts through Copilot on a schedule, per market, and store each answer. For every answer record whether the text names your brand and whether sources cites your domain, and at what position. Report those rates across runs, not from a single answer.
Which fields does a Copilot result carry?
text, markdown (with include.markdown), links, sources and citationPills. Copilot results do not carry the searches it ran, shopping cards or map results, so for commerce prompts measure brand mentions in text and the sources cited.
Can I build metrics on Copilot's rawResponse?
Use it for debugging only. rawResponse follows Copilot's own format, which can change without notice, so build metrics from the typed fields instead.
Does the response include Copilot's inline citation chips?
Yes. result.citationPills lists them; group entries by citationPillId to rebuild a chip, and each entry's position points into result.sources, which lists every page the answer cites, in order.
Should I use synchronous calls or async tasks for Copilot?
Use synchronous calls for interactive checks and debugging. Use async tasks for scheduled prompt sets: they queue with no count limit (about 100,000 typical tasks per account) and can be submitted 500 at a time.