GEOly Agent API
Send one question over HTTP and GEOly's hosted GEO agent researches your AI visibility data and returns a sourced answer, streamed or as JSON. Authenticated with your GEOly OAuth token; usage draws on your organization's own credits.
The GEOly Agent API is the HTTP endpoint behind geoly run. You send one question; GEOly's hosted GEO agent chooses the data tools, reads your monitoring data (and public industry data when your plan includes it), and returns an answer with a receipt: which tools it used, how many steps it took, and what it cost. You do not write prompts or learn GEOly's data model — you get the answer.
The GEOly CLI is the official client of this API: sign in with it, and geoly run sends the request, follows the stream, retries safely, and saves the receipt. This page documents the HTTP interface underneath — endpoints, request and response shapes, streaming, idempotency, and errors — so you know exactly what a run returns.
How it relates to the CLI and MCP
| What runs the reasoning | Best for | |
|---|---|---|
Agent API (POST /api/agent/runs) | GEOly's hosted GEO agent | Getting finished answers into your own systems over HTTP |
GEOly CLI — geoly run | The same hosted agent, through this API | Terminals, scripts, and coding agents |
GEOly CLI — geoly call, and GEOly MCP | Your own model or agent, calling GEOly data tools one by one | When you want the raw data and your own reasoning |
All three use the same GEOly OAuth authorization, the same organizations and brands, and the same permissions.
Quick start
The GEOly CLI is the official client of this API: geoly run signs you in, sends the request below, follows the stream, retries safely, and saves the receipt.
# 1. Install the CLI and sign in once
curl -fsSL https://geoly.ai/install.sh | sh
geoly auth login
# 2. Ask a question — this is a POST /api/agent/runs under the hood
geoly run "How did our visibility on ChatGPT change over the last 7 days?"
You will see the agent's steps and tools on stderr while it works, and one JSON receipt on stdout when it finishes. The rest of this page documents the HTTP interface itself: what geoly run sends and receives.
Authentication
Sign in with the GEOly CLI:
geoly auth login # opens the browser
geoly auth login --remote # no browser on this machine: prints a URL to open anywhere…
geoly auth login --code <code> # …then paste back the code the page shows
The consent screen asks you to choose the organization and approve read (and, if you need it, write) permissions — the same screen as GEOly MCP. Sign-in produces an OAuth access token for your GEOly account, valid for about 14 days; geoly auth status shows when it expires, and signing in again renews it. See the GEOly CLI guide for the full sign-in flow, including SSH, containers, and CI runners.
On the wire, every request carries that token:
Authorization: Bearer <access_token>
Browser session cookies are not accepted. The token represents your GEOly user and the consent you gave: the API can reach exactly the organizations and permissions that consent covers — nothing more. A missing, invalid, or expired token returns 401.
Endpoints
Base URL: https://app.geoly.ai
| Method and path | Purpose |
|---|---|
POST /api/agent/runs | Start a run: one question in, one answer out (streamed or JSON) |
GET /api/agent/runs | Recent runs for the organization (receipt fields, newest first) |
GET /api/agent/runs/{run_id} | One run, including the question and the full answer |
Start a run
POST /api/agent/runs
Authorization: Bearer <access_token>
Content-Type: application/json
Accept: text/event-stream
Idempotency-Key: weekly-report-2026-09-22
{
"question": "Compare our AI visibility with our top three competitors over the last 30 days.",
"org_id": "<org_id>",
"brand_id": "<brand_id>",
"context": "We sell standing desks in the US. Keep the answer under 300 words.",
"max_credits": 300
}
| Field | Required | Limits | Meaning |
|---|---|---|---|
question | Yes | Up to 4,000 characters | The question or task |
org_id | When your access spans several organizations | — | Whose data to read and whose credits to use |
brand_id | When the organization has several brands | — | Which brand the run is about |
context | No | Up to 16,000 characters | Background, requirements, or output format for the agent |
max_credits | No | Integer, 25–2000 | Hard ceiling for this run. Without it, the run can use up to what the organization has left, at most 2,000 |
spec | No | One of the names below | Ask for a fixed-format deliverable checked by the server |
allow_writes | No | true only | Let the agent use write tools your consent granted. Anything other than the literal true means read-only |
When org_id or brand_id is needed and missing — or wrong — the 400 error lists the valid options by name, for example This organization has multiple brands. Pass brand_id explicitly … or brand_id "…" was not found in this organization. Did you mean …? Available: ….
Streamed (Server-Sent Events) — recommended
Send Accept: text/event-stream. The response is a stream of frames in this format:
event: started
data: {"run_id":"run_…","brand":{"id":"…","name":"…"},"model":"…"}
event: tool
data: {"name":"get_brand_overview","ok":true,"ms":1830,"args":{"time_range":"30d"},"output_chars":5120}
event: done
data: {"run_id":"run_…","answer":"…","stopped":"done", …}
| Event | Data | Meaning |
|---|---|---|
started | run_id, brand, model | The run began. Keep the run_id. |
step | index | The agent started step N. |
tool | name, ok, ms, args, output_chars | A tool finished: name, success, duration, argument preview, output size. |
tools_loaded | query, names | The agent searched for and loaded additional tools. |
text | delta | The next piece of the answer text. |
step_error | message | A step hit an upstream problem; the agent continues with what it has. |
heartbeat | {} | Sent every 10 seconds of silence. Not an error — set your client's idle timeout to 30 seconds or more. |
done | The receipt | Finished. |
error | code, message | The run failed. It is not charged. |
In streamed mode, a failed run arrives as an error event; the HTTP status is still 200.
A run keeps going if your client disconnects, and its result is stored. Reconnecting is not possible, but you can always read the result with GET /api/agent/runs/{run_id}.
JSON (synchronous)
Omit Accept: text/event-stream and the response is a single JSON receipt once the run finishes. This suits short questions only: in this mode the agent stops starting new steps after about 30 seconds, and a response that takes longer than about 100 seconds can be cut off by the network edge with a gateway error. The run still completes and is stored; find it with GET /api/agent/runs and read it by id. A failed run returns 502 with {"error":"RUN_FAILED","message":"…"}.
The receipt
The done event (streamed) or the response body (JSON):
{
"run_id": "run_…",
"answer": "…Markdown…",
"stopped": "done",
"stopped_reason": "done",
"steps": 3,
"tools_used": ["get_brand_overview", "get_brand_board"],
"usage": { "input_tokens": 46253, "output_tokens": 1489 },
"credits_cost": 54,
"credits_remaining": 9946
}
| Field | How to read it |
|---|---|
answer | The answer, in Markdown. It follows the language of the question. |
stopped | done when the agent finished on its own terms, otherwise max_steps. |
stopped_reason | done, or why it stopped early: budget (reached max_credits or the organization's remaining credits), deadline (the run's time limit), max_steps (step limit), salvaged (an upstream failure; the agent answered with the data it already had). Anything but done means the answer may be partial. |
tools_used | The tools the agent called — where the numbers came from. |
credits_cost | What this run charged. Never more than max_credits. |
credits_remaining | Credits left for the organization afterward (a number, "unlimited", or null if it could not be read). |
deliverable | Only with spec: { spec, version, valid, problems, … }. valid: false lists what is missing; the answer is returned either way. |
Read runs back
GET /api/agent/runs?org_id=<org_id>&limit=20
GET /api/agent/runs/{run_id}
GET /api/agent/runs returns { "runs": [ … ] }, newest first. Each row has run_id, status, stopped, steps, credits_cost, created_at, finished_at, question (first 200 characters), and brand_id. limit is 1–50 (default 20); pass org_id when your access spans several organizations.
GET /api/agent/runs/{run_id} returns the stored run:
{
"run_id": "run_…",
"status": "succeeded",
"question": "…",
"answer": "…",
"stopped": "done",
"steps": 3,
"tools_used": ["…"],
"usage": { "input_tokens": 46253, "output_tokens": 1489 },
"credits_cost": 54,
"max_credits": 2000,
"error": null,
"created_at": "2026-09-22T08:00:00.000Z",
"finished_at": "2026-09-22T08:01:12.000Z"
}
statusisrunning,succeeded, orfailedhere (the CLI showssucceededasdone).- To wait for a run, poll this endpoint every few seconds until
statusis no longerrunning. - A run still marked
running15 minutes after it started is reported asfailedwitherror: "RUN_ABANDONED". - A run your token cannot see returns
404 RUN_NOT_FOUND, whether or not it exists.
Idempotency: safe retries
Send an Idempotency-Key header to make retries safe:
- Format: 8–128 characters of
A-Z a-z 0-9 _ - : .. A header that is present but malformed (including empty) is rejected with400 IDEMPOTENCY_KEY_INVALID. - Scope: the same organization, brand, and key.
- While the original run is still running, a request with the same key always returns that run.
- After it finishes, the same key replays it for 10 minutes from when it started; after that, the same key starts a new run.
- A replay does not start or charge a new run. It is returned as JSON, whatever you sent in
Accept, with"replayed": truein the body and the response headerIdempotent-Replayed: true. The body has the same shape asGET /api/agent/runs/{run_id}; if itsstatusisrunning, poll that endpoint. - If the key cannot be checked for a moment, the request is refused with
503 IDEMPOTENCY_UNAVAILABLErather than risking a double run. Retry with the same key.
Choose a key that identifies the job, such as weekly-report-<brand>-<date>. The CLI derives its key from the organization, brand, spec, question, context, max_credits, and write permission.
Credits
Agent API usage draws on your organization's own credits.
- Cap a run with
max_credits(25–2000).credits_costin the receipt never exceeds it. - See what is left in every receipt (
credits_remaining), withgeoly credits, or in GEOly under Settings → Billing. - A run that fails is not charged.
- If the organization has fewer than 25 credits left, the run is refused with
402 INSUFFICIENT_CREDITS(the body includesremainingand, when known,period_end).
Fixed-format deliverables (spec)
With spec, the agent follows a specification kept on the server: fixed sections, every number from a tool, and a machine-readable JSON block at the end. The receipt's deliverable field reports whether the answer passed the checks. Streamed runs with a spec get a longer time limit.
spec | Deliverable | Put in question |
|---|---|---|
geo-weekly-brand-health | Weekly brand health: visibility, mention rate, citation rate, by platform, competitor gaps, next steps | Brand (via org_id / brand_id); optionally the period |
geo-content-brief | A content brief for one keyword: audience, SERP and AI citation research, outline, FAQ, AEO notes | primary_keyword, domain; optionally article_title, target_prompt, country |
geo-keyword-research-report | Keyword research: volumes, intent clusters, live SERP, AI query fan-out, content opportunities | topic; optionally domain, country |
geo-serp-gap | A page against Google's top 10 and AI Overview sources: content gaps | url, query; optionally country |
Write the inputs into question, for example "url: https://example.com/blog/x, query: best standing desk for small apartments, country: us". An unknown name returns 400 SPEC_NOT_FOUND: … Available: ….
Errors
Errors are JSON: { "error": "<code or message>" }, sometimes with extra fields.
| HTTP | error | Meaning and what to do |
|---|---|---|
| 400 | INVALID_JSON, INVALID_INPUT, QUESTION_REQUIRED | Fix the request body. |
| 400 | MAX_CREDITS_OUT_OF_RANGE (25..2000) | max_credits must be an integer from 25 to 2000. |
| 400 | IDEMPOTENCY_KEY_INVALID | Fix the Idempotency-Key header. |
| 400 | SPEC_NOT_FOUND: … | Use one of the listed spec names. |
| 400 | A sentence about org_id / brand_id | Ambiguous or wrong target; the message lists the options. |
| 401 | Missing Authorization header, Invalid token, … | Missing, invalid, or expired token. Sign in again with geoly auth login. |
| 402 | INSUFFICIENT_CREDITS | Credits for this period are used up (body: remaining, period_end). Wait for the reset or raise the limit. |
| 402 | Any other message | The organization has no active subscription. |
| 403 | ORG_SELECTION_REQUIRED, BRAND_NOT_ALLOWED_FOR_TOKEN, Authorization not granted for this client. … | The consent does not cover this. Re-authorize and choose the organization. |
| 404 | BRAND_NOT_FOUND, RUN_NOT_FOUND | Not found, or not visible to this token. |
| 413 | QUESTION_TOO_LONG, CONTEXT_TOO_LONG | Over 4,000 / 16,000 characters. |
| 429 | Rate limit exceeded. … | Too many requests; back off and retry. |
| 502 | RUN_FAILED | JSON mode only: the run failed and was not charged. Retry. |
| 503 | BILLING_UNAVAILABLE, IDEMPOTENCY_UNAVAILABLE | Temporary. Retry shortly (with the same Idempotency-Key). |
Rate limits and concurrency
- Requests share one per-account, per-minute rate limit with GEOly MCP; over the limit you get
429. Back off before retrying. - There is no separate cap on concurrent runs. Each run reserves up to its own budget when it starts, so several runs at once each draw on the organization's credits.
Example: Python
import json
import os
import requests
TOKEN = os.environ["GEOLY_ACCESS_TOKEN"] # your GEOly OAuth access token
resp = requests.post(
"https://app.geoly.ai/api/agent/runs",
headers={
"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Idempotency-Key": "weekly-review-2026-09-22",
},
json={"question": "Summarize our AI visibility this week.", "max_credits": 300},
stream=True,
timeout=(10, 60), # connect timeout, idle read timeout (heartbeats arrive every 10 s)
)
resp.raise_for_status()
if resp.headers.get("Content-Type", "").startswith("application/json"):
print(resp.json()) # an idempotent replay of an earlier run
else:
event = None
for line in resp.iter_lines(decode_unicode=True):
if line.startswith("event:"):
event = line[len("event:"):].strip()
elif line.startswith("data:") and event in ("done", "error"):
payload = json.loads(line[len("data:"):])
print(event, payload.get("stopped_reason"), payload.get("answer") or payload.get("message"))
Example: JSON mode with curl
curl https://app.geoly.ai/api/agent/runs \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"question":"What is our mention rate on Perplexity this month?","max_credits":100}'
Integration tips
- Prefer streaming. Use JSON mode only for questions you know finish within seconds.
- Treat
heartbeatas normal and set the idle timeout to 30 seconds or more. - Keep the
run_id. Disconnects and timeouts do not lose the answer; read it back by id. - Check
stopped_reasonbefore using the answer. In automated jobs, rerun or raisemax_creditswhen it is notdone. - Ask once, precisely. Put the period, platforms, and output format in
question, and background incontext. - Always send an
Idempotency-Keyfrom jobs that may retry.
Limits and boundaries
- Scope follows the consent. The API reaches only the organizations and permissions granted to the token.
- Read-only by default. Write tools are used only with
allow_writes: trueand a Write grant for that resource on a single-organization consent. Running monitoring on demand (trigger_prompt) is never available to the hosted agent. - Plans. An active subscription is required. Public industry intelligence is used only when the organization's plan includes it (Grow and above).
- Bounded runs. At most 2,000 credits and 20 steps per run, with a time limit (longer for
specruns). An early stop is reported instopped_reason. - Fixed model. The API uses the model GEOly selects; you cannot bring your own.
- Runs are stored and visible to your token's organization scope through
GET /api/agent/runs.
Related
- GEOly CLI — the easiest way to call this API from a terminal or a coding agent.
- GEOly MCP User Guide — call GEOly data tools from your own AI client.
Basic Management Function
Previous
GEOly CLI: Use GEOly from Your Own Agent
Install the GEOly CLI, authorize once, and your coding agent — Claude Code, Codex, Cursor, or any agent that can run shell commands — can query your brand's AI visibility data directly. The credential stays on your machine and usage draws on your organization's own credits.
GEOLY Docs