Authentication and specification
Create a brand-bound key under Settings → Developers → API keys. Send it as a Bearer token. The brand comes from the key, not a request parameter. A key without the required read/write permission is rejected./api/v1/openapi.json URL redirects to it. Stable v1 fields retain their meaning and units; clients must tolerate additional fields and new response enum values.
Resources and reports
Reports accept JSON bodies and do not create resources. Other GEOly tools, including capabilities without a resource listed here, remain available through MCP or the Agent API.
Response contract
Successful responses use{data, page?, meta}. Errors use {error:{code,message,details?},meta}. Branch on status and error.code, not message text.
Request-Idmatchesmeta.request_id.meta.quotais the actual billing outcome. A 404 is not necessarily free.meta.schema_conformant=falsemeans output drift was detected. Do not advance a synchronization checkpoint on such a response.- Names ending in
_pctuse 0–100,_ratiouses 0–1, and sentiment polarity uses -1 to 1. business_dateis the UTC+8 business day.record_date_atis its stored timestamp; do not relabel it as the business date.is_brandedfollows the product’s shared prompt classification; clients should consume the returned value rather than implement their own text matcher.
Prompt and record pagination
The prompt catalog acceptslimit up to 200, cursor, status=active|archived, and topic_id. Its keyset order is creation time plus id. It is not a snapshot: filters are reapplied on every page.
Prompt records accept limit up to 200, cursor, platform, sort_order=asc|desc, time_range=7d|30d|90d, or a pair of start_date and end_date. Exact windows cover at most 366 days. A window over 90 days uses the existing long-window multiplier.
Record-list cursors wrap bounded offset pagination. At most 10,000 positions are reachable per query, rounded down to complete pages. page.capped=true means you must narrow the date window. Concurrent inserts, backfills and deletes can move positions even in ascending order: deduplicate by id and reconcile when necessary.
Send the same filters and limit with every cursor. A cursor from another brand, prompt or query is rejected. A valid prompt with no records returns an empty array; a confirmed missing or foreign prompt returns 404. If the extra existence check fails, an empty array alone cannot prove the prompt exists.
Full answer text
include=answer_text, answer_text is null and its state is not_requested. A requested answer is either complete or unavailable; the API does not pass off a text preview as the full answer. Oversized results return 422 before settlement. Omit the expansion to fetch metadata.
Detail citations, search sources and shopping are bounded and include total counts and truncation flags. A source-chip citation identifies a website, not a page, so its URL is null. Use displayed for the sentiment and rank shown by the product, rather than interpreting raw analysis fields independently.
Incremental synchronization
GET /api/v1/records scans completed records by change time plus id, so an answer that completes late is not skipped merely because its business date is old. A record whose main row is updated again (for example a sentiment analysis write-back) appears again with the same id. The feed is a REST resource only; there is no equivalent MCP tool or view.
Parameters:
limit: 1–100, default 25.updated_since: start of the first page, a UTC timestamp with milliseconds, default1970-01-01T00:00:00.000Z. Ignored whencursoris present, so you can omit it on later pages.business_date_from: optional business-date filter. It filters inside each scanned batch and does not shorten the scan.include:answer_text,citations, oranswer_text,citations.cursor: the returned continuation or saved synchronization cursor. Keeplimit,business_date_fromandincludeunchanged while using it.
- Upsert returned rows by id and change time.
- Persist the processed data and
page.sync_cursortogether. - Follow
page.next_cursoruntil null, then poll later using the saved sync cursor and unchanged filters. Polling every 5 to 15 minutes is enough; more frequent polls only pay for empty pages. - Stop checkpoint advancement if
schema_conformantis false.
updated_since (for example the last 30 days) rather than relying on business_date_from, which still scans from updated_since and bills every empty page.
mention_rank is a read-time value. Rank write-backs do not advance the record’s change time, so a rank can change while changed_at stays the same, and the feed will not emit the record again for it. In production, ranks are usually filled in about an hour after a record’s last update; historical rescans (alias changes, entity promotions) can reach back days or weeks. The feed also does not emit deletions, records that leave the completed state (for example a same-day rerun that then fails), child-only citation or text edits, or backdated corrections. Reconcile by starting a separate pass without a cursor from an updated_since rewound by an overlap of at least 24 hours (longer after a rescan), or by re-reading records by date. A rewound updated_since sent together with a saved cursor is ignored and reconciles nothing.
Citations are capped at 30 per record, ordered by position (nulls last) and then id, the same order and cut as GET /api/v1/records/{record_id}, with citations_truncated. Oversized expansions return 422; reduce limit or omit expansions. It requires its dedicated index; 503 FEED_NOT_READY means deployment prerequisites are not ready, not that a slow full scan is running.
Writes and idempotency
tags_state is saved when every requested tag was read back, partial when the prompt was created but at least one requested tag was not saved, and unavailable when the saved tags could not be confirmed; tags_warning explains anything but saved. The prompt exists in all three cases, so never create it again because of this field. Duplicate tag names count once.
Triggering POST /api/v1/prompts/{prompt_id}/runs with {} returns 202 if work was dispatched, otherwise 200; Location points to that business day’s records. Refusals decided before anything is written return the matching 4xx with details.reason set to the cause and details.write_outcome = not_started, and the same key can be retried: 402 PLAN_LIMIT_REACHED when the prompt is beyond the plan’s prompt limit, 400 FAILED_PRECONDITION for an archived prompt or when no platform is allowed or available.
An optional Idempotency-Key must be 8–128 characters of letters, digits and _ - : .. Completed responses replay for 24 hours with Idempotent-Replayed: true, after current authorization is checked. The key is scoped to organization, brand and operation; a different normalized body with the same unexpired key returns 409.
REQUEST_IN_PROGRESS: wait and retry the same key.IDEMPOTENCY_KEY_REUSED: the key belongs to a different request.IDEMPOTENCY_OUTCOME_UNKNOWN: this attempt, or the earlier one with the same key, may have written data. It is also returned without a key when the outcome cannot be determined;error.messagesays why andmeta.quotacarries the receipt. Inspect the resource; do not blindly retry with a new key.IDEMPOTENCY_UNAVAILABLE: no write was executed because durable claim storage was unavailable. Retrying with the same key never executes twice; in rare cases a leftover claim answersREQUEST_IN_PROGRESSfor up to 5 minutes and thenIDEMPOTENCY_OUTCOME_UNKNOWN.
details.write_outcome: not_started means nothing was written and the same key can be retried; unknown means inspect first. A 500 returns a fixed message only; quote the Request-Id when reporting it.
Unknown or abandoned in-flight claims are not automatically reexecuted, even after 24 hours. A failure to persist a successful response is treated as unknown. After the 24-hour replay window the same key is treated as a new request and runs again, even with a different body, so do not reuse a key across days. Unknown or in-flight claims are kept for 90 days and keep answering IDEMPOTENCY_OUTCOME_UNKNOWN until they are deleted; only then can that key be used again. Completed results are also kept for 30 days for troubleshooting; that does not extend the replay window. Without a key, the API cannot provide this protection.
Billing
OpenAPIx-geoly-cost declares the nominal price of each operation in MCP credits, the same as the matching GEOly MCP tool (tier “free” with base 0 means no MCP credits are charged, which includes the two write endpoints). The receipt in meta.quota is authoritative; its charge is charged, min_fee, free, refunded, refund_pending or unbilled. A call that finds no data, or asks for an AI platform the brand has not purchased, pays the operation’s minimum fee (min_fee). Record reads and the feed use light per-call pricing (currently 1 credit, including empty feed polls). Monitoring triggers consume monitoring credits through the existing workflow. Some error responses are still billed: a not-found from the record detail or the prompt overview counts as one call, and a report that rejects its parameters after reading data is billed as settled, so always read meta.quota. Other rejected requests, including every 429, 500 and 503, are not charged.
Honor Retry-After on 429/503. Read retries may be billed again. For writes, inspect details.write_outcome and use the same idempotency key; HTTP status alone does not prove that no side effect occurred.
Rate limits, budgets and timeouts
- Per key: a per-minute request limit (currently 120, or 300 for Enterprise organizations; the
X-RateLimit-Limitheader is authoritative), shared with the same key’s MCP calls. Over it:429 RATE_LIMITED. - In flight: a limit on concurrent calls per key and per organization. Over it:
429 BUSY. - Endpoint guards:
x-geoly-cost.guard_rpmis a per-organization limit per minute, counted together with the organization’s MCP and Agent API calls of the same tool. Over it:429 GUARDED_RATE_LIMITED. - Database budget: 3,600 database seconds per rolling 60 minutes per organization, across all its keys and MCP. Over it:
429 BUDGET_EXCEEDED.
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). Every executed call also carries X-DB-Budget-Limit, X-DB-Budget-Used and X-DB-Budget-Reset; slow down as Used approaches Limit instead of waiting for a 429. A 429 is never charged and always carries Retry-After.
One call runs for at most about 45 seconds on the server, so set your HTTP client timeout to at least 60 seconds. A timeout returns 503 TOOL_TIMEOUT (not charged); narrow the window or filters before retrying. JSON request bodies are limited to 64 KB. API result bodies have a 4 MB hard ceiling. Unsupported methods, including HEAD and OPTIONS on resource routes, return 405 without invoking a billable read.