How to pull citations from Microsoft Copilot
Microsoft Copilot runs on OpenAI models but grounds its replies in a live Bing search, so most delivered answers arrive with a source list attached — denser than Gemini, not quite the near-certainty of a Google AI Overview. That source list is the closest thing this surface has to a ranking. To collect it across a query set — positions preserved, hosts aggregated, every run stored so next week's can be diffed against this week's — POST the query to https://api.agentgeo.org/v1/fetches with "surfaces": ["copilot"] and read answers[].sources[], which comes back as structured JSON with title, url and position on every entry.
This guide covers the manual copy, the one-call fetch in curl and Python, a short logger, the agent version over MCP, and the trap that catches people here: an empty sources[] on a delivered Copilot record is a legitimate outcome, not a failed fetch.
To pull citations from a Microsoft Copilot answer, take the numbered source list Copilot shows beside its reply — each entry is a page it pulled from Bing while writing. By hand that's copy-paste. Programmatically it's one POST, and the response hands back sources[] with title, url and position already separated, which is what makes aggregating across a query set and diffing across weeks possible at all.
The manual way (and why it stops working)
It really is simple for one query. Ask Copilot at copilot.microsoft.com — say, best help desk software for small business — and the reply comes back with citation chips beside it. Open each one, copy the link, and keep the order: the first citation is not the eighth. Paste the links into a sheet next to the query and the date and you have one row of a citation log, which is more than most teams keep. The first pass genuinely earns its hour — run a handful of your category queries and the same two or three domains keep supplying Copilot's answers.
The second pass is where it unravels.
- Copying scrambles the order. Multi-select the chips, paste into a sheet, and the positions arrive shuffled — or as bare titles with the URLs stripped out. Since the order was the only thing separating this from a flat list of domains, that's the dataset gone.
- Hosts are the unit you'll actually want. Rolling
docs.globex.com/setupandglobex.com/pricingup into "Globex" is a string-splitting chore, and by hand it's a chore you redo on every citation of every query. - A query set multiplies everything. Twenty queries at eight sources each, weekly, is over a hundred and fifty links a week transcribed without a single typo. Forever.
- Copilot doesn't cite every reply. Sometimes it answers straight from the model and shows no sources at all. By hand that empty answer is indistinguishable from you forgetting to copy the chips — so months later you can't tell which happened.
- A spreadsheet column is a dead end. Pasted links can't fire an alert, populate a client report, or be subtracted from last month's run — and that subtraction is the only reason anyone collects citations in the first place.
Do it with one API call
The API returns the citation list already parsed. Because Copilot grounds its answer in a live Bing search, most delivered records carry one — denser than Gemini, broadly on par with Perplexity, and one of the six engines you can pull the same way. Note the word most: Copilot can answer from what the model already knows without reaching for Bing, and then sources[] comes back empty on a record that still delivered.
curl -X POST https://api.agentgeo.org/v1/fetches \
-H "Authorization: Bearer ag_live_..." \
-H "Content-Type: application/json" \
-d '{"query": "best help desk software for small business", "surfaces": ["copilot"], "country": "US"}'Each entry in sources[] has a title, a url and a position — and because those URLs came out of a live Bing search, they're real pages you can open, not references the model half-remembered. That's enough to print a per-query citation list and, at the same time, aggregate which hosts keep supplying Copilot across the whole set:
{
"id": "run_8f2a1c",
"query": "best help desk software for small business",
"surfaces": ["copilot"],
"status": "completed",
"recordsDelivered": 1,
"creditsCharged": 1,
"answers": [
{
"surfaceKey": "copilot",
"status": "delivered",
"answerText": "For a small support team, Globex Support and Acme Desk come up most often, with Initech Helpdesk a common budget pick...",
"sources": [
{ "title": "Globex Support — Shared Inbox & SLAs", "url": "https://globex.com/help-desk", "position": 1 },
{ "title": "Acme Desk: Help Desk Software Pricing", "url": "https://acme.com/pricing", "position": 2 },
{ "title": "12 Best Help Desk Tools for 2026", "url": "https://example.com/best-help-desk-software", "position": 3 }
],
"fetchedAt": "2026-07-18T09:14:22Z",
"latencyMs": 8231
}
]
}The whole signal sits in answers[].sources[]. This pass prints each query's ordered citations and tallies the hosts that recur — while parking the delivered-but-uncited answers in their own list, so an empty source list never gets counted as a host losing ground:
import requests
from collections import Counter
from urllib.parse import urlparse
KEY = "ag_live_your_key_here"
QUERIES = [
"best help desk software for small business",
"help desk with shared inbox and SLA tracking",
"cheapest help desk software for a support team",
]
def host(url):
h = urlparse(url).netloc.lower()
return h[4:] if h.startswith("www.") else h
cited = Counter() # how often each host appears in a Copilot source list
top_slot = Counter() # how often each host is cited first
uncited = [] # delivered answers that carried no sources at all
for query in QUERIES:
resp = requests.post(
"https://api.agentgeo.org/v1/fetches",
json={"query": query, "surfaces": ["copilot"], "country": "US"},
headers={"Authorization": f"Bearer {KEY}"},
timeout=200, # the API waits up to 180s for live surfaces
)
resp.raise_for_status()
for answer in resp.json()["answers"]:
if answer.get("status") == "failed":
continue # slow scrape: zero credits, redeemable (see gotchas)
sources = sorted(answer.get("sources", []), key=lambda s: s["position"])
if not sources:
# Delivered, but Copilot answered without citing Bing. Legitimate.
uncited.append(query)
continue
print(f"\n{query}")
for src in sources:
h = host(src["url"])
cited[h] += 1
if src["position"] == 1:
top_slot[h] += 1
print(f" {src['position']:>2}. {h} — {src['title']}")
print("\nmost-cited hosts across the query set")
for h, n in cited.most_common(10):
print(f" {n:>2}x {h} (position 1: {top_slot[h]})")
if uncited:
print(f"\ndelivered but uncited: {', '.join(uncited)}")One run of that is a snapshot: today's Copilot citations and the hosts that recur across your queries. It's evidence, not a verdict — the record comes back exactly as fetched and AgentGEO ranks nothing. Three queries against one surface costs at most three credits, one per delivered record; a record that returns status: "failed" costs nothing, and a delivered-but-uncited answer still costs its credit because Copilot did answer. Want to test the shape first? An ag_test_ key returns clearly-labelled demo records at zero credits.
How reliably that source list shows up depends on how the engine is grounded, and it varies a lot across the six:
| Surface | When sources show up | What that means for citation tracking |
|---|---|---|
copilot | Usually — it grounds answers in a live Bing search | Dense and reliable, but not guaranteed. An empty sources[] on a delivered record is legitimate, just less common |
google_ai_overview | Essentially always on a delivered record — the block is built from a search | The most complete series of the six. Plan for a missing overview, not a missing list |
perplexity | Nearly every answer — it searches, then writes | Dense and reliable. Directly comparable to Copilot in shape, if not in source mix |
chatgpt | Only when it browses, and the one surface honouring the web_search toggle | Expect gaps. An empty sources[] is an outcome, not a failed fetch |
gemini | Only when it grounds an answer in a live search | Frequently empty on delivered records. Track brand mentions in the prose instead |
Copilot's citations are live Bing results, which makes them unusually actionable: every url is a page you can open right now and read, not a title the model reconstructed from training. That's the real payoff over a surface like Gemini. The caveat is the word usually — Copilot answers plenty of queries straight from the model without searching, and those records come back delivered with an empty sources[]. Log the empty ones as delivered-and-uncited, never as a fetch that failed: they're two different facts, and only one of them costs a credit.
Let your agent do it
Citations are where an agent pays for itself: it can pull Copilot's sources, then open the Bing result that outranked you and draft the page that should have been there instead. Wire the server in once and the pull becomes an instruction:
claude mcp add agentgeo -- npx -y agentgeo-mcp --key ag_live_...agentgeo-mcp runs over stdio, carries zero npm dependencies and needs Node.js 18+. Its single tool, fetch_raw_answers, returns each record verbatim — sources[] included, positions intact, country defaulting to US. Now ask for the outcome instead of the JSON:
Pull copilot for our five help desk queries and give me every cited host with its position. Then open whatever sits at position 1 for the first query and tell me what our page is missing compared to it.What comes back is the record and nothing else — no score, no ranking, no opinion about whether position 4 is bad news. That's the point of a GEO data layer: the interpretation happens where your context already lives, in the agent that can open the competing page, hold it against yours and commit the resulting brief next to the site it describes. Prefer to drive it from a script? The Python and curl paths hit the identical endpoint.
Common problems
Citation data corrupts quietly. On Copilot, these five are where it happens:
- A slow scrape fails soft, and you can redeem it. When Copilot takes too long to scrape, the record returns
status: "failed"at zero credits with aproviderFields.snapshot_id. A follow-up call with that id and the same singlecopilotsurface redeems the finished answer instead of paying to scrape it again — retry the id, don't re-run the query from scratch. - An empty source list on a delivered record is a real answer, not a failure. Copilot sometimes replies straight from the model without searching Bing, and that record arrives
deliveredwith an emptysources[]. It costs its credit and it belongs in your data as delivered-and-uncited — dropping it silently makes every cited host look like it's slipping. - Normalise the URL before you count it. Bing hands back the same page wearing tracking parameters, trailing slashes and redirect wrappers, and each variant becomes its own row in a naive tally. Lowercase the host, strip a leading
www., drop the query string, then count. - Decide once whether two pages from one domain count once or twice. Copilot often cites several pages from the same site. Both conventions are defensible; switching halfway through a quarter silently rewrites every figure you already reported.
- This is consumer Copilot, and locale steers it. The surface is copilot.microsoft.com — not GitHub Copilot and not the Microsoft 365 assistant. It honours
country, which shifts the Bing results it cites, but notlanguageand notweb_search— so keep a separate log per market rather than one file you can't disentangle later.
Summary
Checking a single answer? Ask Copilot and copy the citations off the screen — they're right there and it costs nothing. Once the same question has to be put to twenty or thirty queries every week, fetch them instead: title, url and position arrive as separate fields, the aggregation is a Counter, and appending each run to JSONL is what turns a pile of snapshots into a trend with the position movement still visible inside it. Just keep the delivered-but-uncited answers in a column of their own — on Copilot they're common enough to distort the picture if you pretend they didn't happen.
Pull your first structured citation list from Microsoft Copilot and store run one today. Start free — no credit card
Start for freeFrequently asked questions
- How do I get the sources Microsoft Copilot used for an answer?
- They're the numbered citation chips beside the reply, and you can copy them from the browser. To get them as data, POST your query to
https://api.agentgeo.org/v1/fetcheswith"surfaces": ["copilot"]and readanswers[].sources[]— every entry carries atitle, aurland aposition, so nothing has to be parsed out of prose or HTML. - Does Microsoft Copilot always cite its sources?
- Usually, but not always. Copilot grounds its answers in a live Bing search, so most delivered records carry a source list — denser than Gemini and broadly comparable to Perplexity. When Copilot answers from the model without searching, though, the record still delivers with an empty
sources[]. That's a legitimate outcome to log, not a failed fetch. - Can I pull Copilot citations programmatically?
- Yes. AgentGEO runs a managed-scraper engine and returns each answer's citation list as structured JSON, so a normal HTTP request to
https://api.agentgeo.org/v1/fetcheswith"surfaces": ["copilot"]gets you the sources with their order intact. There's no headless browser to run, no proxies to rotate, and no selectors that break when the frontend changes. - What does position mean in a Copilot citation list?
- It's the order Copilot cited the source in, and it behaves like a soft ranking: the earliest sources tend to do the most work in the answer that gets written. Log the position rather than a simple present/absent flag — that's what lets you see a domain slipping from 2 to 7 while still technically being cited.
- Which Copilot is this — GitHub Copilot or Microsoft 365 Copilot?
- Neither. This surface is consumer Microsoft Copilot, the web assistant at copilot.microsoft.com, not the GitHub coding assistant or the Microsoft 365 enterprise one. It honours
country(which shifts the Bing results it cites) but notlanguageorweb_search, and you fetch it by passing"surfaces": ["copilot"]to the endpoint.
Keep reading