AnswerLine Start free

, Tutorials · Copilot

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:

  1. Mentioned: your brand name appears in text.
  2. Cited: a URL on your domain appears in sources, and at which position.
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

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.

Try it on your own prompts

100 free requests on any engine, no card. One POST returns the answer, sources and citations as JSON.

Keep reading