Tutorial7 min read2026-10-05

A Copilot answer API: Microsoft Copilot answers + Bing sources

A Copilot answer API means one thing in practice: get Microsoft Copilot's answer text and the sources it cited back as structured data, from a single call, without driving a browser. AgentGEO's fetch endpoint does exactly that — POST to https://api.agentgeo.org/v1/fetches with "surfaces": ["copilot"] and the response carries answers[0].answerText plus answers[0].sources[], each source a title, a url and a position. Two things are specific to Copilot and shape how you use it. First, its citations reflect Bing, and the country parameter steers which Bing market answers — so a US pull and a UK pull are genuinely different results you keep in separate files. Second, Copilot scrapes run slower than most surfaces, so a failed record with a snapshot_id you can redeem for free is a normal part of the loop, not an outage. Below: the one call, the response shape, why country means one file per market, and the failure modes to code for.

Tutorial

Read this page with an AI

The short version: requests.post to https://api.agentgeo.org/v1/fetches with {"query": "...", "surfaces": ["copilot"]} and a bearer key, then read answers[0]["answerText"] and answers[0]["sources"] off the JSON. There's no SDK — the client is a plain HTTP call. What makes Copilot its own page is the country parameter and the failed-then-redeem loop; the rest of this article is those two things, in code.

The one call

query and surfaces are required; country is the optional lever that matters here — it steers Copilot's Bing market. raise_for_status() and the 200-second timeout come back in the failure-modes section, so they're in from the start.

Minimal — Copilot's answer and its Bing sources
import requests

resp = requests.post(
    "https://api.agentgeo.org/v1/fetches",
    json={
        "query": "best project management software for remote teams",
        "surfaces": ["copilot"],
        "country": "US",  # steers Copilot's Bing market — read by copilot
    },
    headers={"Authorization": "Bearer ag_live_your_key_here"},
    timeout=200,  # Copilot scrapes are slow; the API waits up to 180s
)
resp.raise_for_status()

answer = resp.json()["answers"][0]
print(answer["answerText"][:300], "...\n")
for src in answer["sources"]:
    print(f"{src['position']:>2}. {src['title']}  {src['url']}")

The response is one JSON object per run — the answer text and its Bing-backed citations, already parsed:

Response, trimmed
{
  "id": "run_5e28a1f4c9d0",
  "query": "best project management software for remote teams",
  "surfaces": ["copilot"],
  "status": "completed",
  "recordsDelivered": 1,
  "creditsCharged": 1,
  "answers": [
    {
      "surfaceKey": "copilot",
      "answerText": "For distributed teams, the tools most often recommended are ...",
      "sources": [
        { "title": "12 Best PM Tools for Remote Teams", "url": "https://example.com/remote-pm-tools", "position": 1 },
        { "title": "Remote Work Software Compared", "url": "https://another-example.com/blog/remote-software/", "position": 2 }
      ],
      "fetchedAt": "2026-10-05T09:22:41Z"
    }
  ]
}

creditsCharged counts delivered records — one here. position starts at 1 and ranks within this one answer only. The full field reference lives in the docs. For the task-level view — what Copilot citation data is for — see how to pull citations from Copilot; this page stays in the code.

Country steers Bing — one file per market

Copilot's sources come from Bing, and Bing's results are market-specific. The country parameter — a two-letter code like "US" or "GB" — steers which market Copilot answers from, so the same query run for two countries returns two genuinely different citation sets. That's not noise to average away; it's the signal. If you sell in three markets, you track three answer sets, and the cleanest way to keep them straight is one output file per market so a US result never lands in the UK column.

copilot_by_market.py — one file per Bing market
#!/usr/bin/env python3
"""Pull Copilot answers across Bing markets; keep one CSV per country."""
import csv
from collections import Counter
from pathlib import Path
from urllib.parse import urlparse, urlunparse

import requests

API = "https://api.agentgeo.org/v1/fetches"
KEY = "ag_live_your_key_here"
FIELDS = ["query", "country", "position", "host", "url", "title", "fetchedAt"]

MARKETS = ["US", "GB", "AU"]
QUERIES = [
    "best project management software for remote teams",
    "asana alternatives",
    "team collaboration tools for startups",
]


def normalize(url):
    p = urlparse(url)
    host = p.netloc.lower().removeprefix("www.")  # Python 3.9+
    clean = urlunparse((p.scheme, host, p.path.rstrip("/"), "", "", ""))
    return host, clean


def fetch(query, country):
    resp = requests.post(
        API,
        json={"query": query, "surfaces": ["copilot"], "country": country},
        headers={"Authorization": f"Bearer {KEY}"},
        timeout=200,  # Copilot is slow; sit above the 180s budget
    )
    resp.raise_for_status()
    return resp.json()


for country in MARKETS:
    out = Path(f"copilot_{country.lower()}.csv")
    new_file = not out.exists()
    hosts = Counter()
    rows = []

    for query in QUERIES:
        run = fetch(query, country)
        for answer in run["answers"]:
            if answer.get("status") == "failed":
                # Copilot scrapes are slow — a failed record is routine.
                # A budget-exceed failure carries providerFields.snapshot_id;
                # redeem it free (see the failure-modes section).
                print(f"[{country}] failed, skipping: {query}")
                continue
            for src in sorted(answer.get("sources") or [], key=lambda s: s["position"]):
                host, clean = normalize(src["url"])
                hosts[host] += 1
                rows.append({
                    "query": query,
                    "country": country,
                    "position": src["position"],
                    "host": host,
                    "url": clean,
                    "title": src["title"],
                    "fetchedAt": answer.get("fetchedAt", ""),
                })

    with out.open("a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        if new_file:
            writer.writeheader()
        writer.writerows(rows)

    print(f"[{country}] appended {len(rows)} rows -> {out}")
    for host, n in hosts.most_common(5):
        print(f"   {n:>2}x  {host}")

The country column is written into every row and the filename carries the market too — belt and suspenders, because the day you concatenate all three files for a global view, the column is the only thing that keeps a UK citation from masquerading as a US one. Every writer opens in append mode: one run is a snapshot, two runs a month apart are a report.

The script runs as-is once KEY is real — dry-run it first with a zero-credit ag_test_ key. Not ready for code? Get a free AI-visibility audit → — no card, no account.

Get my free audit

How the copilot surface behaves

All six engines share one request and response contract; what differs is behaviour. On copilot:

  • Sources reflect Bing. Copilot is built on Bing, so the citations you get back are Bing's picks for the query, not Google's and not Perplexity's. If a page ranks on Bing for your topic it has a shot at the Copilot answer; if it doesn't, no amount of Google authority moves it here. Track Copilot against Bing's reality, not your Google rankings.
  • country is the lever that matters. It steers the Bing market Copilot answers from. web_search is honoured only by chatgpt and does nothing here; language is honoured only by google_ai_overview. country is read by copilot and google_ai_overview — on Copilot it's the parameter worth setting, and worth keeping straight per market.
  • Copilot scrapes run slow. It's one of the heavier surfaces, so a call reaching toward the 180-second budget is normal, and a status: "failed" record now and then is part of the loop rather than a sign something's down. Build the redeem path in from the start (next section) so a slow scrape costs you nothing.
  • position ranks within one answer only. First of three sources is not first of ten. Keep the query and the per-answer source count beside the position whenever you plan to trend it.

Failure modes worth coding for

  • A failed record is not "zero citations", and on Copilot it's common. A record can carry status: "failed" while the HTTP call returns 200 — raise_for_status() won't catch it, which is why the script checks and skips. Because Copilot is slow, the usual cause is a scrape exceeding the 180-second budget; that record costs zero credits and carries providerFields.snapshot_id.
  • Redeem the snapshot instead of re-paying. When a Copilot record fails with a providerFields.snapshot_id, POST again with a top-level "snapshot_id" and the same single surface — ["copilot"] — and the finished answer is redeemed without running the scrape again. Re-submitting the original query instead pays for a fresh scrape you didn't need.
  • A client timeout below 180 seconds cancels healthy Copilot requests. The API holds the connection up to 180 seconds while the slow scrape finishes, and Copilot leans on that budget more than most surfaces. timeout=30 aborts answers seconds from arriving; timeout=200 sits above the server's budget with margin.
  • Don't mix markets in one file. A US and a UK Copilot pull are different results, not runs of the same thing. Write country into every row (and ideally the filename) so a concatenated view can still tell them apart — otherwise the host counts silently blend three markets into a number that describes none of them.
  • data= is not json=. requests.post(url, data={...}) form-encodes the body and the API rejects it. Use the json= kwarg — it serializes the dict and sets Content-Type: application/json in one move.

Summary

A Copilot answer API is one POST with "surfaces": ["copilot"] — the response hands you answerText and Bing-backed sources[], no browser required. Two things make Copilot its own case: country steers the Bing market, so you keep one file per market, and Copilot's slow scrapes mean a failed record with a redeemable snapshot_id is a routine part of the loop, not an outage. Build the redeem path in and a slow scrape costs you nothing. The same request shape covers the other five engines — the Python API page shows the multi-surface version.

FAQ

Direct answers to the questions this page raises.

POST to https://api.agentgeo.org/v1/fetches with {"query": "...", "surfaces": ["copilot"]} and an Authorization: Bearer ag_live_... header, then read answers[0]["answerText"] and answers[0]["sources"] — each source carries title, url and position. There's no SDK; the client is a plain HTTP call. Set timeout=200 because Copilot scrapes run slow.

It steers which Bing market Copilot answers from. Copilot's citations reflect Bing, and Bing's results are market-specific, so a query run with "country": "US" and the same query with "country": "GB" return genuinely different citation sets. Track each market separately — one output file per country — so a US result never gets counted as a UK one.

Copilot is one of the slower surfaces, so the common cause is a scrape exceeding the API's 180-second budget. That record carries status: "failed" at zero credits and a providerFields.snapshot_id. POST again with a top-level "snapshot_id" and the same single surface to redeem the finished answer for free, instead of re-running the scrape and paying again.

Bing. Copilot is built on Bing, so its cited sources are Bing's picks for the query, steered by the country market. That means Copilot visibility tracks Bing's reality rather than your Google rankings — a page strong on Google but weak on Bing can be absent from the Copilot answer entirely.

200 seconds. The API holds the request open for up to 180 seconds on slow surfaces, and Copilot leans on that budget more than most. A client timeout below 180 aborts fetches that were about to complete, so timeout=200 keeps the client above the server's budget with margin.

Keep reading

Where this page leads next.

Run these checks on your own brand

Two ways in. Send a URL and a person runs the fetches for you, free — or connect your agent over MCP, on a plan, and run them yourself. Either way you get the raw answers and their citations, never a score.