Monitor Copilot answers with an API
Copilot monitoring means sending a fixed set of buyer prompts to Copilot on a schedule, per market, and storing what comes back: the answer and the pages it cites. Over time that tells you whether and how a brand, page or competitor appears.
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. |
country |
yes | Any country code, for the localized answer. GET /v1/countries?model=copilot lists supported codes. |
state |
no | Not accepted: Copilot answers at country level, and a request with state is refused with a 400. |
include.markdown |
no | Adds result.markdown. |
include.rawResponse |
no | Adds result.rawResponse, the answer as normalized stream events; include.rawWire returns null on Copilot. |
All three include flags default to false.
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=360, # a synchronous call can wait up to five 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 }[];
}
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 API with requests.
Step 2: read every field in the response
The body is { "success": true, "result": { ... } }. The fields under result today:
| Field | Type | Meaning |
|---|---|---|
text |
string | Copilot’s answer as plain text. |
markdown |
string | The answer in Markdown, with include.markdown. |
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. |
Coming: search queries, shopping cards and map results
searchQueries (the web searches Copilot ran), shoppingCards (product cards with brands, sellers and numeric prices) and map (businesses and places for local prompts) are coming for Copilot and are not returned yet. Their fields are described on Copilot shopping and map results and 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 metrics
Two numbers per prompt and market cover most reporting:
- Mentioned: your brand name appears in
text. - Cited: a URL on your domain appears in
sources, and at whichposition.
from urllib.parse import urlsplit
def on_domain(url: str, domain: str) -> bool:
host = (urlsplit(url).hostname or "").lower()
return host == domain or host.endswith("." + domain)
def copilot_metrics(result: dict, brand: str, domain: str) -> dict:
cited = [s["position"] for s in result.get("sources", []) if on_domain(s["url"], domain)]
return {
"mentioned": brand.lower() in result.get("text", "").lower(),
"best_source_position": min(cited) if cited else None,
}
Mentions and citations are separate events with separate fixes; mentions vs citations explains why to track both. For share-of-voice formulas across a prompt set, see the AI share of voice framework.
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-09-17",
"webhook": { "url": "https://example.com/hooks/answers" }
},
{
"taskType": "COPILOT",
"payload": { "prompt": "best accounting software for freelancers", "country": "GB" },
"idempotencyKey": "copilot-accounting-freelancers-GB-2026-09-17",
"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-09-17", "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=360).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
searchQueries,shoppingCardsormapon Copilot today. They are coming and are not returned yet. - 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.
- Sending
state. Copilot answers at country level, and a request withstateis refused with a 400 and not billed. - 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
Does the API return Copilot's shopping cards and map results?
Not yet; both are coming. Today a Copilot result carries the answer text, markdown, its cited sources and citation pills. For commerce prompts, measure brand mentions in text and the sources cited until shoppingCards and map ship.
Does the API return the searches Copilot ran?
Not yet. searchQueries is coming for Copilot; until it ships, the field is not returned.
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 API return 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.