AnswerLine Start free

ChatGPT

ChatGPT query fan-out

The web searches ChatGPT ran to build an answer, and the keywords behind its citations.

ChatGPT searches the web before it answers. `searchQueries` lists those searches, which explains why a page was cited and maps each prompt to the keywords behind it. The ChatGPT answers served today do not carry them, so the field comes back empty and costs nothing.

Returned in result

Field
searchQueries

Request

curl -X POST "https://api.answerline.dev/v1/monitor/chatgpt" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt":"What are the best running shoes for flat feet?","country":"US","include":{"markdown":true}}'

What ChatGPT’s query fan-out is

Query fan-out is the set of web searches ChatGPT runs to build an answer. The searches need not match the prompt as typed: one prompt can become several rewritten searches, and the pages it cites come from what those searches return. The API has a field for them, result.searchQueries, a list of strings. The ChatGPT answers served today do not carry their searches, so the list comes back empty.

What the answers do carry is the result of those searches: sources, the cited pages in order, and citationPills, which ties each sentence to the page it cites. They show which pages won, though not the queries they won on.

The query fan-out post introduces the idea. This page covers the field and its request options.

Field reference

Field Type Meaning
result.searchQueries string[] The searches ChatGPT ran to generate the response. Present when include.searchQueries is true; empty on the ChatGPT answers served today.
result.model string, nullable The ChatGPT model used. Can be empty.
result.inlineProducts[].product.generatedProductQuery string Only with include.shopping: the broad shopping query ChatGPT generated for one product. In the contract, but not filled on the answers served today; inlineProducts itself holds the shopping-search products.

Two request options relate to fan-out. Neither fills searchQueries on the ChatGPT answers served today:

Option Default Effect
legacy false Accepted for compatibility; it does not change the answer today.
disableWebSearch false Accepted; it changes nothing today. ChatGPT decides on its own whether to search.

Annotated excerpt

Values are illustrative; field names follow the POST /v1/monitor/chatgpt result schema with include.searchQueries set to true.

{
  "success": true,
  "result": {
    "text": "For a team under 20 people, look for a help desk with shared inboxes and...",
    // The field exists; it comes back empty on the ChatGPT answers served today.
    "searchQueries": [],
    "model": null,
    "sources": [
      { "position": 1, "url": "https://www.example-reviews.com/help-desk-small-teams", "label": "Help desks for small teams", "footnote": false }
    ]
  }
}

Cost and request options

include.searchQueries costs nothing extra: a request stays at the $0.0006 ChatGPT base (Starter tier). The ChatGPT answers served today do not carry the searches behind them, so searchQueries currently comes back empty; the flag is kept so requests that set it keep working. legacy and disableWebSearch do not change the price.

Code for an empty list: an empty searchQueries means the answer did not carry its searches, not that ChatGPT ran none.

Example: counting fan-out queries

The script shows how the field is read: it samples each prompt, then reports average searches per answer, the most frequent queries and terms, and which queries overlap your tracked keyword list. On the ChatGPT answers served today it reports 0 answers with fan-out, because searchQueries comes back empty.

import os
import re
import time
from collections import Counter
from datetime import date

import requests

API = "https://api.answerline.dev"
HEADERS = {"Authorization": f"Bearer {os.environ['API_KEY']}"}
PROMPTS = ["best help desk software for a small team", "cheapest help desk with a shared inbox"]
SAMPLES = 5
DAY = date.today().isoformat()
TRACKED = {"help desk software", "shared inbox software", "help desk pricing"}
STOP = {"the", "a", "for", "and", "of", "vs", "in", "to", "with", "best"}


def run_tasks(tasks):
    resp = requests.post(f"{API}/v1/async/task/batch", headers=HEADERS, json=tasks, timeout=60)
    resp.raise_for_status()
    pending = {item["task"]["id"] for item in resp.json()["results"] if item["success"]}
    results = []
    while pending:
        time.sleep(10)
        for task_id in list(pending):
            body = requests.get(f"{API}/v1/async/task/{task_id}", headers=HEADERS, timeout=30).json()
            status = body["task"]["status"]
            if status in ("COMPLETED", "FAILED"):
                pending.discard(task_id)
                if status == "COMPLETED":
                    results.append(body["response"]["result"])
    return results


def normalize(query):
    return " ".join(re.sub(r"\b20\d\d\b", "", query.lower()).split())


tasks = [
    {
        "taskType": "CHATGPT",
        "payload": {"prompt": prompt, "country": "US", "include": {"searchQueries": True}},
        "idempotencyKey": f"helpdesk-fanout-{p}-s{s}-{DAY}",
    }
    for p, prompt in enumerate(PROMPTS)
    for s in range(SAMPLES)
]
answers = run_tasks(tasks)
with_queries = [r for r in answers if r.get("searchQueries")]

queries = Counter()
terms = Counter()
for result in with_queries:
    seen = {normalize(q) for q in result["searchQueries"]}
    queries.update(seen)
    terms.update({t for q in seen for t in q.split() if t not in STOP})

searches = sum(len(r["searchQueries"]) for r in with_queries)
print(f"{len(with_queries)} of {len(answers)} answers returned fan-out")
print(f"average searches per answer: {searches / max(len(with_queries), 1):.1f}")
for query, n in queries.most_common(15):
    flag = "tracked" if any(k in query for k in TRACKED) else "new"
    print(f"{n:3} answers  [{flag:7}]  {query}")
print("top terms:", ", ".join(t for t, _ in terms.most_common(20)))

Each query is counted once per answer, so the count reads as “ran in N answers” and repeats inside one answer don’t inflate it. Years are stripped before counting so “best help desk 2026” and “best help desk” group together; keep them if the year is what you are studying.

Pitfalls

Every ChatGPT answer served today comes back with an empty searchQueries. Do not count an empty list as zero searches in a report; report the count of answers that returned queries next to any average.

generatedProductQuery describes one product’s shopping lookup, not the answer’s web searches. Report it separately from searchQueries.

Questions

How do I get ChatGPT's query fan-out from the API?

The field is result.searchQueries, an array of strings, requested with include.searchQueries. The ChatGPT answers served today do not carry their searches, so the array comes back empty and the API does not return ChatGPT's fan-out.

How much does include.searchQueries cost?

Nothing extra: a ChatGPT request stays at $0.0006 at the Starter tier. The ChatGPT answers served today do not carry their searches, so the field comes back empty, and a flag that returns nothing is not charged.

Why is searchQueries empty?

The ChatGPT answers served today do not carry the searches behind them, so searchQueries comes back empty on every answer. Setting legacy or disableWebSearch does not change that.

What is the difference between searchQueries and generatedProductQuery?

searchQueries lists the web searches behind the whole answer. generatedProductQuery is defined on an inline product for the broad shopping query ChatGPT generated for that one product. inlineProducts holds the shopping-search products on the answers served today, but generatedProductQuery is not filled on them.

What can I use for keyword work instead?

The fields ChatGPT answers do carry: sources lists the cited pages in order, citationPills shows which sentence cites which page, and text holds the brand and product names the answer uses. Check whether your pages rank for the topics those cited pages cover.

More ChatGPT data

All ChatGPT fields · API reference