Java AI Visibility API
Query the raw answers ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Copilot and Gemini actually give — from Java, with the java.net.http.HttpClient in the JDK and a few records you own. POST a query to https://api.agentgeo.org/v1/fetches with your ag_live_ key and the answer text, citations and sources come back as JSON that Jackson maps in one call. No Selenium Grid, no WebDriver version matrix, no headless Chrome in your containers. AgentGEO is the answer-access layer for AI visibility. Your own code (or your users' AI agent over MCP) runs the GEO/AEO analysis — the API just hands you the raw data, structured and identical across all six engines.
Query the raw answers ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Copilot and Gemini actually return — from Java, with the HttpClient that ships in the JDK. No Selenium, no headless Chrome to keep alive: one POST, and the answer text, citations and sources deserialize into records your services can fan out over with CompletableFuture.
Get your free API key → · Read the quickstart → — Free tier, no credit card required.
Start for freeCall the AI Visibility API from Java
POST to https://api.agentgeo.org/v1/fetches with your ag_live_ key as a bearer token and the engines you want in the surfaces array. HttpClient (JDK 11+) makes the call; Jackson maps the JSON into three records you define once and own. (Want to see the payload before writing code? The raw answer playground runs a query in the browser, no signup.)
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;
public class AgentGeo {
// Records need JDK 16+; on 11–15 use plain classes with the same fields.
@JsonIgnoreProperties(ignoreUnknown = true)
record Source(String title, String url, int position) {}
@JsonIgnoreProperties(ignoreUnknown = true)
record Answer(String surfaceKey, String answerText, List<Source> sources) {}
@JsonIgnoreProperties(ignoreUnknown = true)
record FetchRun(String id, String query, String status,
int recordsDelivered, int creditsCharged, List<Answer> answers) {}
static final HttpClient CLIENT = HttpClient.newHttpClient();
static final ObjectMapper MAPPER = new ObjectMapper();
static FetchRun fetchRun(String query, String... surfaces) throws Exception {
String body = MAPPER.writeValueAsString(Map.of("query", query, "surfaces", surfaces));
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.agentgeo.org/v1/fetches"))
.header("Authorization", "Bearer ag_live_your_key_here")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> resp = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 200) {
throw new RuntimeException("agentgeo: unexpected status " + resp.statusCode());
}
return MAPPER.readValue(resp.body(), FetchRun.class);
}
public static void main(String[] args) throws Exception {
// surfaces: chatgpt | perplexity | gemini | google_ai_overview | google_ai_mode | copilot
FetchRun run = fetchRun("best running shoes for flat feet", "chatgpt");
for (Answer answer : run.answers()) {
System.out.println(answer.surfaceKey() + " → " + answer.answerText());
for (Source src : answer.sources()) { // cited sources, in order
System.out.println(" " + src.position() + " " + src.title() + " " + src.url());
}
}
}
}The response is a plain JSON object — readValue maps it straight into FetchRun, no HTML to scrub:
{
"id": "run_84c1f2ab91d3",
"query": "best running shoes",
"surfaces": ["chatgpt"],
"status": "completed",
"recordsDelivered": 1,
"creditsCharged": 1,
"answers": [
{
"surfaceKey": "chatgpt",
"answerText": "For most runners, top picks include ...",
"sources": [
{ "title": "Best Running Shoes", "url": "https://example.com/guide", "position": 1 }
]
}
]
}The response shape is identical across all six engines, so the same three records cover chatgpt, perplexity, gemini, google_ai_overview, google_ai_mode and copilot. Fan one query across all six and diff the citation sets — that diff is the core of any GEO/AEO analysis you'd want to build.
Fanning a query across every engine is a CompletableFuture per surface — run them in parallel, then print which URLs each engine cited:
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
// Reuses fetchRun and the records above.
String[] surfaces = {"chatgpt", "perplexity", "gemini", "google_ai_overview", "google_ai_mode", "copilot"};
List<CompletableFuture<Void>> futures = Arrays.stream(surfaces)
.map(surface -> CompletableFuture.runAsync(() -> {
try {
FetchRun run = fetchRun("best running shoes for flat feet", surface);
for (Answer answer : run.answers()) {
System.out.println(answer.surfaceKey() + " cites:");
answer.sources().forEach(src -> System.out.println(" " + src.url()));
}
} catch (Exception e) {
System.err.println(surface + ": " + e.getMessage());
}
}))
.toList();
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join();What the Java API gives you
AgentGEO is a thin answer-access layer, not another closed dashboard. The API delivers the raw material; your Java service, batch job or Spring app (or your users' AI agent) does the analysis.
- Six engines, one contract — ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Copilot and Gemini behind one request/response shape, so covering another engine is one more string in the
surfacesarray. - Citations and sources, structured — every answer maps into a
List<Source>with title, URL and position, ready to store, rank or diff. No Jsoup, no regexing links out of markup. - Managed-scraper engine, maintained for you — answers come through supported access paths, so there is no WebDriver session to supervise, no proxy list to rotate and no IP ban to recover from mid-run.
- Provider metadata — every response carries
surfaceKeyandproviderFields, so the dataset you build stays auditable as the engines underneath evolve. - MCP connection, too — the same data is exposed over MCP through the
agentgeo-mcppackage, so Claude Code, Cursor, Codex or any MCP client can pull answers and run the GEO analysis itself, no glue code. - Usage-based billing with a spend cap — start on the free tier with no credit card, then subscription plus overage, priced by usage and never per seat.
Common problems with DIY answer-scraping in Java
The enterprise-Java instinct is Selenium: stand up a Grid, drive ChatGPT or Perplexity in a headless browser, and scrape the DOM. It works in a proof of concept and then becomes infrastructure you own forever.
- Selenium Grid upkeep. A Grid of browser nodes is a distributed system in its own right — hubs, nodes, health checks and scaling — standing between you and what should be one HTTP call.
- The WebDriver/browser version matrix. Chrome, ChromeDriver and the Selenium client must all line up; every browser auto-update is a chance for the matrix to break your pipeline without a single code change on your side.
- Headless Chrome memory in JVM containers. A browser per query alongside the JVM's own heap pushes container limits fast; you end up sizing pods around Chrome and watching for OOM kills.
- Flaky waits against streaming UIs.
WebDriverWaitandExpectedConditionsagainst an answer that streams in token by token are inherently racy — you tune timeouts forever and still get partial captures. - Proxy rotation and CI images that bundle browsers. Staying unblocked needs residential proxies, and your CI images must now ship a pinned Chrome plus fonts and libraries — heavyweight builds for a marketing metric.
| DIY scraping in Java | AgentGEO API | |
|---|---|---|
| Setup | Selenium Grid, ChromeDriver, proxies | JDK HttpClient + a key — no extra infra |
| Parsing | Jsoup/DOM queries against hashed classes | Stable JSON, mapped straight to records |
| Citations | Hand-rolled extraction per engine | Structured answers[].sources[] |
| Containers & CI | Images bundling Chrome, OOM tuning | A plain HTTPS call — lean images |
| Adding an engine | A new scraper and Grid capacity | One more key in the surfaces array |
AgentGEO is the answer-access layer; the ranking, alerting or dashboards you build in Java on top of it are yours. You own the raw data and ship it inside your own product instead of renting a login to someone else's.
Prefer MCP? Same data, no glue code
Everything the REST endpoint returns is also exposed over MCP. When the consumer is an AI agent rather than a Java service — Claude Code, Cursor, Codex or any MCP client — one command wires it up, and the agent pulls answers and runs the GEO analysis itself:
claude mcp add agentgeo -- npx -y agentgeo-mcp --key ag_live_your_key_hereWho builds on this
Mostly backend teams embedding AI-visibility tracking in an existing JVM product — a Spring @Scheduled job that fans a keyword set across six engines and writes citations to Postgres is an afternoon of Java — plus agencies white-labeling GEO/AEO reporting for clients. If you compared closed dashboards like Profound and wanted the raw data underneath, or you were sketching your own scraper, this is the layer to build on.
Ready to pull your first answer? Start free → · Follow the 5-minute quickstart →
Start for freeFrequently asked questions
- Do I need Selenium or a headless browser to track AI answers in Java?
- No. AgentGEO's managed scrapers run server-side, so the whole integration is a
java.net.http.HttpClientPOST tohttps://api.agentgeo.org/v1/fetcheswith aBearer ag_live_key. There is no Grid to run, no ChromeDriver to match to a browser version and no proxy pool to rotate — the browser-automation failure modes never enter your codebase. - Which JDK do I need?
- JDK 11+ for
java.net.http.HttpClient. The example uses records for the response types, which need JDK 16+ — on 11–15, use plain classes (or Lombok@Value) with the same fields and Jackson maps them the same way. Any JSON library works; the sample uses Jackson'sObjectMapper. - How do I use it from Spring?
- Drop the call into a
@Serviceand trigger it from a@Scheduledmethod or a Spring Batch job. You can swapHttpClientforRestClient/WebClientif that's your stack — the request shape and JSON response are identical, so the records and parsing stay the same. - What's the right way to fan out across engines?
- Either request all six in one call —
"surfaces": ["chatgpt", "perplexity", "gemini", "google_ai_overview", "google_ai_mode", "copilot"]— and get one answer per engine, or run aCompletableFutureper surface as shown and join them. For large keyword sets, bound parallelism with a fixed thread pool. Billing is usage-based with a spend cap, so concurrency can't cause a surprise bill. - Which AI engines does the Java API cover?
- Six: ChatGPT, Perplexity, Google AI Overviews, Google AI Mode, Copilot and Gemini. The request and response shape is identical across all of them, so adding an engine to your tracker is one more string in the
surfacesarray — no new scraper, no new parser.
Keep reading