Tutorial7 min read2026-09-30

Get the raw ChatGPT answer text via API

To get the raw ChatGPT answer text via API, POST your query to https://api.agentgeo.org/v1/fetches with "surfaces": ["chatgpt"] and read answers[0].answerText — the complete answer string, verbatim, no browser and no HTML to strip. Citations get most of the attention, but the answerText field is the one you want when the job is storing the full response, diffing it run over run, or running NLP on it: extracting brand mentions, measuring sentiment, tracking how the model describes you. This page focuses on that string — how to fetch it, why it's present even when sources[] is empty, and how to store it so next month's answer has something to compare against.

Tutorial

Read this page with an AI

The short version: answerText is a top-level string on each entry in the answers[] array, and it's the full answer the model produced — not a summary, not a citation list, the prose itself. requests.post to the fetch endpoint, raise_for_status(), then read answers[0]["answerText"]. Everything after that is what you do with a paragraph of text you now own: persist it, diff it, or feed it to an NLP pass. Crucially, answerText is present even when sources[] is empty — the model can answer without browsing, and the text is still the whole point.

Fetching the raw answer text

query and surfaces are the only fields required. web_search is optional and honoured only by chatgpt — send it if you want to nudge the fetch toward browsing, but the answer text arrives either way. Note the timeout: the API holds slow scrapes up to 180 seconds, so stay above it.

Get the full answer string for one query
import requests

resp = requests.post(
    "https://api.agentgeo.org/v1/fetches",
    json={
        "query": "is acme analytics good for enterprise",
        "surfaces": ["chatgpt"],
        "web_search": True,  # chatgpt is the only surface that reads this
    },
    headers={"Authorization": "Bearer ag_live_your_key_here"},
    timeout=200,  # the API holds slow scrapes up to 180s - stay above that
)
resp.raise_for_status()

answer = resp.json()["answers"][0]
if answer.get("status") == "failed":
    raise SystemExit("record failed - skip or redeem via snapshot_id")

text = answer["answerText"]
print(f"{len(text)} chars, {len(answer.get('sources') or [])} sources\n")
print(text)

The response is one JSON object. answerText is the string you came for; sources[] sits beside it, and here it happens to be populated because the model browsed:

Response, trimmed — answerText is the payload
{
  "id": "run_9f2b41c7e0da",
  "query": "is acme analytics good for enterprise",
  "surfaces": ["chatgpt"],
  "status": "completed",
  "recordsDelivered": 1,
  "creditsCharged": 1,
  "answers": [
    {
      "surfaceKey": "chatgpt",
      "answerText": "Acme Analytics is generally well regarded for mid-market use, with strong dashboards and a fast setup. For enterprise, reviewers note gaps in role-based access and single sign-on compared with larger platforms ...",
      "sources": [
        { "title": "Acme Analytics Review 2026", "url": "https://example.com/reviews/acme-analytics", "position": 1 },
        { "title": "Enterprise Analytics Compared", "url": "https://example.org/enterprise-analytics", "position": 2 }
      ],
      "fetchedAt": "2026-09-30T07:22:14Z"
    }
  ]
}

That prose is the raw material for everything downstream. It names your product, describes it in a tone, and places it against competitors — none of which shows up in the citation list. If your question is 'how does ChatGPT talk about us,' the answer lives in this string, not in sources[].

answerText survives an empty sources[]

The single most useful property of answerText for this job: it's present whether or not the model browsed. ChatGPT only carries citations when it actually searched the web, so sources[] is often empty — but the answer text is always there, because the model always produces an answer. A citation-only scraper sees 'nothing.' The answer-text approach sees the whole response.

Same shape, no browse — full text, empty sources
{
  "id": "run_3c8a12f6b4de",
  "query": "what is a good crm for a two-person agency",
  "surfaces": ["chatgpt"],
  "status": "completed",
  "recordsDelivered": 1,
  "creditsCharged": 1,
  "answers": [
    {
      "surfaceKey": "chatgpt",
      "answerText": "For a two-person agency, lightweight CRMs tend to fit best. Common picks include a simple pipeline tool you can set up in an afternoon rather than an enterprise suite ...",
      "sources": [],
      "fetchedAt": "2026-09-30T07:24:51Z"
    }
  ]
}

An empty sources[] here is not a failure and not a retry — it's a completed, billed, correct answer produced from the model's own weights. If you're mining the text for brand mentions or sentiment, this record is just as valuable as the browsed one; arguably more so, because it tells you how the model describes you unprompted, without a fresh page steering it.

Want to know how ChatGPT describes your brand right now — without writing any of this? Get a free AI-visibility audit → — no card, no account.

Get my free audit

Store it, diff it, mine it

The answer text is only worth fetching if you keep it. One pull tells you today's phrasing; two pulls a month apart tell you whether the model's story about you changed. This script fetches a query set, stores each full answerText with its fetchedAt timestamp, and runs a first-pass NLP scan for brand mentions and a crude sentiment cue — enough to show the shape without pretending it's a model.

answer_text_store.py — persist and scan
"""Fetch raw ChatGPT answer text for a query set; store it and scan for mentions."""
import json
import re
from datetime import date
from pathlib import Path

import requests

API = "https://api.agentgeo.org/v1/fetches"
KEY = "ag_live_your_key_here"
BRANDS = ["Acme Analytics", "Acme"]
POSITIVE = {"best", "strong", "great", "recommended", "reliable", "leading"}
NEGATIVE = {"lacks", "gaps", "weak", "limited", "missing", "expensive"}

QUERIES = [
    "is acme analytics good for enterprise",
    "acme analytics vs competitors",
    "best analytics platform for mid-market",
    "analytics tools with single sign-on",
]


def fetch(query):
    resp = requests.post(
        API,
        json={"query": query, "surfaces": ["chatgpt"], "web_search": True},
        headers={"Authorization": f"Bearer {KEY}"},
        timeout=200,  # the API holds slow scrapes up to 180s
    )
    resp.raise_for_status()
    return resp.json()


def scan(text):
    lower = text.lower()
    mentions = sum(len(re.findall(re.escape(b.lower()), lower)) for b in BRANDS)
    pos = sum(w in lower for w in POSITIVE)
    neg = sum(w in lower for w in NEGATIVE)
    cue = "positive" if pos > neg else "negative" if neg > pos else "neutral"
    return mentions, cue


out = Path(f"chatgpt-answers-{date.today():%Y-%m-%d}.jsonl")
with out.open("w", encoding="utf-8") as f:
    for query in QUERIES:
        run = fetch(query)
        for answer in run["answers"]:
            if answer.get("status") == "failed":
                print(f"failed, skipping: {query}")
                continue
            text = answer["answerText"]  # present even when sources[] is empty
            mentions, cue = scan(text)
            record = {
                "query": query,
                "answerText": text,
                "browsed": bool(answer.get("sources")),
                "brand_mentions": mentions,
                "sentiment_cue": cue,
                "fetchedAt": answer.get("fetchedAt"),
            }
            f.write(json.dumps(record, ensure_ascii=False) + "\n")
            print(f"{mentions} mentions  {cue:>8}  browsed={record['browsed']}  {query}")

print(f"\nwrote {out} - full text stored, ready to diff next month")

Two deliberate choices. The store is JSONL with the full answerText intact, because you diff prose against prose — truncate it and next month's comparison is meaningless. And browsed records whether sources[] was populated, so you can separate 'how the model talks about us when a page steered it' from 'how it talks about us from memory' — two different signals that a citation-only pull collapses into one.

The POSITIVE/NEGATIVE word sets are a placeholder, not a sentiment model — they show where a real NLP pass slots in. Swap in whatever you already run: a transformer classifier, an entity extractor, an LLM scoring the passage. The point of this page is getting the clean answerText string in the first place; what you run on it is your call.

Surface behaviour and failure modes

  • answerText is always present; sources[] is conditional. The model always answers, so the text is always there. Citations appear only when it browsed. Build on the text as the primary field and treat sources[] as an optional enrichment, not a required one.
  • A failed record is a 200, not an exception. A record can carry status: "failed" while HTTP returns 200 — raise_for_status() won't catch it, so check answer["status"] before reading answerText. If the failure was a scrape past the 180-second budget, the record costs zero credits and carries providerFields.snapshot_id; POST again with a top-level "snapshot_id" and the same single surface to redeem the finished answer.
  • A client timeout under 180 seconds cancels healthy work. The API holds the connection up to 180 seconds while a slow scrape finishes; timeout=30 aborts answers that were about to arrive, and no timeout hangs forever. timeout=200 is the number.
  • The answer drifts between runs. ChatGPT re-derives its answer each time, so the same query can return different phrasing an hour later even when nothing you control changed. One pull is a sample. Store with fetchedAt and read trends across several runs before calling a change in tone real.
  • web_search changes citations, not the text. Sending web_search: true raises the odds the fetch browses and thus populates sources[] — it doesn't change whether answerText exists. If all you want is the text, you can omit it; if you also want citations, send it.

Summary

The raw ChatGPT answer text is a top-level answerText string on each answer, fetched with one POST and read straight off the JSON — no browser, no HTML stripping. It's present whether or not the model browsed, which makes it the right field to store, diff and run NLP on when the question is how ChatGPT describes you rather than who it links to. Keep the full string with its fetchedAt, and the second pull becomes a report on how your story moved. The same request shape covers the other five engines and the Python API page shows the multi-surface version; if all you need is the citation list, get ChatGPT citations in Python is the narrower path.

FAQ

Direct answers to the questions this page raises.

POST to https://api.agentgeo.org/v1/fetches with {"query": "...", "surfaces": ["chatgpt"]} and a Bearer ag_live_ key, then read answers[0]["answerText"] — it's the complete answer string the model produced, verbatim, with no HTML to strip and no browser to run. Set timeout=200 and check answer["status"] before reading. The whole client is one requests.post; no SDK exists or is needed.

Yes — always. answerText is the answer the model produced, and the model always answers, so the text is present whether or not it searched the web. What's conditional is sources[]: it's populated only when the model browsed. An empty sources[] with a full answerText is a completed, correct, billed record — often the most interesting one, because it shows how the model describes you from memory.

Yes — that's a common reason to fetch answerText rather than just citations. The field is clean prose, so you can pipe it into any NLP pass: entity extraction for brand mentions, a classifier for sentiment, or an LLM scoring the passage. Store the full string with its fetchedAt timestamp so you can diff phrasing and tone across runs; the answer drifts, so read trends, not single pulls.

Because ChatGPT re-derives its answer on every ask — two runs an hour apart can return different phrasing with nothing you control having changed. One pull is a sample, not a baseline. Store each answerText with its fetchedAt timestamp and read trends across several runs before treating a change in wording or tone as real signal.

No. web_search is honoured only by the chatgpt surface and it affects whether the fetch browses — which populates sources[]. It doesn't change whether answerText exists; the text is always returned. Send web_search: true if you also want citations; omit it if you only care about the raw answer string. Either way you get the full text.

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.