Developer Platform: MaxAEO Developer Platform: call the open API and MCP server with an API key to fetch answers and cited sources from AI platforms # Developer Platform Overview > The MaxAEO Developer Platform offers an open API and an MCP server — use one API key to ask ChatGPT, Perplexity, Gemini, and other major AI platforms a question and get back the raw answer, cited sources, and a share link, billed only for successful calls. The MaxAEO Developer Platform opens up the engine MaxAEO uses to collect AI answers so your own code can use it: you submit a question and a target platform, we ask that platform for real, and return the AI’s **raw answer, cited sources, and share link** — plus the reasoning steps and video/image cards when the platform provides them. Common use cases: * **Build your own AI search monitoring**: send brand-related questions to AI platforms on a schedule, and track in your own data warehouse whether your brand is mentioned and which pages get cited. * **Validate content and GEO work**: after publishing, check directly whether AI answers cite your pages (the `cited` field). * **Research and analysis**: collect answers to the same question across platforms and compare viewpoints, sources, and answer structure. * **Give your AI agent an “ask another AI” tool**: connect over MCP so clients like Claude Code and Cursor can call it directly. ## Two ways to integrate [Section titled “Two ways to integrate”](#two-ways-to-integrate) | | Open API | MCP server | | --------------------- | -------------------------------------------------------- | -------------------------------------- | | Best for | Your backend services, scripts, data pipelines | AI clients and agents that support MCP | | Protocol | HTTPS + JSON, always `POST` | MCP Streamable HTTP (stateless) | | Capabilities | List platforms, submit tasks, retrieve tasks, list tasks | The same four, exposed as 4 tools | | Authentication | `Authorization: Bearer ` | Same | | Billing & rate limits | Credits charged per successful task | Identical to the open API | Both share the same API key, the same credit balance, and the same rate limits. ## Build with an AI coding assistant [Section titled “Build with an AI coding assistant”](#build-with-an-ai-coding-assistant) When you write integration code with an AI coding assistant such as Claude Code or Cursor, give it `maxaeo-api.md`. It’s an integration guide written for AI: the basics, the fields of the four endpoints and the task object, error codes, the rules that are easiest to get wrong, and ready-to-run Python / Node.js reference implementations. The file doesn’t contain or need your API key. The file starts with Agent Skill `name` / `description` metadata and the rest is plain Markdown, so you can install it as a skill or use it as an ordinary document. [Download maxaeo-api.md](maxaeo-api.md) * Claude Code Install it as a skill — pick one: ```bash # This project only (can be shared with your team through the repo) mkdir -p .claude/skills/maxaeo-api && curl -fsSL https://maxaeo.ai/docs/developers/maxaeo-api.md -o .claude/skills/maxaeo-api/SKILL.md # All your projects mkdir -p ~/.claude/skills/maxaeo-api && curl -fsSL https://maxaeo.ai/docs/developers/maxaeo-api.md -o ~/.claude/skills/maxaeo-api/SKILL.md ``` From then on, when you ask Claude Code to call the MaxAEO API or set up MCP, it loads the skill automatically based on its `description`. * Other AI coding tools Save it in your project: ```bash mkdir -p docs && curl -fsSL https://maxaeo.ai/docs/developers/maxaeo-api.md -o docs/maxaeo-api.md ``` Then add a line to `AGENTS.md` (or your tool’s rules file): ```markdown - Before calling the MaxAEO API or MCP server, read docs/maxaeo-api.md and follow its "Rules you must follow". ``` Tools that support Agent Skills can also use it as `maxaeo-api/SKILL.md` in their skills directory. * Chat assistants If the assistant can browse the web, put the link in your prompt: > Read , then write me a Python script that sends 10 questions to both ChatGPT and Perplexity and records whether the answers cite example.com. If it can’t browse, download the file and attach it. For more detail, every Developer Platform page is also merged into one text file, [developer-platform.txt](../_llms-txt/developer-platform.txt), which you can hand to an AI in full. ## How it works: asynchronous tasks [Section titled “How it works: asynchronous tasks”](#how-it-works-asynchronous-tasks) An AI platform typically takes anywhere from tens of seconds to two minutes to generate an answer, plus any time spent in the queue, so the API is **asynchronous**: 1. **Submit a task**: call [Submit a Task](api/submit-task/) with a platform and a question. You get a `task_id` back immediately, with status `queued`. Credits for that platform’s price are **reserved** at submission. 2. **Poll for the result**: call [Retrieve a Task](api/get-task/) about every 15 seconds until the status becomes `succeeded`, `failed`, or `canceled`. 3. **Read the result**: on success, read the answer and cited sources from `answer` and `raw_result`, and the credits are **charged** at the price locked in at submission. Failed or canceled tasks are **free** — the reserved credits are released. The full state flow is covered in [Task Lifecycle](guides/task-lifecycle/). ## At a glance [Section titled “At a glance”](#at-a-glance) | Item | Value | | ----------------- | --------------------------------------------------------------------------------------------------------------------------- | | API Base URL | `https://maxaeo.ai/geoapi/geodata/developer` | | MCP endpoint | `https://maxaeo.ai/geoapi/geodata/developer/mcp` | | Authentication | Request header `Authorization: Bearer gdev_…` — see [Authentication](authentication/) | | Request format | Every endpoint is `POST` with a JSON object body | | Response format | `{"ret": 1, "data": {"ent": …}}`; any `ret` other than 1 is an error — see [API Overview](api/) | | Developer Console | [maxaeo.ai/developers](https://maxaeo.ai/developers/): create API keys, try requests in the Playground, view logs and usage | The Developer Platform uses **the same account** as the MaxAEO product — sign in to the console the same way you sign in to MaxAEO. ## Developer Console pages [Section titled “Developer Console pages”](#developer-console-pages) | Console page | What it’s for | Docs | | ---------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------- | | Overview | Quickstart progress, credit balance, last 7 days of requests, platform status | This page | | APIs & Platforms | Supported platforms, their status, and 24-hour success rate and latency | [Platforms, Priority & Country](guides/platforms/) | | Playground | Send requests without writing code, inspect the response, and copy the calling code | [Playground](console/playground/) | | API Keys | Create, disable, roll, and delete API keys | [Manage API Keys](console/api-keys/) | | Request Logs | Per-request details: status, errors, and responses | [Logs & Usage](console/logs-usage/) | | Usage | Credit balance, consumption, success rate, quotas and limits | [Logs & Usage](console/logs-usage/) | | Pricing | Per-platform prices for both tiers, and billing rules | [Billing & Credits](guides/billing/) | Use API keys on the server only An API key acts on behalf of your account and spends your credits directly. Never put it in browser code, mobile apps, browser extensions, or public repositories — read it from an environment variable on your server. ## Start here [Section titled “Start here”](#start-here) [Quickstart](quickstart/)Create an API key, send your first request, and get an AI answer in about five minutes. [Authentication](authentication/)Header format, what a key can access, and security best practices. [API Reference](api/)Parameters, responses, and error codes for the four open API endpoints. [Connect via MCP](mcp/)Call it directly from Claude Code, Cursor, VS Code, and other clients. [Working with Results](guides/results/)Make sense of the answer text, cited sources, reasoning steps, and media cards. [Billing & Credits](guides/billing/)How credits are reserved, charged, and released, and how to estimate costs. # API Overview > Conventions shared by every MaxAEO API endpoint — base URL, endpoint list, request format and headers, the ret/data.ent response envelope, bilingual msg error strings, field naming and data types, unknown-field rejection, and request size limits. The MaxAEO API is a small set of JSON endpoints over HTTPS, authenticated with an API key. This page covers the conventions every endpoint shares; see the endpoint pages below for parameters and responses. ## Base URL [Section titled “Base URL”](#base-url) ```text https://maxaeo.ai/geoapi/geodata/developer ``` Every endpoint lives under this URL. In code you’d usually store it as `BASE_URL` and append each endpoint’s path (such as `/task/submit`). ## Endpoints [Section titled “Endpoints”](#endpoints) | Endpoint | URL | Description | | --------------------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------- | | [List platforms](list-platforms/) | `POST https://maxaeo.ai/geoapi/geodata/developer/platforms/get` | Available platforms, with prices, status, and last-24-hour stats | | [Submit a task](submit-task/) | `POST https://maxaeo.ai/geoapi/geodata/developer/task/submit` | Ask a platform a question and get back a `task_id` | | [Retrieve a task](get-task/) | `POST https://maxaeo.ai/geoapi/geodata/developer/task/get` | Read a task’s status and result by `task_id` | | [List tasks](list-tasks/) | `POST https://maxaeo.ai/geoapi/geodata/developer/task/key/list` | List tasks submitted with the current key, e.g. to recover a `task_id` | | [MCP server](../mcp/) | `POST https://maxaeo.ai/geoapi/geodata/developer/mcp` | MCP Streamable HTTP entry point that wraps the four endpoints above as tools | Every endpoint that returns or operates on a task uses the [task object](task-object/). The same four capabilities are also available as MCP tools — see [Connect via MCP](../mcp/). ## Integration flow [Section titled “Integration flow”](#integration-flow) A complete integration uses only these four endpoints, chained in this order: | Step | Call | What to do | | ------------------ | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1. Pick a platform | `POST https://maxaeo.ai/geoapi/geodata/developer/platforms/get` | Get the `platform` IDs and prices, skipping any whose `platform_status` is `maintenance`; cache for a few minutes rather than hardcoding them | | 2. Submit | `POST https://maxaeo.ai/geoapi/geodata/developer/task/submit` | Send `platform` and `query` (optionally `priority`, `thinking`, `country`); **save the `task_id` as soon as you get it** | | 3. Poll | `POST https://maxaeo.ai/geoapi/geodata/developer/task/get` | Check unfinished tasks about every 15 seconds until `status` is `succeeded`, `failed`, or `canceled` | | 4. Read the result | Same as above | `succeeded`: read `answer` and `raw_result.references`; `failed` / `canceled`: handle by `error_code`, nothing is charged | | 5. Recover | `POST https://maxaeo.ai/geoapi/geodata/developer/task/key/list` | If a submission times out, drops, or returns `-1`, look up the `task_id` by query text and time; only resubmit once you’ve confirmed nothing was created | Success is judged on two levels: a `ret` other than `1` in the response body means the call wasn’t accepted (no task was created); whether a task succeeded is given by its `status`, with the reason for a failure in `error_code`. See [Error Handling](../guides/errors/). For complete runnable code, see the [Quickstart](../quickstart/#complete-example). ## Requests [Section titled “Requests”](#requests) * **Method**: every endpoint uses `POST`. * **Headers**: * `Authorization: Bearer ` (required — see [Authentication](../authentication/)) * `Content-Type: application/json` (recommended) * **Body**: a single JSON object, no larger than 64 KiB. Endpoints without parameters still need `{}`; an empty body returns `1001`. * **Only documented fields are accepted**: if the body contains a field the endpoint doesn’t define — including typos such as `taskId` instead of `task_id` — the whole request fails with `1001` (“The request contains unsupported fields.”) instead of the field being silently ignored. * **Encoding**: UTF-8. ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/get" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"task_id": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23"}' ``` ## Response [Section titled “Response”](#response) Every endpoint uses the same response envelope. On **success**, `ret` is `1` and the payload is in `data.ent`: ```json { "ret": 1, "data": { "ent": { "task_id": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23", "status": "queued" } } } ``` On **failure**, `ret` is an error code and `msg` is a JSON-encoded **string** holding the message in both Chinese and English: ```json { "ret": 1001, "msg": "{\"zh\":\"参数错误:task_id 不能为空且不超过 64 个字符\",\"en\":\"Invalid parameters: task_id is required and must be at most 64 characters\"}" } ``` Key points: * **Check `ret`, not the HTTP status.** Both successful and failed business responses use HTTP `200`. You only see a non-200 status in two cases: the request itself is wrong (a method other than `POST` returns `405`, a wrong path returns `404`), or something goes wrong on the network path (a proxy timeout, for example) — see [Error Handling](../guides/errors/#non-200-http-status-codes). * **The body is always JSON**, but the `Content-Type` header may currently be `text/plain`. Parse the body as JSON directly; don’t rely on `Content-Type`. * Every response carries `Cache-Control: no-store`. Don’t cache responses in your client or proxies. * See [Error Handling](../guides/errors/) for every error code. ## Fields and data types [Section titled “Fields and data types”](#fields-and-data-types) | Convention | Details | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Field names | Request and response fields use snake\_case, such as `task_id` and `created_at`. Exception: fields inside `raw_result` use lowerCamelCase — see [The Task Object](task-object/#raw_result) | | Timestamps | Unix timestamps in **seconds**; `0` means “hasn’t happened yet” (for example `finished_at` on an unfinished task) | | Dates | `monitor_date` is `YYYY-MM-DD` in China Standard Time (UTC+8) | | Zero values | Top-level fields are always present even when empty (`""`, `0`, `false`); zero-value fields inside `raw_result` are omitted | | Empty arrays | An empty list may come back as `[]` or `null` — treat both as an empty array | | New fields | We may add fields to responses. Make your parser ignore fields it doesn’t recognize | ## Authentication and security [Section titled “Authentication and security”](#authentication-and-security) * Send the API key only in the `Authorization` header. Keys in the URL or the request body are not accepted. * The key’s status is re-checked on every request; an invalid key always returns `1030`. * Call the API only from your server. Calling it directly from a browser or mobile app exposes your key to end users. ## Endpoint pages [Section titled “Endpoint pages”](#endpoint-pages) [List platforms](list-platforms/)POST https\://maxaeo.ai/geoapi/geodata/developer/platforms/get [Submit a task](submit-task/)POST https\://maxaeo.ai/geoapi/geodata/developer/task/submit [Retrieve a task](get-task/)POST https\://maxaeo.ai/geoapi/geodata/developer/task/get [List tasks](list-tasks/)POST https\://maxaeo.ai/geoapi/geodata/developer/task/key/list [The task object](task-object/)Full field reference for tasks and raw\_result # Retrieve a Task > POST https://maxaeo.ai/geoapi/geodata/developer/task/get — reads a task's latest status and result by task_id. Successful tasks include the answer text, cited sources, share link, and credits charged. Use it to poll asynchronous tasks; it doesn't use any credits. ```text POST https://maxaeo.ai/geoapi/geodata/developer/task/get ``` Reads the latest status of a task by `task_id`. Once the task succeeds, the response includes the full result. **Doesn’t use any credits.** ## Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------- | | `task_id` | string | Yes | The task ID returned by [Submit a task](../submit-task/) or [List tasks](../list-tasks/), up to 64 characters | **Which tasks you can read**: every task the same user submitted in their personal account, no matter which API key was used or whether it was submitted from the Playground in the Developer Console. If the key that submitted a task is disabled or deleted, you can still read the task with any other valid key. ## Request example [Section titled “Request example”](#request-example) * cURL ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/get" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"task_id": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23"}' ``` * Python ```python import os import time import requests URL = "https://maxaeo.ai/geoapi/geodata/developer/task/get" HEADERS = {"Authorization": f"Bearer {os.environ['MAXAEO_API_KEY']}"} def wait_task(task_id, interval=15): while True: data = requests.post(URL, headers=HEADERS, json={"task_id": task_id}, timeout=30).json() if data["ret"] != 1: raise RuntimeError(f"ret={data['ret']}: {data['msg']}") task = data["data"]["ent"] if task["status"] in ("succeeded", "failed", "canceled"): return task time.sleep(interval) task = wait_task("task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23") print(task["status"], task["answer"][:200]) ``` * Node.js ```js const TASK_GET_URL = "https://maxaeo.ai/geoapi/geodata/developer/task/get"; const headers = { Authorization: `Bearer ${process.env.MAXAEO_API_KEY}`, "Content-Type": "application/json", }; async function waitTask(taskId, intervalMs = 15_000) { for (;;) { const resp = await fetch(TASK_GET_URL, { method: "POST", headers, body: JSON.stringify({ task_id: taskId }) }); const data = await resp.json(); if (data.ret !== 1) throw new Error(`ret=${data.ret}: ${data.msg}`); const task = data.data.ent; if (["succeeded", "failed", "canceled"].includes(task.status)) return task; await new Promise((resolve) => setTimeout(resolve, intervalMs)); } } const task = await waitTask("task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23"); console.log(task.status, task.answer.slice(0, 200)); ``` ## Response [Section titled “Response”](#response) `data.ent` is a [task object](../task-object/). Read it based on `status`: * **Unfinished** (`queued` / `dispatching` / `pending` / `running`): result fields are empty and `credits` is the reserved amount. Keep polling. * **`succeeded`**: `answer` holds the answer text, `raw_result` holds the structured result (sources, agent steps, media cards, and so on), and `credits` is the amount charged. * **`failed` / `canceled`**: the reason is in `error_code` and `error_message`, and `credits` is `0`. Example of a successful task (long text in `answer`, `result_json`, and similar fields is truncated): ```json { "ret": 1, "data": { "ent": { "thinking": false, "task_id": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23", "platform": "chatgpt", "query": "best mechanical keyboards for programmers", "status": "succeeded", "answer": "For programming, the best mechanical keyboards balance a comfortable switch feel, a practical layout, and good ergonomics for long sessions…", "result_json": "{\"taskId\":\"task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23\",\"platform\":\"chatgpt\",…}", "share_link": "https://chatgpt.com/share/…", "error_code": "", "error_message": "", "started_at": 1793018431, "finished_at": 1793018497, "created_at": 1793018425, "updated_at": 1793018503, "monitor_date": "2026-10-26", "priority": 2, "country": "", "credits": 6, "raw_result": { "taskId": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23", "platform": "chatgpt", "monitorDate": "2026-10-26", "query": "best mechanical keyboards for programmers", "success": true, "rawContent": "For programming, the best mechanical keyboards balance a comfortable switch feel, a practical layout, and good ergonomics for long sessions…", "references": [ { "rank": 1, "title": "The Best Mechanical Keyboards for Programmers in 2026", "url": "https://www.example.com/best-keyboards-for-programmers", "snippet": "We tested switches, layouts, and keycaps to find the best keyboards for coding…", "source": "Example Tech", "domain": "www.example.com" }, { "rank": 2, "title": "Keyboard layouts compared: TKL vs 75% vs 60%", "url": "https://blog.example.org/keyboard-layouts", "domain": "blog.example.org" } ], "shareLink": "https://chatgpt.com/share/…", "elapsedMs": "48213", "completedAt": "2026-10-26T12:41:37.512Z", "priority": 2 } } } } ``` Global platforms currently don’t distinguish whether a source was cited, so their `references` carry no `cited` field — that’s different from `cited: false`. See [Working with Results](../../guides/results/#the-three-states-of-cited). Example of a task where the platform run failed: ```json { "ret": 1, "data": { "ent": { "thinking": false, "task_id": "task_9b2e4f60-1c3d-4e8a-b7f5-2d6c8a0e4b19", "platform": "gemini", "query": "how should a beginner choose their first pair of running shoes?", "status": "failed", "answer": "", "result_json": "{\"taskId\":\"task_9b2e4f60-1c3d-4e8a-b7f5-2d6c8a0e4b19\",\"error\":\"{\\\"zh\\\":\\\"平台抓取失败,本次不扣费,请稍后重试或更换平台\\\",\\\"en\\\":\\\"The platform crawl failed. No credits were charged. Please try again later or choose another platform.\\\"}\",…}", "share_link": "", "error_code": "crawl_failed", "error_message": "{\"zh\":\"平台抓取失败,本次不扣费,请稍后重试或更换平台\",\"en\":\"The platform crawl failed. No credits were charged. Please try again later or choose another platform.\"}", "started_at": 0, "finished_at": 1793007356, "created_at": 1793006882, "updated_at": 1793007363, "monitor_date": "2026-10-26", "priority": 2, "country": "", "credits": 0, "raw_result": { "taskId": "task_9b2e4f60-1c3d-4e8a-b7f5-2d6c8a0e4b19", "platform": "gemini", "monitorDate": "2026-10-26", "query": "how should a beginner choose their first pair of running shoes?", "error": "{\"zh\":\"平台抓取失败,本次不扣费,请稍后重试或更换平台\",\"en\":\"The platform crawl failed. No credits were charged. Please try again later or choose another platform.\"}", "elapsedMs": "2", "completedAt": "2026-10-26T09:35:56.853Z", "priority": 2 } } } } ``` `error_message` and `raw_result.error` are `{"zh","en"}` JSON strings — parse them to show the message in your user’s language, and branch on `error_code` rather than the wording. The top-level `thinking` is the thinking mode requested at submission. Neither task above requested thinking, so `raw_result` has no `thinking`; when it’s requested and actually takes effect, `raw_result` includes `"thinking": true`. See [Thinking mode](../../guides/platforms/#thinking-mode-thinking). Note that `started_at` can be `0`: if a task jumps straight to its final result without a “running” status being observed, no start time is recorded. To measure duration, use `finished_at - created_at` (includes queueing) or `raw_result.elapsedMs` (execution only). ## Polling tips [Section titled “Polling tips”](#polling-tips) * Polling about every 15 seconds is enough; polling more often usually returns the same status. See [Task Lifecycle](../../guides/task-lifecycle/#polling-how-often-to-check). * Tasks have no server-side deadline and can queue for a while. Once your own business timeout passes, you can set the task aside and fetch the result later by `task_id`. * Decide whether a task is finished from `status` alone. Don’t stop polling just because `error_code` isn’t empty. ## Errors [Section titled “Errors”](#errors) | `ret` | Cause | | ------ | -------------------------------------------------------------------------------------- | | `1001` | `task_id` is empty or longer than 64 characters, or the body contains undefined fields | | `1030` | Invalid API key | | `1031` | The task doesn’t exist or doesn’t belong to the current user | | `1003` | Account permission problem (rare) — please contact us | | `-1` | Service temporarily unavailable — safe to retry | # List Platforms > POST https://maxaeo.ai/geoapi/geodata/developer/platforms/get — returns the AI platform IDs available to the API (including any under maintenance), with each platform's region, status, P1/P2 prices, and last-24-hour success rate and latency stats. Free to call. ```text POST https://maxaeo.ai/geoapi/geodata/developer/platforms/get ``` Returns the AI platforms available to the API (including any under maintenance — see `platform_status`). Call it before submitting to get platform IDs and prices, or use it as a connectivity check for your API key. **Doesn’t use any credits.** ## Request [Section titled “Request”](#request) No parameters. Send an empty object `{}` as the body. * cURL ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/platforms/get" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` * Python ```python import os import requests resp = requests.post( "https://maxaeo.ai/geoapi/geodata/developer/platforms/get", headers={"Authorization": f"Bearer {os.environ['MAXAEO_API_KEY']}"}, json={}, timeout=30, ) data = resp.json() if data["ret"] != 1: raise RuntimeError(data["msg"]) for item in data["data"]["ent"]["items"] or []: print(item["platform"], item["platform_status"], item["p1_credits"], item["p2_credits"]) ``` * Node.js ```js const resp = await fetch("https://maxaeo.ai/geoapi/geodata/developer/platforms/get", { method: "POST", headers: { Authorization: `Bearer ${process.env.MAXAEO_API_KEY}`, "Content-Type": "application/json", }, body: "{}", }); const data = await resp.json(); if (data.ret !== 1) throw new Error(data.msg); for (const item of data.data.ent.items ?? []) { console.log(item.platform, item.platform_status, item.p1_credits, item.p2_credits); } ``` ## Response [Section titled “Response”](#response) Fields in `data.ent`: | Field | Type | Description | | ----------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `platforms` | string\[] | IDs of platforms with a configured price, in the same order as `items` — the possible values for `platform` in [Submit a task](../submit-task/). **Includes platforms under maintenance** (submitting to them returns `1029`), so check `items[].platform_status` first | | `items` | PlatformItem\[] | Details for each platform, in the same order as `platforms` | `PlatformItem`: | Field | Type | Description | | ---------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `platform` | string | Platform ID, such as `chatgpt` | | `region` | string | `domestic` (mainland China) or `overseas` (global) | | `platform_status` | string | `normal` (operational), `degraded`, or `maintenance` (not accepting tasks — submissions return `1029`) | | `p1_credits` | integer | Credits per successful call at P1 Priority | | `p2_credits` | integer | Credits per successful call at P2 Standard | | `has_stats` | boolean | Whether the last-24-hour sample size meets the stats threshold. When `false`, `success_rate` and `p50_duration_seconds` are `0` and shouldn’t be displayed | | `success_rate` | number | Success rate over the last 24 hours, 0–1 | | `p50_duration_seconds` | integer | Median time from submission to completion for successful tasks over the last 24 hours (seconds, queueing included) | | `sample_count` | integer | Tasks that reached a final status in the last 24 hours (across all callers; excludes non-platform causes such as invalid keys, rejected parameters, and cancellations) | | `stats_updated_at` | integer | When the stats were computed (Unix seconds); `0` means not computed yet | See [Platforms, Priority & Country](../../guides/platforms/#platform-status) for how to read the status and stats fields. ### Example response [Section titled “Example response”](#example-response) ```json { "ret": 1, "data": { "ent": { "platforms": ["chatgpt", "perplexity", "gemini"], "items": [ { "platform": "chatgpt", "region": "overseas", "platform_status": "normal", "p1_credits": 12, "p2_credits": 6, "has_stats": true, "success_rate": 0.9531, "p50_duration_seconds": 64, "sample_count": 128, "stats_updated_at": 1793018400 }, { "platform": "perplexity", "region": "overseas", "platform_status": "normal", "p1_credits": 10, "p2_credits": 5, "has_stats": false, "success_rate": 0, "p50_duration_seconds": 0, "sample_count": 0, "stats_updated_at": 1793018400 }, { "platform": "gemini", "region": "overseas", "platform_status": "degraded", "p1_credits": 10, "p2_credits": 5, "has_stats": true, "success_rate": 0.8125, "p50_duration_seconds": 88, "sample_count": 32, "stats_updated_at": 1793018400 } ] } } } ``` Prices and stats in this example are for illustration only — always use the values the API actually returns. ## Errors [Section titled “Errors”](#errors) | `ret` | Cause | | ------ | -------------------------------------------------------------------------------------- | | `1001` | The body isn’t a JSON object (for example, an empty body) or contains undefined fields | | `1030` | The API key is missing, invalid, disabled, or expired | | `1003` | Account permission problem (rare) — please contact us | | `-1` | Service temporarily unavailable — retry later | See [Error Handling](../../guides/errors/) for details. # List Tasks > POST https://maxaeo.ai/geoapi/geodata/developer/task/key/list — pages through the tasks submitted with the current API key, newest first, with filters for time range, platform, status, and keyword. Mainly used to recover a task_id when a submit response was lost, and for reconciliation. Excludes answer text and doesn't use any credits. ```text POST https://maxaeo.ai/geoapi/geodata/developer/task/key/list ``` Pages through the tasks submitted with **the current API key**, newest first. List items don’t include the answer; call [Retrieve a task](../get-task/) when you need the result. **Doesn’t use any credits.** Main uses: * **Recovering a `task_id`**: when a submit request timed out or your process crashed and you’re not sure the task was created, look it up by query text and time instead of submitting again. * **Reconciliation and audits**: page through a time range or status to total up credits for successful tasks and find failed ones. ## Parameters [Section titled “Parameters”](#parameters) All optional. | Parameter | Type | Description | | ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `page` | integer | Page number, starting at 1. Defaults to `1` | | `page_size` | integer | Items per page. Defaults to `20`, maximum `100` | | `start_time` | integer | Lower bound on submission time (Unix seconds, **inclusive**) | | `end_time` | integer | Upper bound on submission time (Unix seconds, **exclusive**); must be greater than `start_time` | | `platform` | string | Only tasks for this platform | | `status` | string | Only tasks in this status: `queued`, `dispatching`, `pending`, `running`, `succeeded`, `failed`, `canceled`. `pending` returns all three waiting statuses — `queued`, `dispatching`, and `pending` — and each item’s `status` is still its actual status; other values match exactly | | `keyword` | string | If it starts with `task_`, matches the task ID **exactly**; otherwise matches tasks whose query contains the text. Up to 256 characters | ## Request example [Section titled “Request example”](#request-example) Find tasks from the last hour whose query contains “mechanical keyboard”: * cURL ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/key/list" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"keyword\": \"mechanical keyboard\", \"start_time\": $(($(date +%s) - 3600)), \"page_size\": 10}" ``` * Python ```python import os import time import requests resp = requests.post( "https://maxaeo.ai/geoapi/geodata/developer/task/key/list", headers={"Authorization": f"Bearer {os.environ['MAXAEO_API_KEY']}"}, json={"keyword": "mechanical keyboard", "start_time": int(time.time()) - 3600, "page_size": 10}, timeout=30, ) data = resp.json() if data["ret"] != 1: raise RuntimeError(f"ret={data['ret']}: {data['msg']}") for item in data["data"]["ent"]["items"]: print(item["task_id"], item["status"], item["query"]) ``` * Node.js ```js const resp = await fetch("https://maxaeo.ai/geoapi/geodata/developer/task/key/list", { method: "POST", headers: { Authorization: `Bearer ${process.env.MAXAEO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ keyword: "mechanical keyboard", start_time: Math.floor(Date.now() / 1000) - 3600, page_size: 10, }), }); const data = await resp.json(); if (data.ret !== 1) throw new Error(`ret=${data.ret}: ${data.msg}`); for (const item of data.data.ent.items) { console.log(item.task_id, item.status, item.query); } ``` ## Response [Section titled “Response”](#response) Fields in `data.ent`: | Field | Type | Description | | ----------- | ----------- | -------------------------------- | | `total` | integer | Total number of matching tasks | | `page` | integer | Current page number | | `page_size` | integer | Items per page | | `items` | TaskItem\[] | Tasks on this page, newest first | `TaskItem` is a slimmed-down task object. It **doesn’t include** `answer`, `result_json`, `share_link`, `raw_result`, or `updated_at`: | Field | Type | Description | | --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `task_id` | string | Task ID | | `status` | string | Task status | | `platform` | string | Platform ID | | `query` | string | The question | | `priority` | integer | Priority at submission, `1` or `2` | | `country` | string | Requested exit country (uppercase), empty if not specified | | `thinking` | boolean | The thinking mode requested at submission; to see whether it was actually on, read `raw_result.thinking` with [Retrieve a task](../get-task/) | | `monitor_date` | string | Business date, `YYYY-MM-DD` | | `credits` | integer | Credits charged if succeeded, reserved if unfinished, `0` if failed or canceled | | `error_code` | string | Task-level error code — see [Error Handling](../../guides/errors/#task-failures-error_code) | | `error_message` | string | Error message, a `{"zh","en"}` JSON string | | `created_at` | integer | Submission time (Unix seconds) | | `started_at` | integer | When execution started; `0` if not recorded | | `finished_at` | integer | When the task finished; `0` if unfinished | ```json { "ret": 1, "data": { "ent": { "total": 2, "page": 1, "page_size": 10, "items": [ { "thinking": false, "task_id": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23", "status": "succeeded", "platform": "chatgpt", "query": "best mechanical keyboards for programmers", "priority": 2, "country": "", "monitor_date": "2026-10-26", "credits": 6, "error_code": "", "error_message": "", "created_at": 1793018425, "started_at": 1793018431, "finished_at": 1793018497 }, { "thinking": false, "task_id": "task_5a7d9c1b-3e2f-4d6a-8b0c-9e1f2a3b4c5d", "status": "pending", "platform": "perplexity", "query": "mechanical keyboard vs membrane keyboard for coding", "priority": 2, "country": "US", "monitor_date": "2026-10-26", "credits": 5, "error_code": "", "error_message": "", "created_at": 1793018390, "started_at": 0, "finished_at": 0 } ] } } } ``` ## Paging and reconciliation [Section titled “Paging and reconciliation”](#paging-and-reconciliation) * The list is sorted newest first, so if new tasks are submitted while you’re paging, later pages can repeat items from earlier pages. For reconciliation, pin `end_time` (for example, to when you started) and page within that fixed window. * To total spend for a period, filter by `status` `succeeded` and sum `credits`. ## Errors [Section titled “Errors”](#errors) | `ret` | Cause | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `1001` | Invalid parameters: `page` or `page_size` out of range, `end_time` not greater than `start_time`, a negative time, a `status` value not in the list, a `keyword` that’s too long, or undefined fields in the body | | `1003` | Account permission problem (rare) — please contact us | | `1030` | Invalid API key | | `-1` | Service temporarily unavailable — safe to retry | # Submit a Task > POST https://maxaeo.ai/geoapi/geodata/developer/task/submit — submits one question to an AI platform and immediately returns a queued task with its task_id. Supports priority (P1/P2), thinking mode, country (exit location), monitor_date, and a synchronous wait_seconds option. Successful tasks are charged at the price locked in at submission. ```text POST https://maxaeo.ai/geoapi/geodata/developer/task/submit ``` Submits one question to one AI platform. The endpoint returns the new task right away (status `queued`) along with its `task_id`; use [Retrieve a task](../get-task/) to get the result. **Every call creates a new task.** A successful task is charged at the price in effect when you submitted it. That amount is reserved at submission and released if the task fails or is canceled. ## Parameters [Section titled “Parameters”](#parameters) | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `platform` | string | Yes | Platform ID, such as `chatgpt` or `perplexity`. See [List platforms](../list-platforms/) for valid values; platforms whose `platform_status` is `maintenance` can’t be submitted to. Case-insensitive, up to 64 characters | | `query` | string | Yes | The question to ask. Up to 4,096 characters after trimming leading and trailing whitespace. Must contain visible text and no control characters other than line feed, carriage return, and tab | | `priority` | integer | No | `1` = P1 Priority, `2` = P2 Standard. Omitted or `0` means P2. The tiers queue differently and are priced differently — see [Priority](../../guides/platforms/#priority-p1-vs-p2) | | `thinking` | boolean | No | Whether to request the platform’s thinking mode. Defaults to `false`. Must be a JSON boolean. See below | | `country` | string | No | Two-letter country code (ISO 3166-1 alpha-2) such as `US` or `JP`, case-insensitive. Only affects the exit location for global platforms — see [Exit country](../../guides/platforms/#exit-country-country) | | `monitor_date` | string | No | The task’s business date, `YYYY-MM-DD`; defaults to today in China Standard Time (UTC+8). Also accepts an RFC 3339 timestamp with an offset (such as `2026-10-26T09:30:00-07:00`), which is converted to the UTC+8 date. Doesn’t affect when the task runs — see [What monitor\_date means](../../guides/task-lifecycle/#what-monitor_date-means) | | `wait_seconds` | integer | No | Seconds to wait synchronously, `0`–`120`. Defaults to `0` (return immediately). See below | No other fields are accepted; any undefined field returns `1001`. ## Synchronous wait: `wait_seconds` [Section titled “Synchronous wait: wait\_seconds”](#synchronous-wait-wait_seconds) * `0` (default): returns immediately with the task in `queued`. * `1`–`120`: the server checks the task every 2 seconds for up to this long. As soon as the task reaches a final status (`succeeded`, `failed`, or `canceled`) it returns; if time runs out first, it returns the task in whatever status it’s in. When you use a synchronous wait: * Set your HTTP client timeout longer than `wait_seconds` — leave at least 15 extra seconds. * The returned task **may still be unfinished**, so your code must be able to keep polling by `task_id`. * If the connection drops while waiting, the task itself isn’t affected — recover its `task_id` as described in [Avoiding duplicate submissions](../../guides/task-lifecycle/#avoiding-duplicate-submissions). ## Thinking mode: `thinking` [Section titled “Thinking mode: thinking”](#thinking-mode-thinking) Pass `"thinking": true` to ask the platform to answer in its thinking mode, such as ChatGPT’s thinking or DeepSeek’s DeepThink: ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/submit" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "chatgpt", "query": "best mechanical keyboards for programmers", "thinking": true}' ``` * Platforms without a thinking switch run with thinking off, without an error. Whether thinking was actually on is `raw_result.thinking` in the result; the top-level `thinking` in the response only echoes your request. * It’s noticeably slower and costs nothing extra. See [Thinking mode](../../guides/platforms/#thinking-mode-thinking) for which platforms support it. ## Request example [Section titled “Request example”](#request-example) * cURL ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/submit" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "platform": "chatgpt", "query": "best mechanical keyboards for programmers", "priority": 2 }' ``` * Python ```python import os import requests resp = requests.post( "https://maxaeo.ai/geoapi/geodata/developer/task/submit", headers={"Authorization": f"Bearer {os.environ['MAXAEO_API_KEY']}"}, json={"platform": "chatgpt", "query": "best mechanical keyboards for programmers", "priority": 2}, timeout=30, ) data = resp.json() if data["ret"] != 1: raise RuntimeError(f"ret={data['ret']}: {data['msg']}") task = data["data"]["ent"] print(task["task_id"], task["status"], task["credits"]) ``` * Node.js ```js const resp = await fetch("https://maxaeo.ai/geoapi/geodata/developer/task/submit", { method: "POST", headers: { Authorization: `Bearer ${process.env.MAXAEO_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ platform: "chatgpt", query: "best mechanical keyboards for programmers", priority: 2 }), }); const data = await resp.json(); if (data.ret !== 1) throw new Error(`ret=${data.ret}: ${data.msg}`); const task = data.data.ent; console.log(task.task_id, task.status, task.credits); ``` ## Response [Section titled “Response”](#response) `data.ent` is a [task object](../task-object/). Without a synchronous wait, a freshly submitted task has: * `status` set to `queued`; * a `task_id` of the form `task_` followed by a UUID — use it for all later lookups; * `credits` set to the reserved amount, i.e. the current price for that platform and tier; * `thinking` set to the thinking mode you requested (`false` if you didn’t pass it); * empty result fields such as `answer` and `result_json`, and no `raw_result`. ```json { "ret": 1, "data": { "ent": { "thinking": false, "task_id": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23", "platform": "chatgpt", "query": "best mechanical keyboards for programmers", "status": "queued", "answer": "", "result_json": "", "share_link": "", "error_code": "", "error_message": "", "started_at": 0, "finished_at": 0, "created_at": 1793018425, "updated_at": 1793018425, "monitor_date": "2026-10-26", "priority": 2, "country": "", "credits": 6 } } } ``` ## Errors [Section titled “Errors”](#errors) These errors happen before a task is created: **no task is created and no credits are charged**. | `ret` | Cause | What to do | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | | `1001` | Invalid parameters; `msg` names the field — for example `query` is empty or too long, `priority` isn’t 1 or 2, `country` isn’t two letters, `monitor_date` is malformed, or `wait_seconds` is outside 0–120. A body with undefined fields, or a field of the wrong type (such as `thinking` sent as the string `"true"`), also returns `1001` | Fix the parameters | | `1022` | Developer API billing isn’t enabled | Contact sales | | `1023` | Available credits don’t cover this task’s price | Top up, or use a cheaper platform or tier | | `1024` | Too many tasks in progress | Retry after some tasks finish | | `1025` | Submitting too fast | Slow down and retry with backoff | | `1029` | The platform isn’t available (no price configured, or retired) or is under maintenance | Refresh the platform list and pick another platform | | `1030` | Invalid API key | Check your key | | `1003` | Account permission problem (rare) | Contact us | | `-1` | Service temporarily unavailable | **The task may have been created.** Check with [List tasks](../list-tasks/) before deciding whether to resubmit | There’s one more case: the request passes validation and a task is created, but dispatching or running it fails afterwards. The submit call returns success, the task ends up with `status` `failed`, and the reason is in `error_code` (for example `crawler_rejected` when a platform has been retired). It isn’t charged either. See [Error Handling](../../guides/errors/#task-failures-error_code). Don't blindly retry submissions Submitting isn’t idempotent: repeated calls create multiple tasks, and each one is charged if it succeeds. If a request times out, the connection drops, or you get `-1`, first check the task list by query text and time to see whether the task was created. See [When a submission’s outcome is unknown](../../guides/errors/#when-a-submissions-outcome-is-unknown) for the full approach. # The Task Object > Full field reference for the API task object — top-level task fields (status, timestamps, credits, error codes) and every structure inside the raw_result structured result, including references, agentSteps, and mediaCards, plus its encoding rules. [Submit a task](../submit-task/) and [Retrieve a task](../get-task/) both return the task object; [List tasks](../list-tasks/) returns a slimmed-down version of it. ## Task fields [Section titled “Task fields”](#task-fields) | Field | Type | Description | | --------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `task_id` | string | Task ID, of the form `task_` followed by a UUID | | `platform` | string | Platform ID (lowercased) | | `query` | string | The question (leading and trailing whitespace trimmed) | | `status` | string | `queued`, `dispatching`, `pending`, `running`, `succeeded`, `failed`, or `canceled` — see [Task Lifecycle](../../guides/task-lifecycle/) | | `answer` | string | Answer text (Markdown), identical to `raw_result.rawContent`; empty string when there’s no result | | `result_json` | string | The raw result as a JSON string. Same structure as `raw_result`, but may include newer fields that `raw_result` doesn’t map yet; empty string when there’s no result | | `share_link` | string | The platform’s share link; empty string if none | | `error_code` | string | Task-level error code — see [Error Handling](../../guides/errors/#task-failures-error_code). **A non-empty value doesn’t mean the task failed**; rely on `status` | | `error_message` | string | Error message as a `{"zh","en"}` JSON string — parse it to show your user’s language; don’t build logic on the wording | | `created_at` | integer | Submission time (Unix seconds) | | `updated_at` | integer | Last update time (Unix seconds) | | `started_at` | integer | When execution started (Unix seconds); `0` if not started or not recorded | | `finished_at` | integer | When the task finished (Unix seconds); `0` if unfinished | | `monitor_date` | string | Business date `YYYY-MM-DD` (UTC+8), fixed at submission | | `priority` | integer | Priority at submission: `1` = P1, `2` = P2. The tier it actually ran at is `raw_result.priority` | | `country` | string | Requested exit country (uppercase); empty string if not specified. Not a verified record of the actual exit location | | `thinking` | boolean | The thinking mode requested at submission; `false` if not requested. Whether thinking was actually on is `raw_result.thinking` — see [Thinking mode](../../guides/platforms/#thinking-mode-thinking) | | `credits` | integer | `succeeded`: credits charged; unfinished: credits reserved; `failed` / `canceled`: `0` | | `raw_result` | object | Structured result, described below. **Only present once there’s a result** (success, or a failed platform run); tasks that are queued, canceled, or failed to dispatch don’t have this field | Top-level fields are always present even when empty (`""`, `0`, or `false`). ## `raw_result` [Section titled “raw\_result”](#raw_result) The structured result uses protobuf’s JSON encoding, which follows different conventions from the top-level fields: * Field names are **lowerCamelCase** (`rawContent`, `shareLink`). * **Zero-value fields are omitted**: `false`, `0`, empty strings, and empty arrays don’t appear. Fill in defaults when reading. * `elapsedMs` is a 64-bit integer encoded as a **string** (such as `"48213"`). | Field | Type | Description | | ---------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `taskId` | string | Task ID | | `platform` | string | Platform ID | | `monitorDate` | string | Business date `YYYY-MM-DD` | | `query` | string | The question as actually asked | | `success` | boolean | Whether an answer was obtained; absent when `false` | | `rawContent` | string | Raw answer text (Markdown), with the platform’s own citation markers and card placeholders preserved | | `references` | Reference\[] | Sources, described below | | `shareLink` | string | The platform’s share link | | `error` | string | Failure message — the same `{"zh","en"}` JSON string as `error_message`; don’t build logic on the wording | | `elapsedMs` | string | Execution time in milliseconds, **excluding queueing** | | `completedAt` | string | Completion time, ISO 8601 UTC, such as `2026-10-26T12:41:37.512Z` | | `priority` | integer | The tier actually used: `1` = P1, `2` = P2. Use this for reconciliation | | `thinking` | boolean | Whether thinking was actually on: `true` if it was; absent when it was off, the platform has no thinking switch, or the task failed. Use this, not the top-level `thinking`, to tell whether thinking mode took effect | | `agentSteps` | AgentStep\[] | The run’s process (thinking, searching, tool calls), in order | | `mediaCards` | MediaCard\[] | Media cards in the answer, in order of appearance | | `conversationId` | string | The platform’s conversation ID, for reference only | | `messageId` | string | The platform’s message ID, for reference only | ### Reference [Section titled “Reference”](#reference) | Field | Type | Description | | ----------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rank` | integer | Display order on the platform, starting at 1 | | `title` | string | Title | | `url` | string | Link. A few sources have no link or carry a platform-internal ID instead — validate before use | | `snippet` | string | Excerpt | | `source` | string | Source name (site name) | | `domain` | string | Domain | | `contentPlatform` | string | The content platform the source was identified as, absent if not identified | | `cited` | boolean | `true` if the answer text cites it, `false` if it was found but not cited. **If the field is absent, the platform doesn’t make the distinction** — don’t treat it as `false` | | `publishTime` | string | Publish time: ISO 8601, `YYYY-MM-DD`, or the platform’s relative-time text (such as “1 day ago”) | | `author` | string | Author (post author, video creator, etc.) | References include every source the platform found or read; cited sources come first. See [Working with Results](../../guides/results/#the-three-states-of-cited) for how each platform handles `cited`. ### AgentStep [Section titled “AgentStep”](#agentstep) Read the fields that match `type`: | Field | Type | Applies to `type` | Description | | --------- | -------------------- | ----------------- | --------------------------------------------------------------- | | `type` | string | — | `thinking`, `search` (web search), or `tool` (other tool calls) | | `content` | string | `thinking` | Thinking text | | `summary` | string | `thinking` | Thinking summary or the platform’s progress text | | `name` | string | `search`, `tool` | The platform’s tool name | | `queries` | string\[] | `search` | Search queries used in this step | | `results` | AgentSearchResult\[] | `search` | Search results returned; not necessarily cited in the answer | | `args` | string | `tool` | Raw call arguments | `AgentSearchResult`: `title`, `url`, `siteName` (display name of the site), `snippet`. ### MediaCard [Section titled “MediaCard”](#mediacard) | Field | Type | Description | | -------------- | ---------------- | ---------------------------------------------------------------------------------------------------------- | | `type` | string | `image`, `video`, `product`, `ranking`, or `other` | | `platformType` | string | The platform’s original card type name, for reference only; values vary by platform | | `anchor` | string | The card’s placeholder text in `rawContent`; absent when the platform doesn’t put placeholders in the text | | `items` | MediaCardItem\[] | Items in the card | `MediaCardItem`: | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------- | | `title` | string | Title or product name | | `url` | string | The page it links to (video, product, or post page) | | `imageUrl` | string | Cover or image URL — **usually signed and expires** | | `source` | string | Source site, store, or marketplace | | `author` | string | Author | | `duration` | string | Video length, as the platform shows it (such as `05:03`) | | `price` | string | Price, as the platform shows it | | `rating` | string | Rating or recommendation score, as the platform shows it | | `publishTime` | string | Publish time, same format as Reference | | `description` | string | The platform’s description or comment | ## Full example [Section titled “Full example”](#full-example) See [Retrieve a task](../get-task/#response) for a complete example of a successful task. # Authentication > The MaxAEO open API and MCP server both authenticate with a Bearer API key. This page covers the header format, what a key can access, what authentication failures look like, and best practices for storing and rotating keys. The open API and the MCP server accept exactly one form of authentication: an API key sent in an HTTP request header. There’s no OAuth and no cookie-based session. ## Header format [Section titled “Header format”](#header-format) ```http Authorization: Bearer gdev_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` * Send **exactly one** `Authorization` header per request: the scheme `Bearer` (case-insensitive), a single space, then the full key. * API keys start with `gdev_` and are 48 characters long. The console list shows the first 12 characters (for example, `gdev_Ab3dEfG`) so you can tell keys apart. * The key is accepted **only in the header**. Keys in URL query parameters or in the request body are ignored, and an undefined body field such as `api_key` makes the whole request fail with parameter error `1001`. ## Getting an API key [Section titled “Getting an API key”](#getting-an-api-key) Create one in the [Developer Console](https://maxaeo.ai/developers/) → **API Keys**. The full key is shown only once, when it’s created or rolled; the server stores only a hash of it, so it can’t be retrieved later. If you lose it, roll the key or create a new one. To create, disable, roll, or delete keys, see [Manage API Keys](../console/api-keys/). ## What a key can access [Section titled “What a key can access”](#what-a-key-can-access) An API key calls the API on behalf of **the user who created it**, within that user’s **personal account**: * **Billing**: all keys, plus Playground requests in the console, share the account’s credit balance and rate limits. * **Retrieving tasks**: any valid key can retrieve, by `task_id`, any task that user has submitted — including tasks submitted with other keys or from the console Playground. * **Listing tasks**: the [List Tasks](../api/list-tasks/) endpoint returns only tasks submitted with **the key making the request**. * **Team accounts**: not available yet; keys can only be created under a personal account. A key can do exactly what the open API offers: list platforms, submit tasks, retrieve tasks, and list tasks. It can’t manage keys, and it can’t access the brand-monitoring data in the MaxAEO product. ## Authentication errors [Section titled “Authentication errors”](#authentication-errors) The server re-validates the key on **every request**: the key must exist, be enabled, not be expired, and belong to a user and account that are still active. If any check fails, the open API returns: ```json { "ret": 1030, "msg": "{\"zh\":\"API Key 无效、已停用或已过期\",\"en\":\"The API key is invalid, disabled or expired.\"}" } ``` You also get `1030` when the `Authorization` header is missing, malformed, or sent more than once. For security reasons, `1030` doesn’t say which check failed, so work through this list: 1. Does your environment variable hold the complete key? (Common mistakes: copying only the prefix, or a trailing newline or space.) 2. Is the header written as `Authorization: Bearer ` — not `Bearer: ` or `Token `? 3. Check the key’s status on the console’s **API Keys** page. Disabled, expired, and deleted keys are all rejected, and after a roll the old key stops working when its grace period ends. ## What happens to in-flight tasks when a key stops working [Section titled “What happens to in-flight tasks when a key stops working”](#what-happens-to-in-flight-tasks-when-a-key-stops-working) After a key is disabled, deleted, or expires: * **New requests** with that key are rejected immediately (`1030`). * Tasks that were submitted but are still waiting locally (`queued`) and haven’t been dispatched yet will fail at dispatch with `error_code` `credential_invalid`. No credits are charged. * Tasks already dispatched to the platform finish normally, and you can still retrieve them with another valid key or in the console. ## Security best practices [Section titled “Security best practices”](#security-best-practices) * **Use keys on the server only.** Never put a key in browser code, mobile apps, mini-programs, browser extensions, or public repositories. Read it from an environment variable or a secrets manager on your server. * **Use separate keys per environment and project** — for example “Production”, “Staging”, and “Dashboard”. If one key leaks, you can disable or roll just that key without affecting anything else, and the console breaks down usage by key. * **Set an expiration for short-lived uses.** For one-off scripts or contractor access, choose a 30- or 90-day expiration so the key retires itself. * **Rotate regularly, without downtime.** Use **Roll key** to generate a new key and give the old one a grace period (1 hour, 24 hours, or 7 days). Deploy the new key first, then let the old one expire. * **Act immediately if you suspect a leak.** Roll the key in the console with the old key set to expire **Immediately**, or disable it, then check Request Logs for unexpected calls. * **Never log the full key.** If you need to identify a key in logs, the first 12 characters are enough. ## Related pages [Section titled “Related pages”](#related-pages) [Manage API Keys](../console/api-keys/)Create, disable, roll, and delete keys in the console. [Error Handling](../guides/errors/)Request-level error codes beyond 1030, and how to handle them. # Manage API Keys > Create, edit, disable, enable, roll, and delete API keys in the MaxAEO Developer Console — expiration options, the one-time display of the full key, the old key's grace period when rolling, the per-account key limit, and how each action affects in-flight requests and tasks. API keys live on the **API Keys** page of the [Developer Console](https://maxaeo.ai/developers/). It lists every key in your account that hasn’t been deleted, with each key’s status, credits used this month, and when it was last used. ## Create an API key [Section titled “Create an API key”](#create-an-api-key) 1. Click **Create API key** in the top-right corner. 2. Fill in: * **Name**: tells keys apart. Name it after an environment or project, such as “Production” or “Analytics dashboard”. Up to 128 characters. * **Expiration**: Never, 30 days, 90 days, 180 days, 1 year, or a custom date. **Expiration can’t be changed after creation** — to change it, roll the key or create a new one. 3. Click **Create**. A dialog shows the full key. **The full key is shown only this once**: copy it into your secrets store right away — once you close the dialog, you can’t view it again. 4. Optionally, use **Try it** in the dialog to list supported platforms with the new key and confirm it works (this costs no credits). Each account can keep up to 10 keys that haven’t been deleted (disabled and expired keys count). Once you hit the limit, delete keys you no longer use. The current limit is shown at the top of the page. ## The key list [Section titled “The key list”](#the-key-list) | Column | Description | | -------------------- | ------------------------------------------------------------------------------------------------------ | | Name | The name you set when creating or editing the key | | API key | The first 12 characters of the key, to tell keys apart; the full key is never shown again | | Status | **Active**, **Disabled**, or **Expired**; keys close to expiring show the number of days left | | This month (credits) | Credits charged for successful calls with this key in the current calendar month (China Standard Time) | | Last used | The last time this key passed authentication; keys that have never been used show “Never used” | **View request logs** in the actions menu jumps straight to this key’s requests. ## Edit [Section titled “Edit”](#edit) Only the **name** can be changed. The key itself and its expiration can’t. ## Disable and enable [Section titled “Disable and enable”](#disable-and-enable) * **Disable**: requests using the key are rejected immediately (`ret 1030`). You can re-enable it at any time. Useful when a key is temporarily unused, or to stop the bleeding while you investigate suspicious calls. * **Enable**: restores a disabled key. **Expired keys can’t be enabled** — create a new one instead. ## Roll a key [Section titled “Roll a key”](#roll-a-key) Rolling generates a **new key**. Use it for scheduled rotation or when you suspect a key has leaked: 1. Choose **Roll key** in the actions menu. 2. Pick when the **old key expires**: Immediately, in 1 hour, in 24 hours (default), or in 7 days. During the grace period both keys work, so you can deploy the new key first and let the old one expire on its own. 3. Confirm. A dialog shows the full new key — again, **only this once**. Rules for rolling: * The new key keeps the old key’s name and expiration date and appears as a new row in the list. The old key stays in the list: it becomes Expired when the grace period ends (or Disabled right away if you chose Immediately). * The old key stops working at whichever comes first: now plus the grace period, or its original expiration date. * Only **active, unexpired** keys can be rolled. Enable a disabled key first; create a new key to replace an expired one. * You can still roll once when you’re at the key limit; beyond that, delete old keys before rolling again. * Tasks submitted with the old key before the roll don’t appear in the new key’s [task list](../../api/list-tasks/), but you can still retrieve them by `task_id`. Check the impact before choosing Immediately With Immediately, every request still using the old key is rejected right away (`ret 1030`). Only choose it when you’re sure the key has leaked and need to cut it off now. ## Delete [Section titled “Delete”](#delete) Deletion **can’t be undone**. Requests using the key are rejected immediately (`ret 1030`). To pause a key temporarily, disable it instead. Tasks submitted with a deleted key are kept: Request Logs and Usage still show the key’s name and prefix, and you can retrieve those tasks by `task_id` with any other valid key. ## Effect on in-flight tasks [Section titled “Effect on in-flight tasks”](#effect-on-in-flight-tasks) After a key is disabled, deleted, expires, or reaches the end of its roll grace period: * Tasks submitted with that key that are still waiting (`queued`) and haven’t been dispatched fail at dispatch (`error_code` `credential_invalid`), and no credits are charged. * Tasks already dispatched to the platform finish normally. ## Security best practices [Section titled “Security best practices”](#security-best-practices) * Never put keys in browser code, mobile apps, mini-programs, or public repositories. Read them from environment variables on your server. * Create separate keys per environment and project, so if one leaks you can disable or roll it without affecting anything else. * Roll immediately if you suspect a leak; give the old key a grace period when you need a smooth handover. More in [Authentication](../../authentication/#security-best-practices). ## Related pages [Section titled “Related pages”](#related-pages) [Authentication](../../authentication/)Header format, what a key can access, and troubleshooting authentication errors. [Logs & Usage](../logs-usage/)Request details and credit usage broken down by key. # Logs & Usage > Use the MaxAEO Developer Console to inspect every request (status, duration, credits, errors, and the full response) and to track your credit balance, credits on hold, spending trends, usage broken down by platform and by source, and your account's limits. The Developer Console has two pages for keeping an eye on your calls: **Request Logs** for individual requests, and **Usage** for totals and your balance. ## Request Logs [Section titled “Request Logs”](#request-logs) **Request Logs** lists every task request in your account, including both API key calls and requests sent from the Playground. ### Filters [Section titled “Filters”](#filters) * **Time**: Today, Last 7 days, Last 30 days, or a custom date range. Date filters follow China Standard Time (UTC+8); times in the list are shown in your computer’s local time zone. The console shows the last 30 days of requests. * **Platform**, **Status**, and **Source** (each API key, or Playground). * **Search**: enter a task ID for an exact match, or text from the question. ### Columns [Section titled “Columns”](#columns) | Column | Description | | -------- | ----------------------------------------------------------------------------------------- | | Time | When the task was submitted | | Task ID | The open API’s `task_id` | | Request | Platform and question | | Status | Queued, Running, Succeeded, Failed, or Cancelled | | Duration | Time from submission to completion (including queue time) | | Credits | Credits charged if it succeeded, credits reserved while it’s running, 0 if it failed | | Source | The API key used (name and prefix), or **Playground**; deleted keys still show their name | ### Request details [Section titled “Request details”](#request-details) Click any row to open its details: * Request time, platform, exit country, source, duration, tier (P1 / P2), and credits; * The **Request** parameters; * The **Response**: **Preview** shows the answer and sources in readable form, **JSON** is the complete response, identical to what the open API returns; * For failed tasks, the **error code** and **details** — see [Error Handling](../../guides/errors/#task-failures-error_code) for what they mean. If the request is still running, the details pane tells you to refresh once it finishes. Click **Replay in Playground** to load the request’s parameters into the [Playground](../playground/) and send it again. ## Usage [Section titled “Usage”](#usage) **Usage** summarizes your credit balance and spending. Choose Last 7 days, Last 30 days, or This month; days follow China Standard Time (UTC+8), and **only successful requests are billed**. ### Key metrics [Section titled “Key metrics”](#key-metrics) | Metric | Description | | -------------- | --------------------------------------------------------------------------------------- | | Credit balance | Credits currently available; if some credits expire soon, the amount and date are shown | | On hold | Credits reserved by running tasks — charged or released once the tasks finish | | Credits used | Credits charged for successful tasks in the selected period, with a daily average | | Requests | Tasks submitted in the selected period, with the success rate | | Avg. duration | Average time from submission to completion for finished tasks (including queue time) | ### Trends and breakdowns [Section titled “Trends and breakdowns”](#trends-and-breakdowns) * **Daily usage**: credits, requests, and failures per day. * **By platform**: requests, success rate, credits, and average duration per platform — useful for judging which platforms give you the best value. * **By source**: how many credits each API key and the Playground used, and their share — useful for allocating costs by project or environment. ### Limits [Section titled “Limits”](#limits) The bottom of the page shows the limits currently in effect for your account: AI Answers concurrency, requests per minute, and the maximum number of API keys. See [Rate Limits & Quotas](../../guides/rate-limits/) for what they mean. Need more concurrency or credits? Contact our sales team. If developer API billing isn’t enabled yet, a notice at the top of the page tells you tasks can’t be submitted. ## Overview page [Section titled “Overview page”](#overview-page) The console’s home page, **Overview**, brings the essentials onto one screen: your progress through Get started (create a key → try the Playground → integrate), credit balance, requests today, credits used in the last 7 days, active API keys, the 7-day request trend, platform status, and your most recent requests. It’s a good place to start your daily check-in. ## Related pages [Section titled “Related pages”](#related-pages) [Billing & Credits](../../guides/billing/)How credits are reserved, charged, released, and when they expire. [Manage API Keys](../api-keys/)Split keys by environment or project to track costs by source. # Playground > Use the Playground in the MaxAEO Developer Console to ask AI platforms a question without writing code, preview the answer, inspect the full JSON response, and copy cURL, Python, or Node.js code for the current parameters. Playground requests always run and are billed as P1 Priority. The **Playground** lets you call the open API without writing any code: pick a platform, type a question, and send it to see the AI’s answer, its cited sources, and the full response structure. When you’re happy with the result, copy calling code for the same parameters straight into your project. ## Send a request [Section titled “Send a request”](#send-a-request) 1. Open the [Developer Console](https://maxaeo.ai/developers/) and go to **Playground**. 2. Fill in the parameters: * **Platform**: the AI platform to ask. A **Degraded** platform shows a warning that requests may be slow or fail occasionally; a platform under **Maintenance** isn’t accepting tasks, and submissions are rejected right away without being billed. * **Question**: what you want to ask the platform. You can also click **Try** to use a sample question. * **Country / region**: applies to Global platforms only — the platform is accessed from a network exit in that country, and answers can vary by region. Platforms in China are always accessed from mainland China. Defaults to **Auto**. 3. Check the **Estimated cost** shown on the page, then click **Send request** (or press ⌘ / Ctrl + Enter). 4. Wait for the result. The page walks through stages — **Submitted**, **Queued**, **Crawling**, **Done** — and shows how long you’ve been waiting. Crawls usually take tens of seconds to two minutes, plus queue time. ## Inspect the result [Section titled “Inspect the result”](#inspect-the-result) The response panel has three tabs: * **Preview**: the answer rendered close to how the platform shows it, the list of sources, and an **Open on platform** share link. Failed tasks show the error code and details. * **JSON**: the complete response JSON, with the same fields [Retrieve a Task](../../api/get-task/) returns from the open API — handy for checking your parsing code. * **Code**: calling code for the current parameters in cURL, Python, or Node.js. The API key is read from the `MAXAEO_API_KEY` environment variable, and the snippet covers the full flow: submit, then poll every 15 seconds until the task finishes. The top of the response panel also shows the **Task ID**, **Duration**, and **Cost** in credits. ## Billing and rate limits [Section titled “Billing and rate limits”](#billing-and-rate-limits) * Playground requests **always run as P1 Priority and are billed at the P1 price**. Failures are free. * They share your account’s credit balance and the concurrency and per-minute submission limits with your API calls. If your balance is too low or you’re over a limit, you’ll see a message right away and no task is created. * Playground requests are recorded in Request Logs with the source **Playground**. ## Replay from Request Logs [Section titled “Replay from Request Logs”](#replay-from-request-logs) In a request’s details in [Request Logs](../logs-usage/#request-logs), click **Replay in Playground** to load that request’s parameters into the Playground, tweak them, and send again. A replay creates a new task and is billed at P1 like any Playground request. ## Related pages [Section titled “Related pages”](#related-pages) [Quickstart](../../quickstart/)Go from the Playground to calling the API from code. [Billing & Credits](../../guides/billing/)P1 and P2 prices and how credits are charged. # FAQ > Common questions about integrating with the MaxAEO Developer Platform — tasks stuck in the queue, whether a timed-out submission gets billed twice, webhooks, calling from the browser, failures with HTTP 200, empty sources, citation markers in answers, thinking mode, degraded platforms, free credits, multi-turn conversations, and model selection. ## Tasks [Section titled “Tasks”](#tasks) ### My task has been `pending` or `queued` for a long time. Is that normal? [Section titled “My task has been pending or queued for a long time. Is that normal?”](#my-task-has-been-pending-or-queued-for-a-long-time-is-that-normal) Yes. The API is asynchronous and queue time has no upper bound: when a platform is busy or its accounts are temporarily unavailable, P2 tasks stay in the queue until it recovers. The server never marks a task as failed just because it has waited a long time. You can: * Keep polling about every 15 seconds; * Switch time-sensitive tasks to P1; * Check each platform’s `p50_duration_seconds` and status in the [platform list](../api/list-platforms/), and avoid platforms that are currently degraded. See [Task Lifecycle](../guides/task-lifecycle/). ### My submit request timed out. Will resubmitting charge me twice? [Section titled “My submit request timed out. Will resubmitting charge me twice?”](#my-submit-request-timed-out-will-resubmitting-charge-me-twice) It might. Every submit call creates a new task, and if both tasks succeed you’re charged for both. After a timeout, first look up the task with [List Tasks](../api/list-tasks/) by question text and time, and only resubmit once you’ve confirmed it wasn’t created. See [When a submission’s outcome is unknown](../guides/errors/#when-a-submissions-outcome-is-unknown). ### Are there webhooks? [Section titled “Are there webhooks?”](#are-there-webhooks) Not currently — you get results by polling. Checking unfinished tasks about every 15 seconds works well. ### Can I cancel a task I’ve submitted? [Section titled “Can I cancel a task I’ve submitted?”](#can-i-cancel-a-task-ive-submitted) The open API doesn’t offer a cancel endpoint today. You can simply stop polling a task you no longer need, but it will still be charged if it succeeds. ### I asked the same question twice and got different answers. Why? [Section titled “I asked the same question twice and got different answers. Why?”](#i-asked-the-same-question-twice-and-got-different-answers-why) That’s expected. AI answers are inherently non-deterministic — the wording, recommendations, and citations can change from one run to the next. Each submission is an independent, real request, and each result is stored separately. ### Why is `started_at` 0? [Section titled “Why is started\_at 0?”](#why-is-started_at-0) If a task went straight to its final result without us observing a “running” state, no start time is recorded. To measure duration, use `finished_at - created_at` (including queue time) or `raw_result.elapsedMs` (execution only). ### How long are task results kept? [Section titled “How long are task results kept?”](#how-long-are-task-results-kept) The console’s Request Logs cover the last 30 days. We recommend saving results to your own system as soon as you get them. ## Requests and errors [Section titled “Requests and errors”](#requests-and-errors) ### Why is the HTTP status 200 when the request failed? [Section titled “Why is the HTTP status 200 when the request failed?”](#why-is-the-http-status-200-when-the-request-failed) Business outcomes are reported in the `ret` field of the response body: `1` means success and anything else is an error, while the HTTP status is always 200. See [API Overview](../api/#response). ### I keep getting 1030. What should I check? [Section titled “I keep getting 1030. What should I check?”](#i-keep-getting-1030-what-should-i-check) `1030` means the API key is missing, invalid, disabled, or expired. Make sure your environment variable contains the complete key, the header is `Authorization: Bearer `, and the key is active in the console. See [Authentication](../authentication/#authentication-errors). ### I get “The request contains unsupported fields.” [Section titled “I get “The request contains unsupported fields.””](#i-get-the-request-contains-unsupported-fields) Your request body includes a field the endpoint doesn’t define. The most common cause is camelCase names like `taskId` or `waitSeconds` instead of `task_id` and `wait_seconds` — all open API request fields use snake\_case. ### Can I call the API directly from a web page or mobile app? [Section titled “Can I call the API directly from a web page or mobile app?”](#can-i-call-the-api-directly-from-a-web-page-or-mobile-app) Please don’t. The API key would ship with your client code and be visible to every user, and anyone could spend your credits with it. Call the open API from your server and pass the results to your frontend. ### Is there an official SDK? [Section titled “Is there an official SDK?”](#is-there-an-official-sdk) Not yet. There are only four endpoints, all “POST + JSON”, so a few lines with any HTTP client will do — see [Quickstart](../quickstart/#complete-example). ### Is there a test environment or sandbox? [Section titled “Is there a test environment or sandbox?”](#is-there-a-test-environment-or-sandbox) There’s no separate sandbox right now. To experiment, use the console’s [Playground](../console/playground/) or a small number of real requests — listing and retrieval endpoints are free, failed tasks aren’t billed, and the 1,000 free credits you get on first use work for testing too. ## Results [Section titled “Results”](#results) ### The task succeeded but `references` is empty. Is that right? [Section titled “The task succeeded but references is empty. Is that right?”](#the-task-succeeded-but-references-is-empty-is-that-right) Yes. For some questions the platform doesn’t search the web, so the answer has no sources. `succeeded` only means an answer was returned. ### What are `[citation:1]` or `【turn0search2】` in the answer? [Section titled “What are \[citation:1\] or 【turn0search2】 in the answer?”](#what-are-citation1-or-turn0search2-in-the-answer) They’re the platform’s own citation markers. We keep them exactly as-is in `answer` and `rawContent`. Clean them up before displaying if you need to, and use `references` and `cited` to work out what was cited. See [Working with Results](../guides/results/#answer-text). ### Sometimes `cited` is missing. Why? [Section titled “Sometimes cited is missing. Why?”](#sometimes-cited-is-missing-why) `cited` has three states: `true` means the answer text cites the source, `false` means the platform found it but didn’t cite it, and **a missing field** means the platform doesn’t distinguish — don’t treat it as `false`. ### A `url` in `references` isn’t a link. Why? [Section titled “A url in references isn’t a link. Why?”](#a-url-in-references-isnt-a-link-why) Some platforms return sources without a link, or with a platform-internal ID instead. Check that `url` starts with `http://` or `https://` before using it. ### Can I choose the model, or turn deep thinking or web search on or off? [Section titled “Can I choose the model, or turn deep thinking or web search on or off?”](#can-i-choose-the-model-or-turn-deep-thinking-or-web-search-on-or-off) You can turn on thinking: pass `"thinking": true` when submitting, and the platform answers in its thinking mode (such as ChatGPT’s thinking or DeepSeek’s DeepThink). It’s off by default and costs nothing extra. Some platforms have no thinking switch and run with it off; check `raw_result.thinking` in the result to see whether it was actually on. See [Thinking mode](../guides/platforms/#thinking-mode-thinking). You can’t choose the model or toggle web search; these depend on how each platform is crawled. You can tell whether the platform searched from `agentSteps` and `references` in the result. ### Can I ask a follow-up question based on a previous answer? [Section titled “Can I ask a follow-up question based on a previous answer?”](#can-i-ask-a-follow-up-question-based-on-a-previous-answer) No. Each task is a single, independent question. `conversationId` in `raw_result` is for reference only and can’t be used to continue a conversation. ## Platforms and billing [Section titled “Platforms and billing”](#platforms-and-billing) ### A platform shows as Degraded. Can I still use it? [Section titled “A platform shows as Degraded. Can I still use it?”](#a-platform-shows-as-degraded-can-i-still-use-it) Yes, you can submit. Degraded means failures were observed recently, so requests may be slower or fail occasionally — and failures are free. Platforms under Maintenance aren’t accepting tasks, and submissions return `1029` right away. See [Platform status](../guides/platforms/#platform-status). ### Are there free credits? [Section titled “Are there free credits?”](#are-there-free-credits) Yes. The first time you use the Developer Platform, 1,000 credits are added to your personal account automatically. They never expire, and each account gets them once. See [Getting credits](../guides/billing/#getting-credits). ### Am I charged for failed tasks? [Section titled “Am I charged for failed tasks?”](#am-i-charged-for-failed-tasks) No. Only `succeeded` tasks cost credits. `failed` and `canceled` tasks, as well as rejected submissions, are free, and credits reserved at submission are released. See [Billing & Credits](../guides/billing/). ### Is MCP billed the same way as the API? [Section titled “Is MCP billed the same way as the API?”](#is-mcp-billed-the-same-way-as-the-api) Yes. The MCP tools run on the open API and share the same account balance, prices, and rate limits. ### Can several people on a team share one account’s credits? [Section titled “Can several people on a team share one account’s credits?”](#can-several-people-on-a-team-share-one-accounts-credits) Team accounts aren’t available yet; API keys and credits belong to a personal account. Within a team, you can create several keys under one account (for example, one per project) and break down spending by source on the Usage page. ## Related pages [Section titled “Related pages”](#related-pages) [Error Handling](../guides/errors/)Every error code and how to retry. [Production Checklist](../guides/production-checklist/)What to check before going to production. # Billing & Credits > The MaxAEO Developer Platform charges credits per successful task — reserved at submission at the platform's price for the chosen tier, charged on success, and released on failure or cancellation. This page covers the 1,000 free credits on first use, finding prices, what the task's credits field means, what happens when credits run low, reconciliation, and estimating costs. The Developer Platform bills in **credits**: only tasks that successfully return an answer are charged. Failed tasks, canceled tasks, and rejected requests cost nothing. ## Billing rules [Section titled “Billing rules”](#billing-rules) * **You pay per successful task.** Each `succeeded` task is charged once; `failed` and `canceled` tasks are free. * **Prices depend on platform and tier.** Each platform has a P1 Priority price and a P2 Standard price; tasks submitted without `priority` are billed as P2. * **The price is locked at submission.** A task is billed at the price in effect **when it was submitted**; later price changes don’t affect tasks already submitted. * **Country targeting and [thinking mode](../platforms/#thinking-mode-thinking) cost nothing extra.** * **Reads are free.** List Platforms, Retrieve a Task, and List Tasks don’t consume credits. * **One balance per account.** All API keys, MCP calls, and the console Playground in the same account share a single credit balance. The Playground always bills at the P1 price. ## Getting credits [Section titled “Getting credits”](#getting-credits) * **1,000 free credits on first use.** The first time you use the Developer Platform — by opening the [Developer Console](https://maxaeo.ai/developers/) or calling any endpoint with an API key — 1,000 credits are added to your personal account automatically. They **never expire**, and you don’t need to create a key or submit a task first. Each account gets them once; using them up or replacing or deleting keys doesn’t grant them again. * **Top-ups:** contact sales for more credits. The free credits count toward your credit balance, shown on the console’s **Overview** and **Usage** pages. ## Finding prices [Section titled “Finding prices”](#finding-prices) Two ways: * Call [List Platforms](../../api/list-platforms/): `p1_credits` and `p2_credits` on each platform are the credits charged per successful call for each tier. * Open the **Pricing** page in the [Developer Console](https://maxaeo.ai/developers/) for a price table by platform. Prices follow each platform’s crawling cost and vary significantly between platforms. In code, use the API response rather than hardcoding them. ## How credits move [Section titled “How credits move”](#how-credits-move) Credits are **reserved** at submission and settled when the task reaches a final status. The task object’s `credits` field reflects this: | Task status | `credits` value | Effect on your balance | | --------------------------------------------- | ---------------------------------- | ------------------------------------------------------------ | | `queued`, `dispatching`, `pending`, `running` | The price at submission (reserved) | Reserved, so your available balance goes down by that amount | | `succeeded` | The price at submission | Charged | | `failed`, `canceled` | `0` | The reserved credits are released | **Available balance = valid credit balance − credits reserved by in-progress tasks.** If your available balance is lower than the price of a submission, the request returns `1023` right away — no task is created, and your balance never goes into overdraft. Your current balance and credits on hold (reserved) are shown on the console’s **Usage** page. Until billing is enabled, [List Platforms](../../api/list-platforms/) returns an empty list and submitting a task returns `1022`. ## Reconciliation [Section titled “Reconciliation”](#reconciliation) * **Per task:** the task object’s `credits` is what the task was charged (succeeded) or has reserved (in progress). The tier it actually ran on is `raw_result.priority`. * **By key, platform, and day:** the console **Usage** page breaks down credits and requests by day, by platform, and by source (each API key / the Playground), with days split on Beijing time (UTC+8). The **API Keys** page shows each key’s spend this month. * **On your own:** page through [List Tasks](../../api/list-tasks/) by time range and sum `credits` over tasks whose `status` is `succeeded`. Note that List Tasks only returns tasks submitted with the current key. ## Estimating costs [Section titled “Estimating costs”](#estimating-costs) Estimated cost = number of questions × the sum of the per-platform prices you send each question to (only successful tasks are charged). For example: 50 questions a day, each sent to ChatGPT, Perplexity, and Gemini on P2. Assuming P2 prices of 10, 10, and 8 credits respectively (check the API for actual prices), that’s at most 50 × (10 + 10 + 8) = 1,400 credits a day, with failed tasks not counted. Common ways to keep costs down: * Use P2 for batch and scheduled work. * Compare the value of each platform’s answers on a small set of questions first, then send to only the platforms you actually need. * Avoid submitting the same question twice (see [Avoiding duplicate submissions](../task-lifecycle/#avoiding-duplicate-submissions)). ## Related pages [Section titled “Related pages”](#related-pages) [Rate Limits & Quotas](../rate-limits/)Concurrency, rate, and API key limits. [Logs & Usage](../../console/logs-usage/)See your balance, spend, and per-request details in the console. # Error Handling > The two layers of errors in the MaxAEO API — request-level ret codes (1001, 1022–1031, -1) and task-level error_code values (crawl_failed, credential_invalid, and more). What each one means, whether to retry, and how to handle it, with code for safe retries and for submissions whose outcome is unknown. Errors in the API come in two layers, and you handle them very differently: | Layer | Where to look | Meaning | Examples | | ----------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------- | | **Request** | `ret` in the response body is not `1` | This HTTP request wasn’t accepted — **no task was created and nothing was charged** | Invalid parameters, insufficient credits, invalid API key | | **Task** | The task’s `status` is `failed` or `canceled`, with the reason in `error_code` | The task was created but didn’t produce an answer — **nothing is charged** | Platform crawl failed, API key invalidated before dispatch | ## Request errors [Section titled “Request errors”](#request-errors) ### Response format [Section titled “Response format”](#response-format) When a request fails, the HTTP status code is still `200`, and the body looks like this: ```json { "ret": 1023, "msg": "{\"zh\":\"点数余额不足,请充值后重试\",\"en\":\"Insufficient credits. Please top up and try again.\"}" } ``` * **Always branch on `ret` in code.** Message wording may change, so don’t build logic on it. * `msg` is a **JSON string**. Parsing it gives you `{"zh": "…", "en": "…"}` — the message in Chinese and English — which you can show to your users. To be safe, fall back to the raw `msg` if parsing fails. * The message for invalid parameters (`1001`) names the offending field, for example “Invalid parameters: query must be at most 4096 characters”. ### Error codes [Section titled “Error codes”](#error-codes) | `ret` | Meaning | Can occur on | Retry? | What to do | | ------ | --------------------------------------------------------------------------------------------------------------------------- | --------------- | --------------------- | ----------------------------------------------------------------------------------------------------- | | `1` | Success | All | — | — | | `1001` | Invalid parameters: the body isn’t a single JSON object, exceeds 64 KiB, contains undefined fields, or has an invalid value | All | No | Fix the request as the message says. Field names are snake\_case, e.g. `task_id`, `wait_seconds` | | `1022` | Developer API billing isn’t enabled | Submit a Task | No | Contact sales to enable it | | `1023` | Insufficient credits | Submit a Task | After topping up | Top up, or switch to a cheaper platform / P2 | | `1024` | Too many tasks waiting for dispatch or running | Submit a Task | Yes, later | Wait for some tasks to finish, then submit again. See [Rate Limits & Quotas](../rate-limits/) | | `1025` | Too many submissions | Submit a Task | Yes, later | Slow down and retry with backoff | | `1029` | Platform unsupported or under maintenance | Submit a Task | With another platform | Fetch the platform list again and pick an available platform | | `1030` | API key missing, invalid, disabled, or expired | All | No | Check your key — see [Authentication errors](../../authentication/#authentication-errors) | | `1031` | Task not found | Retrieve a Task | No | Check the `task_id`; you can only retrieve tasks under your own account | | `1003` | No permission (account permission issue, e.g. ownership of the key’s account was transferred) | All | No | Rare — please contact us | | `-1` | Service temporarily unavailable | All | Yes, with backoff | Retry with exponential backoff. **On submit, first confirm whether the task was created** — see below | Request errors (except `-1`) all happen before a task is created: **a rejected submission creates no task and charges no credits**. ### Non-200 HTTP status codes [Section titled “Non-200 HTTP status codes”](#non-200-http-status-codes) `405` (the method isn’t `POST`) and `404` (wrong path) mean the request never reached the business logic, so **no task was created** — fix your code and send it again. Otherwise, if you get a non-200 status such as `502` or `504`, or the connection times out or drops, something went wrong on the network path and you **can’t tell whether the server processed the request**: * Read endpoints (List Platforms, Retrieve a Task, List Tasks): simply retry. * Submit a Task: follow the steps in “When a submission’s outcome is unknown” below before deciding whether to submit again. ## Task failures: `error_code` [Section titled “Task failures: error\_code”](#task-failures-error_code) When a task’s `status` is `failed` or `canceled`, `error_code` explains why. **Failed and canceled tasks are never charged**; the reserved credits are released. | `error_code` | `status` | Meaning | What to do | | ---------------------------- | ---------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `crawl_failed` | `failed` | The platform didn’t return a usable answer (timeout, platform error, etc.) | Retry later or switch platforms; if timing isn’t critical, use P2, which waits in the queue while the platform is unavailable | | `credential_invalid` | `failed` | The API key that submitted the task was disabled, deleted, or expired before dispatch | Resubmit with a valid key | | `crawler_rejected` | `failed` | The platform isn’t supported or has been retired | Fetch the platform list again and switch platforms | | `crawler_internal_error` | `failed` | The crawler hit an internal error and didn’t accept the task | Retry later | | `crawler_submit_failed` | `failed` | The crawler didn’t accept the task | Retry later | | `crawler_invalid_request` | `failed` | The crawler rejected the request parameters | Check the query and `country`, then resubmit | | `crawler_method_unavailable` | `failed` | The crawler hasn’t enabled raw-result crawling (a server-side configuration issue) | Please contact us | | `crawler_auth_failed` | `failed` | A server-side configuration issue | Please contact us | | `crawl_canceled` | `canceled` | The task was canceled | Resubmit if you still need it | These two values appear on **unfinished** tasks and don’t indicate failure: | `error_code` | `status` | Meaning | | --------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------- | | `dispatch_uncertain` | `dispatching` | The dispatch outcome is being confirmed. The server confirms it on its own and never dispatches twice — keep polling | | `crawler_query_unavailable` | Unfinished | Progress temporarily can’t be synced. Keep polling; contact us if it doesn’t change for a long time | Decide success from status only A non-empty `error_code` doesn’t mean failure (see `dispatching` above). Use `status` to tell whether a task has finished and whether it succeeded; use `error_code` only to branch after a failure. Treat any `error_code` not listed here as “task failed, not charged”, and log its `error_message`. `error_message` uses the same format as `msg` on request errors: a JSON **string** that parses to `{"zh": "…", "en": "…"}`. Log it, or show the message in your user’s language; if parsing fails, use the raw string. Don’t build logic on the wording — branch on `error_code`. `raw_result.error` (and `error` inside `result_json`) use the same format. ## Retry strategy [Section titled “Retry strategy”](#retry-strategy) | Situation | What to do | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | | `-1`, network errors, non-200 status (read endpoints) | Retry with exponential backoff, e.g. 2, 4, 8 seconds…, a few times at most | | `1024`, `1025` | No task was created, so retrying after a wait is safe; also lower your concurrency and submission rate | | `-1`, timeout, dropped connection (Submit a Task) | **Don’t retry right away** — first confirm whether the task was created (see the next section) | | `1001`, `1003`, `1022`, `1023`, `1029`, `1030`, `1031` | Don’t retry automatically; fix the problem first | | Task `failed` / `canceled` | Resubmitting creates a new task, charged only if it succeeds; for `crawl_failed`, wait a while or switch platforms | ### When a submission’s outcome is unknown [Section titled “When a submission’s outcome is unknown”](#when-a-submissions-outcome-is-unknown) If a submit request times out, the connection drops, or it returns `-1`, the task may already have been created. Retrying blindly creates a second task — and if both succeed, you’re charged twice. Do this instead: 1. Call [List Tasks](../../api/list-tasks/) with the question text as `keyword` (up to 256 characters, matched as “contains” — for a longer question, pass its first 256 characters) and `start_time` set to just before the submission. 2. Found it: keep polling that `task_id`. 3. Not found: submit again. * Python ```python # BASE_URL and HEADERS are defined as in the Quickstart import json import time import requests RETRYABLE = {-1, 1024, 1025} class MaxAEOError(Exception): def __init__(self, ret, msg): self.ret = ret try: self.message = json.loads(msg).get("en") or msg except (TypeError, ValueError, AttributeError): self.message = msg super().__init__(f"ret={ret}: {self.message}") def post(path, body): resp = requests.post(BASE_URL + path, headers=HEADERS, json=body, timeout=30) resp.raise_for_status() data = resp.json() if data["ret"] != 1: raise MaxAEOError(data["ret"], data.get("msg", "")) return data["data"]["ent"] def get_with_retry(path, body, retries=4): """Read endpoints: safe to retry.""" for attempt in range(retries + 1): try: return post(path, body) except (requests.RequestException, MaxAEOError) as e: retryable = not isinstance(e, MaxAEOError) or e.ret in RETRYABLE if not retryable or attempt == retries: raise time.sleep(2 ** (attempt + 1)) def submit_safely(platform, query): """Submit a task, resubmitting only once we've confirmed none was created.""" started = int(time.time()) - 60 # allow for clock skew for attempt in range(4): try: return post("/task/submit", {"platform": platform, "query": query}) except MaxAEOError as e: if e.ret in (1024, 1025): # no task was created; wait and try again time.sleep(15 * (attempt + 1)) continue if e.ret != -1: raise except requests.RequestException: pass # Outcome unknown: look for the submission we just made found = get_with_retry("/task/key/list", { "keyword": query.strip()[:256], "platform": platform, "start_time": started, "page_size": 5, }) if found["items"]: return get_with_retry("/task/get", {"task_id": found["items"][0]["task_id"]}) raise RuntimeError("Submission failed after several attempts; try again later") ``` * Node.js ```js // BASE_URL and headers are defined as in the Quickstart const RETRYABLE = new Set([-1, 1024, 1025]); class MaxAEOError extends Error { constructor(ret, msg) { let message = msg; try { message = JSON.parse(msg).en || msg; } catch {} super(`ret=${ret}: ${message}`); this.ret = ret; } } const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function post(path, body) { const resp = await fetch(BASE_URL + path, { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(30_000), }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); if (data.ret !== 1) throw new MaxAEOError(data.ret, data.msg ?? ""); return data.data.ent; } // Read endpoints: safe to retry async function getWithRetry(path, body, retries = 4) { for (let attempt = 0; ; attempt++) { try { return await post(path, body); } catch (e) { const retryable = !(e instanceof MaxAEOError) || RETRYABLE.has(e.ret); if (!retryable || attempt === retries) throw e; await sleep(2 ** (attempt + 1) * 1000); } } } // Submit a task, resubmitting only once we've confirmed none was created async function submitSafely(platform, query) { const started = Math.floor(Date.now() / 1000) - 60; // allow for clock skew for (let attempt = 0; attempt < 4; attempt++) { try { return await post("/task/submit", { platform, query }); } catch (e) { if (e instanceof MaxAEOError && (e.ret === 1024 || e.ret === 1025)) { await sleep(15_000 * (attempt + 1)); // no task was created; wait and try again continue; } if (e instanceof MaxAEOError && e.ret !== -1) throw e; } // Outcome unknown: look for the submission we just made const found = await getWithRetry("/task/key/list", { keyword: Array.from(query.trim()).slice(0, 256).join(""), platform, start_time: started, page_size: 5, }); if (found.items.length > 0) { return getWithRetry("/task/get", { task_id: found.items[0].task_id }); } } throw new Error("Submission failed after several attempts; try again later"); } ``` ## Contacting us about an issue [Section titled “Contacting us about an issue”](#contacting-us-about-an-issue) When you need our help investigating, please include: * the `task_id` (for task-related issues); * the request time (to the minute) and the endpoint you called; * the request trace ID from the `ipalfish-trace-id` response header, if present; * the full `ret` and `msg`, or `error_code` and `error_message`. **Never** send your full API key to anyone, including our staff. ## Related pages [Section titled “Related pages”](#related-pages) [Task Lifecycle](../task-lifecycle/)Status transitions, polling frequency, and avoiding duplicate submissions. [Rate Limits & Quotas](../rate-limits/)The concurrency and rate limits behind 1024 and 1025. # Platforms, Priority & Country > The AI platform IDs and regions supported by the MaxAEO API, what the platform statuses (operational, degraded, maintenance) mean, how the P1 Priority and P2 Standard tiers differ and how to choose between them, how the country parameter affects the crawl's exit location, and which platforms support the thinking mode. When you submit a task, `platform` picks which AI platform to ask, `priority` picks the queue tier and price, `country` picks the network exit for Global platforms, and `thinking` asks for the platform’s thinking mode. This page covers each in turn. ## Supported platforms [Section titled “Supported platforms”](#supported-platforms) Use the **platform ID** from the table below as the `platform` parameter (case-insensitive). | Platform ID | Platform | Region | | -------------------- | -------------------- | ------ | | `chatgpt` | ChatGPT | Global | | `gemini` | Gemini | Global | | `claude` | Claude | Global | | `perplexity` | Perplexity | Global | | `copilot` | Copilot | Global | | `grok` | Grok | Global | | `google-ai-mode` | Google AI Mode | Global | | `google-ai-overview` | Google AI Overview | Global | | `doubao` | Doubao | China | | `deepseek` | DeepSeek | China | | `kimi` | Kimi | China | | `yuanbao` | Tencent Yuanbao | China | | `qianwen` | Qwen | China | | `wenxin` | ERNIE Bot | China | | `zhipu` | Zhipu ChatGLM | China | | `xunfei` | iFlytek Spark | China | | `metaso` | Metaso | China | | `xiaohongshu` | Xiaohongshu Diandian | China | In API responses, `region` is `overseas` for Global platforms and `domestic` for China platforms. The live API response is the source of truth Platforms are added and retired over time, so this table is only a snapshot at the time of writing. In code, call [List Platforms](../../api/list-platforms/) to get the platforms available right now — don’t hardcode the list. Platforms without a configured price, or that have been retired, don’t appear in the response; platforms under maintenance do appear, with `platform_status` set to `maintenance`. Submitting to either kind returns `1029`. ## Platform status [Section titled “Platform status”](#platform-status) Each item returned by [List Platforms](../../api/list-platforms/) includes `platform_status` and stats for the last 24 hours: | `platform_status` | Shown in the console as | Meaning | Can you submit? | | ----------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | `normal` | Operational | Not flagged as degraded or under maintenance, and the most recent task to finish in the last 24 hours didn’t fail | Yes | | `degraded` | Degraded | Recent failures (the latest failure is no earlier than the latest success), or manually flagged as degraded; tasks may be slower or fail occasionally | Yes; failed tasks are free | | `maintenance` | Maintenance | Not accepting tasks | No — submissions return `1029` right away and no task is created | How to read the stats fields: * `sample_count`: the number of tasks on this platform that reached a final status in the last 24 hours, across all callers (excluding non-platform causes such as invalid API keys, rejected parameters, and cancellations). * `has_stats`: `true` once the sample count reaches the statistical threshold. Only then are `success_rate` (0–1) and `p50_duration_seconds` meaningful. When it’s `false`, both are `0` — don’t display that as a “0% success rate”. * **`normal` with a `sample_count` of 0** means nobody has called this platform in the last 24 hours. It only tells you that no failures were observed, not that the platform has been verified to work. The console shows this case as “No samples”. * Stats are recomputed every few minutes; `stats_updated_at` is when they were last computed. They don’t distinguish tasks with and without thinking mode. ## Priority: P1 vs. P2 [Section titled “Priority: P1 vs. P2”](#priority-p1-vs-p2) `priority` has two tiers with different prices (see [Billing & Credits](../billing/)): | | P1 Priority (`priority: 1`) | P2 Standard (`priority: 2`, default) | | -------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------- | | Queue order | Dequeued before P2 | Picked up after P1 | | Capacity | Can use all of it | Uses at most half, leaving the rest for P1 | | When platform accounts are temporarily unavailable | Doesn’t wait long; may end quickly as a failure (free) | Stays in the queue until they recover, remaining `pending` | | Retries | Retried within the same run | A retryable failure goes back to the queue and runs once more (`running` → `pending`) | | Best for | Time-sensitive work: a user is waiting, live dashboards | Work that isn’t time-sensitive: daily batch monitoring, offline analysis | | Price | Higher | Lower | * Omitting `priority` or passing `0` runs and bills the task as **P2**; any value other than `1` or `2` returns `1001`. * The console [Playground](../../console/playground/) always runs and bills as P1. * The tier a task actually ran on is `raw_result.priority` in the result — use it for reconciliation. * **Neither tier guarantees a queue time.** P1 just goes first; when the platform as a whole is busy, it still has to wait. **How to choose:** use P2 for scheduled batch monitoring — it’s cheaper, and a slightly later result doesn’t matter. Use P1 when someone is waiting on the result in a UI, or when a downstream step is blocked on it. ## Exit country: `country` [Section titled “Exit country: country”](#exit-country-country) `country` is an optional two-letter country code (ISO 3166-1 alpha-2, such as `US`, `JP`, `DE`; case-insensitive). * **Global platforms:** sets which country’s network exit is used to reach the platform. The AI’s answer — language, recommended brands, cited sources — can vary by region. If omitted, the server decides; currently that’s a US exit. Global platforms don’t support a mainland China exit — passing `CN` also uses the US exit. * **China platforms:** always accessed from mainland China; `country` doesn’t change the exit. * A dedicated exit isn’t guaranteed for every country; if none is configured for the requested country, the default exit is used. The `country` in the response is the value you requested (uppercased), not a verified actual exit. * Specifying a country **costs nothing extra**. In most cases you don’t need `country`. Set it only when you’re researching something like “what answer does a user in Germany get from ChatGPT?”. ## Thinking mode: `thinking` [Section titled “Thinking mode: thinking”](#thinking-mode-thinking) `thinking` is an optional boolean that defaults to `false` (off), in which case the platform answers in its fast mode. Pass `true` to request the platform’s thinking mode — the default strength of modes like ChatGPT’s thinking or DeepSeek’s DeepThink on the platform’s website, not higher tiers such as deep research. * **It’s a request, not a guarantee.** Platforms without a thinking switch run with thinking off even if you pass `true` — no error, and billing is unchanged. Whether thinking was actually on is `raw_result.thinking` in the result: `true` means it was on; the field is absent when it was off, when the platform has no switch, or when the task failed. The top-level `thinking` on the task object only echoes what you requested. * **Noticeably slower.** The platform reasons before answering, so tasks usually take noticeably longer than with thinking off, possibly over two minutes. With `wait_seconds`, the call is also more likely to return before the task finishes, so keep polling. * **No extra cost.** Tasks are charged the same price for the same platform and tier (P1 / P2) — see [Billing & Credits](../billing/). * It must be a JSON boolean: passing the string `"true"` returns `1001`. Per-platform support (October 2026): | Platform | Off (default) | On (`"thinking": true`) | | --------------- | ------------------------------ | -------------------------------- | | ChatGPT | Thinking off | Thinking on | | DeepSeek | DeepThink off, with web search | DeepThink on, with web search | | Doubao | Fast | Expert | | Kimi | “Standard” thinking strength | “Advanced” thinking strength | | Tencent Yuanbao | Quick answer | Expert mode (website default) | | Qwen | Fast | Thinking & research | | Zhipu ChatGLM | Fast | Deep | | Metaso | Fastest | Quick thinking (website default) | | iFlytek Spark | Fast | Reasoning | Gemini, Claude, Perplexity, Copilot, Grok, Google AI Mode, Google AI Overview, ERNIE Bot, and Xiaohongshu Diandian have no thinking switch; passing `true` still runs them with thinking off. Rely on raw\_result.thinking in the result This table reflects October 2026 and can change whenever a platform updates, and [List Platforms](../../api/list-platforms/) doesn’t report whether a platform supports thinking. In code, read `raw_result.thinking` on each task instead of hardcoding this table. Tencent Yuanbao and Metaso use their thinking modes by default on their websites, and Tencent Yuanbao’s two modes use different models. A task that requested thinking and actually ran with it looks like this (excerpt): ```json { "thinking": true, "task_id": "task_…", "platform": "chatgpt", "status": "succeeded", "…": "…", "raw_result": { "…": "…", "priority": 2, "thinking": true } } ``` With thinking off, `thinking` doesn’t appear in `raw_result`. See [Working with Results](../results/#execution-steps-agentsteps) for how to read thinking steps. ## How results differ between platforms [Section titled “How results differ between platforms”](#how-results-differ-between-platforms) Platforms return content of varying richness: some expose their thinking and search steps, some embed video or product cards in the answer, and some don’t distinguish whether a source was cited in the answer text. See [Working with Results](../results/) for the specifics and how to parse them. ## Related pages [Section titled “Related pages”](#related-pages) [List Platforms](../../api/list-platforms/)Request parameters, response fields, and examples. [Billing & Credits](../billing/)Prices for both tiers, and how credits are reserved and charged. # Production Checklist > A checklist to work through before taking your MaxAEO API integration to production, covering API key security, asynchronous tasks and retries, rate limits, cost control, result parsing, and monitoring and alerting. Before moving from a script to production, go through this checklist item by item. Each item links to the details. ## Security [Section titled “Security”](#security) * [ ] API keys live only in server-side environment variables or a secrets manager, and never appear in frontend code, apps, code repositories, or logs. See [Authentication](../../authentication/#security-best-practices). * [ ] Production and testing use different keys, and keys for short-lived purposes have an expiry date. * [ ] Your team knows what to do if a key leaks: roll it in the console and choose “Immediately”, or disable it. See [Manage API Keys](../../console/api-keys/). * [ ] You have a key rotation process: when rolling a key, give the old key a grace period, and let it expire only after the new key is deployed. ## Asynchronous tasks and reliability [Section titled “Asynchronous tasks and reliability”](#asynchronous-tasks-and-reliability) * [ ] You persist the `task_id` right after submitting, and record the submission time and parameters in your own system. * [ ] You poll about every 15 seconds, with a scheduled loop that checks unfinished tasks in batches instead of blocking per task. See [Task Lifecycle](../task-lifecycle/#polling-how-often-to-check). * [ ] A business timeout doesn’t mean failure: after a timeout you keep the `task_id` and can still fetch the result later. * [ ] You decide success or failure from `status` only, and never treat a non-empty `error_code` as failure. * [ ] When a submit request times out, disconnects, or returns `-1`, you check List Tasks first instead of **blindly resubmitting**. See [Error Handling](../errors/#when-a-submissions-outcome-is-unknown). * [ ] If you use `wait_seconds`, your HTTP client timeout is longer than `wait_seconds`, and you handle a non-final task coming back. ## Requests and error handling [Section titled “Requests and error handling”](#requests-and-error-handling) * [ ] You decide whether a request succeeded from `ret` in the response body, not the HTTP status code — while also handling non-200 statuses and network errors. * [ ] You parse `msg` (a JSON string) into a displayable message, falling back to the raw text if parsing fails. * [ ] Request bodies contain only fields defined by the endpoint, in snake\_case (`task_id`, `wait_seconds`). * [ ] Your response parsing ignores unknown fields, so new fields added later don’t break it. * [ ] `-1`, `1024`, and `1025` are retried with backoff; `1001`, `1003`, `1022`, `1023`, `1029`, `1030`, and `1031` aren’t retried automatically but raise an alert or a message. * [ ] An unknown `error_code` is treated as “task failed, not charged”, and its `error_message` is logged. ## Rate limits and cost [Section titled “Rate limits and cost”](#rate-limits-and-cost) * [ ] Bulk submissions are throttled on the client to stay under the per-minute limit, with backoff on `1024` / `1025`. See [Rate Limits & Quotas](../rate-limits/). * [ ] The platform list is fetched from the API, not hardcoded, and platforms in `maintenance` are skipped. * [ ] You pick the tier by use case: P2 for batch and scheduled work, P1 for time-sensitive work. See [Platforms, Priority & Country](../platforms/#priority-p1-vs-p2). * [ ] You get an alert when your balance drops below a threshold (the console **Usage** page shows balance and expiry), so tasks don’t stall on `1023`. See [Billing & Credits](../billing/). ## Result parsing [Section titled “Result parsing”](#result-parsing) * [ ] You check that `raw_result` exists before reading it, and fill in defaults for omitted zero-value fields. * [ ] `elapsedMs` is parsed from a string into a number. * [ ] `cited` is handled as three states, with a missing field not treated as `false`. See [Working with Results](../results/#the-three-states-of-cited). * [ ] When you use thinking mode, you check `raw_result.thinking` to see whether it was actually on, rather than trusting the `thinking` you submitted. See [Thinking mode](../platforms/#thinking-mode-thinking). * [ ] `url` is checked to be an `http(s)` link before use. * [ ] Images you need long-term are copied to your own storage (`imageUrl` expires). * [ ] Platform citation markers in the answer text are cleaned up as needed before showing it to users. ## Observability [Section titled “Observability”](#observability) * [ ] For each request you log the endpoint, `ret`, latency, `task_id`, and the `ipalfish-trace-id` response header (if present), to help with troubleshooting. * [ ] You monitor the task failure rate and the distribution of `error_code` values per platform; if a platform keeps returning `crawl_failed`, consider switching temporarily. * [ ] You regularly reconcile against the console **Request Logs** and **Usage** pages. ## Related pages [Section titled “Related pages”](#related-pages) [API Overview](../../api/)Conventions shared by every request and response. [FAQ](../../faq/)Common questions during integration. # Rate Limits & Quotas > Limits on the MaxAEO API — concurrent tasks, submissions per minute, number of API keys, request body size, and parameter lengths — how each is counted, which error code you get when you exceed it, and how to control concurrency and rate on the client side. To share platform capacity fairly across accounts, the API applies concurrency and rate limits to **task submissions**, and size limits to requests themselves. ## Limits at a glance [Section titled “Limits at a glance”](#limits-at-a-glance) | Limit | Default | Counted over | When exceeded | | ---------------------- | ---------------- | ----------------------------------------------------------------------- | --------------------------------- | | Concurrent tasks | 5 | Tasks in your account with status `queued`, `dispatching`, or `running` | Submit returns `1024` | | Submissions per minute | 120 | Tasks submitted in your account in the last 60 seconds (rolling window) | Submit returns `1025` | | API keys | 10 | Non-deleted keys in your account (including disabled and expired ones) | The console won’t create new keys | | Request body size | 64 KiB | A single request | Returns `1001` | | `query` length | 4,096 characters | A single task | Returns `1001` | | `wait_seconds` | 0–120 seconds | A single request | Returns `1001` | | List Tasks `page_size` | Max 100 | A single request | Returns `1001` | * The concurrency, requests-per-minute, and API key limits currently in effect for your account are shown under **Usage → Limits** in the console. Contact sales if you need higher limits. * Limits apply **per account**: every API key, MCP call, and Playground request in the same account shares them. * **Only task submissions count toward concurrency and rate.** List Platforms, Retrieve a Task, and List Tasks don’t use up either limit, but please keep polling reasonable (about every 15 seconds per task). * A submission rejected by a limit **creates no task** and costs no credits, so retrying after a wait is safe. ## How concurrency is counted [Section titled “How concurrency is counted”](#how-concurrency-is-counted) Concurrency only counts tasks in `queued`, `dispatching`, and `running` — **not `pending`** (dispatched and waiting in the platform queue). In other words: * Once a submitted task is dispatched and moves to `pending`, it no longer counts against your concurrency limit. * Large numbers of P2 tasks sitting in `pending` while a platform is busy won’t exhaust your concurrency. * What actually counts are tasks that were just submitted but not yet dispatched, and tasks that are running. ## Controlling your rate on the client [Section titled “Controlling your rate on the client”](#controlling-your-rate-on-the-client) When submitting in bulk, it’s better to pace yourself on the client than to wait for the server to reject you: * **Throttle:** leave a gap between submissions so you stay under the per-minute limit (for example, at most one submission every 0.6 seconds). * **Back off on `1024` / `1025`:** pause (starting at 15 seconds and doubling each time), then submit the same task again. * **Separate submitting from polling:** one queue handles submissions; a separate scheduled loop checks unfinished tasks in batches. - Python ```python # Submit a batch of questions one by one: throttle proactively, back off on rate limits import time SUBMIT_INTERVAL = 0.6 # seconds, about 100 per minute def submit_batch(platform, queries): task_ids = [] for query in queries: delay = 15 while True: try: task = post("/task/submit", {"platform": platform, "query": query}) task_ids.append(task["task_id"]) break except MaxAEOError as e: if e.ret not in (1024, 1025): raise time.sleep(delay) # no task was created; wait, then retry the same question delay = min(delay * 2, 120) time.sleep(SUBMIT_INTERVAL) return task_ids ``` - Node.js ```js // Submit a batch of questions one by one: throttle proactively, back off on rate limits const SUBMIT_INTERVAL = 600; // milliseconds, about 100 per minute async function submitBatch(platform, queries) { const taskIds = []; for (const query of queries) { let delay = 15_000; for (;;) { try { const task = await post("/task/submit", { platform, query }); taskIds.push(task.task_id); break; } catch (e) { if (!(e instanceof MaxAEOError) || (e.ret !== 1024 && e.ret !== 1025)) throw e; await sleep(delay); // no task was created; wait, then retry the same question delay = Math.min(delay * 2, 120_000); } } await sleep(SUBMIT_INTERVAL); } return taskIds; } ``` `post`, `MaxAEOError`, and `sleep` are defined in [Error Handling](../errors/#when-a-submissions-outcome-is-unknown). In production, also follow that page’s steps to confirm whether a task was created when a submission times out or returns `-1`. ## Related pages [Section titled “Related pages”](#related-pages) [Error Handling](../errors/)How to handle 1024, 1025, and other error codes, and when to retry. [Billing & Credits](../billing/)Beyond rate limits, insufficient credits return 1023. # Working with Results > How to read an AI answer once a task succeeds — how answer, result_json, and raw_result relate; references and the three states of cited; platform citation markers in the answer text; agentSteps, mediaCards, and share links; what to expect from Global platforms; and sample code to check whether your site was cited. Once a task reaches `succeeded`, the answer and its sources are in the task object. This page covers how to read them and where the common pitfalls are. For the full field definitions, see [The Task Object](../../api/task-object/). ## One result, three shapes [Section titled “One result, three shapes”](#one-result-three-shapes) | Field | Contents | When to use it | | ------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | `answer` | The answer text (Markdown), identical to `raw_result.rawContent` | When you only need the text | | `raw_result` | The structured result: answer text, sources, execution steps, cards, timing, and more | **Recommended** for most use cases | | `result_json` | The original result as a JSON **string** | For full archiving, or to read new fields `raw_result` doesn’t expose yet | The three overlap, and each field can be up to 8 MiB, so task responses can be large. `raw_result` is only present once the task has a result (it succeeded, or the platform run failed). Queued, canceled, or dispatch-failed tasks **don’t have this field**, so check for it before reading. ### How `raw_result` is encoded [Section titled “How raw\_result is encoded”](#how-raw_result-is-encoded) `raw_result` uses protobuf’s JSON encoding, which differs from the outer fields: * Field names are **camelCase**: `rawContent`, `shareLink`, `agentSteps` — whereas outer fields use snake\_case like `share_link`. * **Zero-value fields are omitted**: `success: false`, `thinking: false`, empty strings, empty arrays, and a `rank` of 0 don’t appear. Fill in defaults when reading — in Python, for example, `raw.get("references", [])`. * `elapsedMs` (execution time in milliseconds, queueing excluded) is a **string**, such as `"48213"`; convert it to a number yourself. * `completedAt` is a UTC timestamp in ISO 8601 format, such as `2026-10-26T12:41:37.512Z`. ## Answer text [Section titled “Answer text”](#answer-text) `rawContent` (and `answer`) is the platform’s original Markdown. **The platform’s own citation markers and card placeholders are kept as-is, not converted.** On Global platforms (ChatGPT, Gemini, Claude, Perplexity, Copilot, Grok, Google AI Mode, Google AI Overview), the answer text is returned as the platform wrote it; no platform-specific marker conventions have been catalogued for them yet. For China platforms, these are the markers we’ve observed: | Platform | Markers in the answer text (examples) | | --------------------- | --------------------------------------------------------------------------------------------- | | DeepSeek | `[citation:3]`; `[reference:3]` with [thinking mode](../platforms/#thinking-mode-thinking) on | | Tencent Yuanbao | `[citation:2]`; card placeholders `[](@replace=media_…)` | | Kimi | Citations wrapped in `…` | | Zhipu ChatGLM | `【turn0search2】` | | Metaso | `[[2]]`; images left in the text as Markdown images | | ERNIE Bot | `^2^` (uncommon); may also appear as plain Markdown links | | Qwen | Card placeholders such as `[(video_note_list_1)]` | | Doubao, iFlytek Spark | Citation positions aren’t marked in the text | | Xiaohongshu Diandian | Platform-native `` underline tags, zero-width spaces, etc. | Marker formats are up to each platform The observations on this page are from October 2026, and a platform redesign can change them at any time. Clean up these markers before showing the text to users if you need to, but don’t rely on their format for strict validation — use `references` and `cited` to determine what was cited. ## Sources: `references` [Section titled “Sources: references”](#sources-references) `references` lists **every source the platform found or read** for this answer, deduplicated by URL. Sources cited in the answer text come first, followed by those found but not cited. Each item: | Field | Description | | ----------------- | --------------------------------------------------------------------------------------------------------- | | `rank` | Display order on the platform, starting at 1 | | `title` | Title | | `url` | Link; platform redirect links are resolved to the original URL where possible | | `snippet` | Excerpt; may be empty | | `source` | Source name (site name); may be empty | | `domain` | Domain; may be empty | | `contentPlatform` | The content platform detected (such as a particular content community); empty if not detected | | `cited` | Whether the answer text cites it — **three states**, see below | | `publishTime` | Publish time: ISO 8601, `YYYY-MM-DD`, or the platform’s relative wording (e.g. “1 day ago”); may be empty | | `author` | Author (post author, video creator, etc.); may be empty | ### The three states of `cited` [Section titled “The three states of cited”](#the-three-states-of-cited) | Value | Meaning | | ---------------- | ------------------------------------------------------------------------------------ | | `true` | The answer text cites this source | | `false` | The platform found or read it, but the answer text doesn’t cite it | | **Field absent** | The platform doesn’t distinguish cited vs. uncited — **don’t treat this as `false`** | By platform (observed October 2026): * **Global platforms: `cited` is not set.** Every source arrives without the field. * Doubao, Kimi, Tencent Yuanbao, Zhipu ChatGLM, Metaso, ERNIE Bot: distinguish `true` / `false`. * DeepSeek: sources it can tie to a specific web page are marked `true`; sources it can only tie to a batch of search results are left unset. * iFlytek Spark: the platform currently doesn’t return search sources, so `references` is built from links the model wrote into its answer. `cited` is always `true`, and there’s no information beyond the title. * Qwen and Xiaohongshu Diandian: `cited` is not set. ### Things to watch when reading [Section titled “Things to watch when reading”](#things-to-watch-when-reading) * **`url` isn’t always a valid link.** A few platforms have sources without a link (map plugins, for example), leaving `url` empty; some return an internal platform ID instead. Check that it starts with `http://` or `https://` before using it. * **`domain` can be empty.** If you’re aggregating by domain and `domain` is empty, parse it from `url`. * **An empty `references` list is a normal result.** Some questions don’t trigger a web search, so the answer has no sources. ## Example: was your site cited? [Section titled “Example: was your site cited?”](#example-was-your-site-cited) The code below finds sources from a given domain in the answer and splits them into “cited in the answer”, “found but not cited”, and “platform doesn’t distinguish”: * Python ```python from urllib.parse import urlparse def match_domain(ref, domain): host = ref.get("domain") or urlparse(ref.get("url", "")).hostname or "" return host == domain or host.endswith("." + domain) def citation_report(task, domain): refs = (task.get("raw_result") or {}).get("references", []) hits = [r for r in refs if match_domain(r, domain)] return { "cited": [r for r in hits if r.get("cited") is True], "not_cited": [r for r in hits if r.get("cited") is False], "unknown": [r for r in hits if "cited" not in r], } report = citation_report(task, "example.com") print({k: len(v) for k, v in report.items()}) ``` * Node.js ```js function matchDomain(ref, domain) { let host = ref.domain ?? ""; if (!host) { try { host = new URL(ref.url ?? "").hostname; } catch { host = ""; } } return host === domain || host.endsWith("." + domain); } function citationReport(task, domain) { const hits = (task.raw_result?.references ?? []).filter((r) => matchDomain(r, domain)); return { cited: hits.filter((r) => r.cited === true), notCited: hits.filter((r) => r.cited === false), unknown: hits.filter((r) => r.cited === undefined), }; } const report = citationReport(task, "example.com"); console.log(Object.fromEntries(Object.entries(report).map(([k, v]) => [k, v.length]))); ``` On Global platforms every hit lands in `unknown`, because they don’t set `cited`. There, “the platform used a page from your domain as a source” is the strongest signal available. ## Execution steps: `agentSteps` [Section titled “Execution steps: agentSteps”](#execution-steps-agentsteps) Some platforms expose how they arrived at the answer, in order. Read the fields that match each step’s `type`: | `type` | Fields to read | Contents | | ---------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `thinking` | `content`, `summary` | The reasoning text and the platform’s summary of it | | `search` | `name`, `queries`, `results` | The **search queries** the AI actually ran, and the results of each search (`title`, `url`, `siteName`, `snippet`) — not all of which are cited | | `tool` | `name`, `args` | Other tool calls and their raw arguments | The `queries` of `search` steps are especially useful: they show what the AI actually searched for to answer the question, which can guide the keywords in your content. When a platform doesn’t expose its steps for a tier, `agentSteps` is absent. **On Global platforms, `agentSteps` is currently usually absent.** `thinking` steps mostly appear only with [thinking mode](../platforms/#thinking-mode-thinking) on; with it off, some China platforms such as Qwen, Zhipu ChatGLM, and Metaso may still include a short thinking or progress note. To tell whether an answer was produced with thinking on, check `raw_result.thinking`, not whether there are `thinking` steps. ## Media cards: `mediaCards` [Section titled “Media cards: mediaCards”](#media-cards-mediacards) Video, image, product, and ranking cards in the answer, in the order they appear: * `type`: `image`, `video`, `product`, `ranking`, or `other`. * `anchor`: the card’s placeholder text in `rawContent`, which tells you where the card sits. It’s empty when the platform doesn’t put placeholders in the text; then you can only go by order. * `items[]`: each entry in the card, with `title`, `url`, `imageUrl`, `source`, `author`, `duration`, `price`, `rating`, `publishTime`, and `description`. Fields that weren’t captured are omitted. **On Global platforms, `mediaCards` is currently usually absent.** Image links expire `imageUrl` is usually a signed, temporary link from the platform and stops working after a while. For long-term storage, keep the `url` (the video, post, or product page), or copy the image to your own storage soon after you get the result. ## Share link [Section titled “Share link”](#share-link) `share_link` (`raw_result.shareLink`) is the platform’s share page for this conversation, letting someone review the original answer on the platform. It’s an empty string when the platform doesn’t provide one or creating the share failed; availability varies on Global platforms. Note that Xiaohongshu Diandian share links only open inside the Xiaohongshu app — they load as a blank page in a browser. `raw_result.conversationId` and `messageId` are the platform’s own conversation and message IDs, for reference only. The API doesn’t support continuing a conversation from them. ## Results of failed tasks [Section titled “Results of failed tasks”](#results-of-failed-tasks) Tasks whose platform run failed (`error_code` is `crawl_failed`) also carry a `raw_result`. In it, `success` is absent (that is, `false`), `rawContent` is empty, and `error` holds the failure message (the same `{"zh","en"}` JSON string as `error_message`). To detect failure, check the outer `status` and `error_code` — see [Error Handling](../errors/#task-failures-error_code). ## Related pages [Section titled “Related pages”](#related-pages) [The Task Object](../../api/task-object/)Definitions of the outer fields and every raw\_result sub-structure. [Platforms, Priority & Country](../platforms/)Each platform's region and status. # Task Lifecycle > Tasks in the MaxAEO API run asynchronously. This page explains the seven statuses (queued, dispatching, pending, running, succeeded, failed, canceled) and how tasks move between them, how often to poll, the wait_seconds synchronous wait, and how to avoid duplicate submissions when a submission's outcome is unknown. Every call to [Submit a Task](../../api/submit-task/) creates a new task and immediately returns its `task_id`. The task is then dispatched, queued, and executed on the server, and eventually settles in a **final status**. You read its latest state with [Retrieve a Task](../../api/get-task/). ## Statuses at a glance [Section titled “Statuses at a glance”](#statuses-at-a-glance) | `status` | Meaning | Final | `credits` | | ------------- | ------------------------------------------------------------ | ----- | ------------ | | `queued` | Accepted and stored, waiting to be dispatched | | Reserved | | `dispatching` | Being dispatched, or the dispatch outcome is being confirmed | | Reserved | | `pending` | Dispatched and waiting in the platform queue | | Reserved | | `running` | Asking the AI platform and capturing the answer | | Reserved | | `succeeded` | An answer was captured | ✓ | Charged | | `failed` | Failed; see `error_code` for the reason | ✓ | 0 (released) | | `canceled` | The task was canceled | ✓ | 0 (released) | Only `succeeded`, `failed`, and `canceled` are final — once a task reaches one of them, it normally doesn’t change again (in rare cases an operator rerun can replace a final result with a newer one — reconcile against the latest read). For how credits work, see [Billing & Credits](../billing/). ```text submit ──▶ queued ──▶ dispatching ──▶ pending ⇄ running ──▶ succeeded ──▶ failed ──▶ canceled ``` A few things to keep in mind: * **A task can fail at several stages.** The API key became invalid before dispatch (`queued` → `failed`), the dispatch was rejected (`dispatching` → `failed`), or the platform crawl failed (`running` → `failed`). Queued or running tasks can also be canceled (→ `canceled`). * **`pending` and `running` can alternate.** When a P2 task hits a retryable failure (network issue, temporary platform error), it goes back to the queue and runs once more. `started_at` is reset to 0 and recorded again when execution restarts. * **`dispatching` is not a failure.** When the dispatch outcome needs confirming, the task stays in `dispatching` with `error_code` set to `dispatch_uncertain`. The server confirms it on its own and never dispatches twice. **Decide success or failure from `status` only — a non-empty `error_code` doesn’t mean the task failed.** * **A successful answer can be short.** `succeeded` only means the platform returned an answer. An empty source list or a very short answer can be perfectly normal (some questions don’t trigger a web search on the platform). ## Timing expectations [Section titled “Timing expectations”](#timing-expectations) * **The crawl itself** usually takes anywhere from tens of seconds to two minutes, depending on the platform and the length of the answer; with [thinking mode](../platforms/#thinking-mode-thinking) on, it’s usually noticeably slower. Platforms differ a lot; use `p50_duration_seconds` from [List Platforms](../../api/list-platforms/) as a reference (the median time from submission to completion for successful tasks over the last 24 hours, queueing included). * **Queueing has no upper bound and no SLA.** When a platform is busy or its accounts are temporarily unavailable, P2 tasks stay in the queue until it recovers, which can take a while. P1 tasks are dequeued first, but there’s still no guarantee they start within seconds. See [Platforms, Priority & Country](../platforms/#priority-p1-vs-p2) for how the two tiers differ. * **There’s no server-side execution deadline.** A task is never marked as failed just because it waited too long, and it doesn’t expire once its `monitor_date` has passed. ## Polling: how often to check [Section titled “Polling: how often to check”](#polling-how-often-to-check) The server syncs the progress of unfinished tasks roughly every 15 seconds, and [Retrieve a Task](../../api/get-task/) returns that synced, stored state. So: * **Poll about every 15 seconds.** Polling more often usually just returns the same status. * With many tasks in flight, run a single scheduled loop that checks every unfinished `task_id` in a batch, rather than blocking a thread per task. * Feel free to set your own business timeout (say, stop waiting after 30 minutes), but **don’t treat a timeout as a failure**: the task keeps running, and you can fetch the result with its `task_id` at any time later. * Retrieving a task doesn’t cost credits. ## Synchronous wait: `wait_seconds` [Section titled “Synchronous wait: wait\_seconds”](#synchronous-wait-wait_seconds) If you’d rather not write a polling loop, pass `wait_seconds` (0–120) when submitting. The server checks the task every 2 seconds during that window and returns as soon as the task reaches a final status, or when the time is up. * `0` (default): don’t wait — return the `queued` task immediately. * If time runs out before the task finishes, you get a non-final task back and still need to keep polling by `task_id`. * Set your HTTP client timeout longer than `wait_seconds` (for example, 15 extra seconds). How long you can actually wait is also capped by proxy timeouts along the network path. If the connection drops mid-wait, the task isn’t affected — recover its `task_id` as described in the next section and keep polling. * The synchronous wait only changes **when this HTTP request returns**. It doesn’t make the task run faster or slower, and it doesn’t affect billing. ## Avoiding duplicate submissions [Section titled “Avoiding duplicate submissions”](#avoiding-duplicate-submissions) **Every call to the submit endpoint creates a new task**, and each one is billed if it succeeds. The server doesn’t deduplicate by question. So when you’re not sure whether a previous submission went through (the request timed out, the connection dropped, your process crashed), **don’t just retry the submission**. Instead: 1. Call [List Tasks](../../api/list-tasks/) with the question text as `keyword` (up to 256 characters — for a longer question, pass its first 256), or narrow it down with `start_time` / `end_time`, to find the `task_id` of that submission. 2. If you find it, keep polling it. Only if you’ve confirmed it isn’t there should you submit again. In your own system, write a “to submit” record first and save the `task_id` as soon as you get it back, so you can always reconcile. ## What `monitor_date` means [Section titled “What monitor\_date means”](#what-monitor_date-means) `monitor_date` is the task’s business date (`YYYY-MM-DD`, Beijing time, UTC+8), defaulting to the day of submission. It labels which day’s monitoring the task belongs to, so you can file and reconcile results by day: * It’s **not** a scheduled run time: tasks enter the queue as soon as they’re submitted, not on that date. * It’s **not** a deadline: tasks still run after that date has passed. * Systems in other time zones can pass an RFC 3339 timestamp with an offset (such as `2026-10-26T09:30:00-07:00`); the server converts it to the corresponding Beijing-time date. ## No callbacks [Section titled “No callbacks”](#no-callbacks) Webhook callbacks aren’t available yet — poll to get task results. ## Related pages [Section titled “Related pages”](#related-pages) [Retrieve a Task](../../api/get-task/)Read a task's status and result by task\_id. [Error Handling](../errors/)What each error\_code on a failed task means and what to do about it. # Connect via MCP > The MaxAEO Developer Platform MCP server wraps the API as four tools. With an API key, any client that supports Streamable HTTP — Claude Code, Cursor, VS Code, and more — can ask ChatGPT, Perplexity, Gemini, and other AI platforms questions directly. MCP (Model Context Protocol) is an open protocol that lets AI clients call external tools. The MaxAEO MCP server wraps the four API endpoints as tools, so once it’s connected you can tell your AI assistant something like “ask ChatGPT and Perplexity how they answer this question and check whether they cite our website” — and it handles submitting, waiting, and reading the results. | Item | Value | | ----------------------- | ----------------------------------------------------------------------------------------------------- | | URL | `https://maxaeo.ai/geoapi/geodata/developer/mcp` | | Transport | Streamable HTTP (stateless, JSON responses only) | | Authentication | `Authorization: Bearer ` header | | Tools | `list_platforms`, `submit_task`, `get_task`, `list_tasks` — see [MCP Tools Reference](tools/) | | Billing and rate limits | Exactly the same as the API: successful tasks use credits and share your account’s balance and limits | Your client must support custom headers The server authenticates with an API key and doesn’t support OAuth. Your client needs to let you set an `Authorization` header for a remote MCP server. Clients that only support stdio can bridge through `mcp-remote` — see below. ## Get an API key [Section titled “Get an API key”](#get-an-api-key) Create a key on the **API Keys** page of the [Developer Console](https://maxaeo.ai/developers/). We recommend a dedicated key for MCP (for example, “MCP – my laptop”) so its usage shows up separately and you can disable it on its own. See [Manage API Keys](../console/api-keys/). ## Configure your client [Section titled “Configure your client”](#configure-your-client) * Claude Code Run in your terminal: ```bash claude mcp add --transport http maxaeo https://maxaeo.ai/geoapi/geodata/developer/mcp \ --header "Authorization: Bearer $MAXAEO_API_KEY" ``` Run `export MAXAEO_API_KEY="gdev_…"` first. Afterwards, run `claude mcp list` and check that `maxaeo` shows as connected. * Cursor Edit `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in your project: ```json { "mcpServers": { "maxaeo": { "url": "https://maxaeo.ai/geoapi/geodata/developer/mcp", "headers": { "Authorization": "Bearer gdev_your_full_key" } } } } ``` Save, then confirm on the MCP page of Cursor’s settings that `maxaeo` is enabled and lists four tools. * VS Code Create `.vscode/mcp.json` in your project. Use `inputs` so VS Code prompts for the key on first start instead of storing it in the file: ```json { "inputs": [ { "type": "promptString", "id": "maxaeo-api-key", "description": "MaxAEO API key", "password": true } ], "servers": { "maxaeo": { "type": "http", "url": "https://maxaeo.ai/geoapi/geodata/developer/mcp", "headers": { "Authorization": "Bearer ${input:maxaeo-api-key}" } } } } ``` Then enable the `maxaeo` tools in Copilot Chat’s agent mode. * stdio-only clients For clients that can only launch local commands (such as Claude Desktop’s config file), use [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) to bridge the remote server into a local process (requires Node.js): ```json { "mcpServers": { "maxaeo": { "command": "npx", "args": [ "-y", "mcp-remote", "https://maxaeo.ai/geoapi/geodata/developer/mcp", "--header", "Authorization:${MAXAEO_AUTH}" ], "env": { "MAXAEO_AUTH": "Bearer gdev_your_full_key" } } } } ``` There’s no space after the colon in `Authorization:${MAXAEO_AUTH}`; the space lives inside the environment variable’s value. This sidesteps the way some clients handle spaces in arguments. * Other clients Any client that supports the **Streamable HTTP** transport and custom request headers works the same way. When adding the MCP server, enter: * URL: `https://maxaeo.ai/geoapi/geodata/developer/mcp` * Header: `Authorization` with the value `Bearer gdev_your_full_key` Protect your config files A key written into a config file is effectively a plaintext password. Don’t commit an `mcp.json` that contains a key. When sharing configuration with a team, pass the key through environment variables or your client’s input prompts. ## Verify the connection [Section titled “Verify the connection”](#verify-the-connection) The server checks your key as soon as the client connects: with an invalid key, the client reports a connection failure or “unauthorized” (HTTP 401). Once connected, try asking your assistant: > List the platforms MaxAEO supports and their prices. The assistant calls `list_platforms` and shows the result. This doesn’t use any credits. ## Example prompts [Section titled “Example prompts”](#example-prompts) Once connected, just describe what you want in plain language, for example: * “Ask ChatGPT: what are the best mechanical keyboards for programmers? When the answer is ready, list the web pages it cites.” * “Ask ChatGPT and Perplexity ‘what’s the best CRM for a small team’ using P2, and compare which brands and sources each one cites.” * “Ask ChatGPT the same question with thinking mode on, and tell me whether thinking was actually on.” * “I lost the result of the task I just submitted. Find it among today’s tasks by searching for ‘mechanical keyboard’.” When connecting, the server tells the assistant the recommended flow: call `list_platforms` first, then `submit_task`, then `get_task` repeatedly until the task finishes; and if a submission’s outcome is unknown, look it up with `list_tasks` instead of submitting again. ## Related [Section titled “Related”](#related) [MCP Tools Reference](tools/)Parameters, output, and error format for the four tools, plus protocol details. [Billing & Credits](../guides/billing/)MCP calls share the same balance and billing rules as the API. # MCP Tools Reference > Inputs, output, and error format for the four tools on the MaxAEO MCP server — list_platforms, submit_task, get_task, and list_tasks — plus protocol details such as supported versions, authentication failures, and stateless transport, with JSON-RPC examples. The MCP server exposes four tools that map one-to-one to the API. Parameter names, value ranges, and business rules match the corresponding endpoints, and tool arguments likewise **reject undefined fields** (such as `taskId` instead of `task_id`). | Tool | Endpoint | Read-only | Uses credits | | ---------------- | ------------------------------------------- | --------- | ---------------------------- | | `list_platforms` | [List platforms](../../api/list-platforms/) | Yes | No | | `submit_task` | [Submit a task](../../api/submit-task/) | No | Successful tasks use credits | | `get_task` | [Retrieve a task](../../api/get-task/) | Yes | No | | `list_tasks` | [List tasks](../../api/list-tasks/) | Yes | No | Each tool returns a block of JSON text (the `text` in the MCP `content`), with the same field names as the API. ## list\_platforms [Section titled “list\_platforms”](#list_platforms) Lists the available platforms (including any under maintenance), with region, status, both tier prices, and last-24-hour stats. **Parameters**: none. **Returns**: the same as `data.ent` from [List platforms](../../api/list-platforms/#response) — `platforms` and `items`. ## submit\_task [Section titled “submit\_task”](#submit_task) Submits a question to a platform and creates a new task. **Every call creates a new task that uses credits if it succeeds.** | Parameter | Type | Required | Description | | -------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `platform` | string | Yes | Platform ID from `list_platforms`; skip platforms whose `platform_status` is `maintenance` | | `query` | string | Yes | The question, up to 4,096 characters | | `priority` | integer | No | `1` = P1 Priority, `2` = P2 Standard (default) | | `thinking` | boolean | No | Whether to request the platform’s thinking mode, default `false`. Noticeably slower, no extra cost; platforms without a thinking switch run with it off — `effective_thinking` in the result reports the actual mode. See [Thinking mode](../../guides/platforms/#thinking-mode-thinking) | | `country` | string | No | Two-letter country code such as `US`. A dedicated exit for that country isn’t guaranteed | | `monitor_date` | string | No | Business date `YYYY-MM-DD` (UTC+8), defaults to today. An RFC 3339 timestamp with an offset is converted to its UTC+8 date | | `wait_seconds` | integer | No | `0`–`120`, default `0`: how long to wait for the task to finish. If it hasn’t finished in time, the current status is returned | **Returns**: a task summary with the same fields as `get_task` below. Without `wait_seconds`, `status` is usually `queued`. ## get\_task [Section titled “get\_task”](#get_task) Reads a task’s status and result. | Parameter | Type | Required | Description | | ------------- | ------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | `task_id` | string | Yes | The task ID returned by `submit_task` or `list_tasks` | | `include_raw` | boolean | No | When `true`, also returns the full `raw_result` (agent steps, media cards, source snippets, and so on). Can be large | **Returns**: to save model context, a compact task summary by default — no `result_json` and no full `raw_result`: | Field | Description | | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `task_id`, `status`, `platform`, `query` | Basic task info | | `monitor_date`, `priority`, `country`, `thinking` | Submission parameters; `thinking` is the requested thinking mode | | `effective_thinking` | Whether thinking was actually on. Present only once there’s a result (success, or a failed platform run), and returned even when `false`; doesn’t need `include_raw` | | `credits` | Credits charged or reserved | | `answer` | Answer text (Markdown) | | `references` | Sources; each item has only `rank`, `title`, `url`, `domain`, and `cited` | | `share_link` | The platform’s share link | | `error_code`, `error_message` | Task-level error; `error_message` is a `{"zh","en"}` JSON string | | `created_at`, `finished_at` | Unix seconds | | `raw_result` | Only when `include_raw` is `true`; see [The Task Object](../../api/task-object/#raw_result) for its structure | Apart from `thinking`, `task_id`, `status`, `platform`, `query`, and `credits`, empty fields are omitted (see the table above for `effective_thinking`). `references[].cited` is three-state here too: an absent field means the platform doesn’t make the distinction — don’t treat it as `false`. Statuses: `queued`, `dispatching`, `pending`, and `running` mean the task isn’t finished; a P2 task may go from `running` back to `pending` once to retry; `succeeded`, `failed`, and `canceled` are final. ## list\_tasks [Section titled “list\_tasks”](#list_tasks) Lists tasks submitted with **the API key this connection uses**, newest first, without answers. Use it to recover a `task_id` when a submission result was lost, then call `get_task`. | Parameter | Type | Description | | ------------ | ------- | ---------------------------------------------------------------------------- | | `page` | integer | Page number, default `1` | | `page_size` | integer | Items per page, default `20`, maximum `100` | | `status` | string | Only tasks in this status; `pending` also matches `queued` and `dispatching` | | `platform` | string | Only tasks for this platform | | `keyword` | string | Text contained in the query, or an exact `task_id` | | `start_time` | integer | Lower bound on submission time (Unix seconds, inclusive) | | `end_time` | integer | Upper bound on submission time (Unix seconds, exclusive) | **Returns**: the same as `data.ent` from [List tasks](../../api/list-tasks/#response) — `total`, `page`, `page_size`, and `items`. ## Errors [Section titled “Errors”](#errors) Errors in MCP fall into three groups: | Type | How it shows up | Example | | -------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | | **Tool error** | A normal tool result with `isError: true`; the text reads `error : ()` | `error 1023: Insufficient credits. Please top up and try again. (点数余额不足,请充值后重试)` | | **Authentication failure** | HTTP `401`, JSON-RPC error code `-32001`, with a `WWW-Authenticate` response header | The key is missing, invalid, disabled, or expired, or was disabled or rolled mid-session | | **Protocol error** | A JSON-RPC error | Unknown tool `-32602`, unknown method `-32601` | * Tool error codes are the same as the API’s `ret` values — see [Error Handling](../../guides/errors/#error-codes). If arguments are wrong, the text reads `invalid arguments: …`, so the model can fix them and try again. * When the service is temporarily unavailable, the tool error text is `Service unavailable. Please retry later.` * Authentication failures return HTTP 401 rather than a tool error, so your client prompts you to reconfigure the key instead of letting the model retry over and over. ## Protocol details [Section titled “Protocol details”](#protocol-details) For building your own MCP client or troubleshooting connections: | Item | Details | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Endpoint | Accepts `POST` only; `GET` (event stream) returns `405` | | Sessions | Stateless; no `Mcp-Session-Id` is issued | | Responses | Each request gets a single JSON response (`Content-Type: application/json`); SSE isn’t used | | Protocol versions | `2025-11-25`, `2025-06-18`, `2025-03-26`, and `2024-11-05`. If `initialize` requests a version outside this list, `2025-11-25` is returned. An unsupported version in the `MCP-Protocol-Version` header returns HTTP `400` | | Capabilities | `tools` only (`listChanged: false`) | | Server info | `serverInfo.name` is `geodata-developer` | | When the key is checked | On `initialize`, `tools/list`, and `tools/call` | | Notifications | Notification messages (no `id`) return HTTP `202` with no body | | Batching | Not supported; the body must be a single JSON-RPC object | | Request size | Up to 64 KiB; larger bodies return HTTP `413` | ### JSON-RPC examples [Section titled “JSON-RPC examples”](#json-rpc-examples) ```bash # Initialize curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/mcp" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-client","version":"1.0.0"}}}' # Call a tool curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/mcp" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -H "MCP-Protocol-Version: 2025-06-18" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_task","arguments":{"task_id":"task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23"}}}' ``` The `tools/call` response: ```json { "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "{\"thinking\":false,\"effective_thinking\":false,\"task_id\":\"task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23\",\"status\":\"succeeded\",\"platform\":\"chatgpt\",\"query\":\"best mechanical keyboards for programmers\",\"monitor_date\":\"2026-10-26\",\"priority\":2,\"credits\":6,\"answer\":\"For programming, the best mechanical keyboards balance…\",\"references\":[{\"rank\":1,\"title\":\"The Best Mechanical Keyboards for Programmers in 2026\",\"url\":\"https://www.example.com/best-keyboards-for-programmers\",\"domain\":\"www.example.com\"}],\"share_link\":\"https://chatgpt.com/share/…\",\"created_at\":1793018425,\"finished_at\":1793018497}" } ] } } ``` # Quickstart > Create an API key, list the available platforms, submit your first question to ChatGPT, and poll until you get the answer and its cited sources — with complete cURL, Python, and Node.js examples. This page takes you from zero to your first answer: create an API key → list available platforms → submit a question → poll for the result → read the answer. All you need is a terminal. 1. ## Create an API key [Section titled “Create an API key”](#create-an-api-key) Sign in to the [Developer Console](https://maxaeo.ai/developers/), open **API Keys**, and click **Create API key**. Give it a name (for example, “Local testing”) and pick an expiration. Once it’s created, a dialog shows the full key — a string that starts with `gdev_`. **The full key is shown only once.** Copy it and store it in an environment variable: ```bash export MAXAEO_API_KEY="gdev_your_full_key" ``` For everything else you can do with keys, see [Manage API Keys](../console/api-keys/). 2. ## List the available platforms [Section titled “List the available platforms”](#list-the-available-platforms) Start with the simplest request to confirm the key works and get each platform’s ID and price: ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/platforms/get" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Response (excerpt): ```json { "ret": 1, "data": { "ent": { "platforms": ["chatgpt", "gemini", "claude", "perplexity", "…"], "items": [ { "platform": "chatgpt", "region": "overseas", "platform_status": "normal", "p1_credits": 12, "p2_credits": 6, "has_stats": true, "success_rate": 0.95, "p50_duration_seconds": 58, "sample_count": 112, "stats_updated_at": 1793018400 } ] } } } ``` `ret` equal to `1` means success. When you submit a task, set `platform` to one of the IDs in `platforms` (platforms whose `platform_status` is `maintenance` aren’t accepting tasks). `p1_credits` / `p2_credits` are the credits charged per **successful** call at each priority tier (the numbers above are illustrative — rely on the live response). If you get `{"ret":1030, …}`, the key is invalid. Check that the environment variable holds the full key and that the key hasn’t been disabled — see [Authentication](../authentication/). 3. ## Submit a task [Section titled “Submit a task”](#submit-a-task) Ask ChatGPT a question. If you omit `priority`, the task runs and is billed at P2 Standard: ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/submit" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "chatgpt", "query": "best mechanical keyboards for programmers"}' ``` The endpoint returns the task right away, with status `queued`: ```json { "ret": 1, "data": { "ent": { "task_id": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23", "platform": "chatgpt", "query": "best mechanical keyboards for programmers", "status": "queued", "priority": 2, "credits": 6, "…": "…" } } } ``` Save the `task_id`. At this point `credits` is the **reserved** amount: it’s only charged if the task succeeds, and released if it fails or is canceled. 4. ## Poll for the result [Section titled “Poll for the result”](#poll-for-the-result) Check every 15 seconds until `status` becomes `succeeded`, `failed`, or `canceled`: ```bash curl -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/get" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"task_id": "task_3f6c1d2e-8a4b-4c1f-9e2d-7b5a0c9d1e23"}' ``` Fetching the answer itself usually takes tens of seconds to two minutes, plus queue time; it can take longer when a platform is busy. The intermediate statuses `dispatching`, `pending`, and `running` are all normal — just keep polling. See [Task Lifecycle](../guides/task-lifecycle/). 5. ## Read the answer [Section titled “Read the answer”](#read-the-answer) Once the task succeeds, the response contains: * `answer`: the AI’s raw answer (Markdown), with the platform’s own citation markers preserved. * `raw_result.references`: the cited sources; `cited: true` means the answer text actually cites that source. * `share_link`: the platform’s share link (empty if the platform doesn’t provide one). * `credits`: the credits actually charged. Field definitions are in [The Task Object](../api/task-object/); parsing tips are in [Working with Results](../guides/results/). ## Complete example [Section titled “Complete example”](#complete-example) The scripts below chain submit, poll, and read together. The API key is read from the `MAXAEO_API_KEY` environment variable. * Python quickstart.py ```python import os import time import requests BASE_URL = "https://maxaeo.ai/geoapi/geodata/developer" HEADERS = {"Authorization": f"Bearer {os.environ['MAXAEO_API_KEY']}"} TERMINAL = ("succeeded", "failed", "canceled") def call(path, body): resp = requests.post(BASE_URL + path, headers=HEADERS, json=body, timeout=30) resp.raise_for_status() data = resp.json() if data["ret"] != 1: raise RuntimeError(f"ret={data['ret']}: {data.get('msg')}") return data["data"]["ent"] task = call("/task/submit", {"platform": "chatgpt", "query": "best mechanical keyboards for programmers"}) print("Submitted", task["task_id"]) while task["status"] not in TERMINAL: time.sleep(15) task = call("/task/get", {"task_id": task["task_id"]}) print("Status", task["status"]) if task["status"] == "succeeded": print(task["answer"]) for ref in (task.get("raw_result") or {}).get("references", []): if ref.get("cited") is True: print(ref.get("rank"), ref.get("title"), ref.get("url")) print("Credits charged", task["credits"]) else: print("Task did not succeed", task["error_code"], task["error_message"]) ``` Run it: `pip install requests && python quickstart.py` * Node.js quickstart.mjs ```js const BASE_URL = "https://maxaeo.ai/geoapi/geodata/developer"; const headers = { Authorization: `Bearer ${process.env.MAXAEO_API_KEY}`, "Content-Type": "application/json", }; const TERMINAL = ["succeeded", "failed", "canceled"]; async function call(path, body) { const resp = await fetch(BASE_URL + path, { method: "POST", headers, body: JSON.stringify(body) }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); if (data.ret !== 1) throw new Error(`ret=${data.ret}: ${data.msg}`); return data.data.ent; } let task = await call("/task/submit", { platform: "chatgpt", query: "best mechanical keyboards for programmers" }); console.log("Submitted", task.task_id); while (!TERMINAL.includes(task.status)) { await new Promise((resolve) => setTimeout(resolve, 15_000)); task = await call("/task/get", { task_id: task.task_id }); console.log("Status", task.status); } if (task.status === "succeeded") { console.log(task.answer); for (const ref of task.raw_result?.references ?? []) { if (ref.cited === true) console.log(ref.rank, ref.title, ref.url); } console.log("Credits charged", task.credits); } else { console.log("Task did not succeed", task.error_code, task.error_message); } ``` Run it: `node quickstart.mjs` (requires Node.js 18 or later, which ships with `fetch`) * cURL ```bash # 1. Submit and capture the task_id (requires jq) TASK_ID=$(curl -s -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/submit" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d '{"platform": "chatgpt", "query": "best mechanical keyboards for programmers"}' | jq -r '.data.ent.task_id') echo "Submitted $TASK_ID" # "null" means the submission failed; rerun the submit command alone to see ret and msg # 2. Check every 15 seconds until the task reaches a final status while true; do RESP=$(curl -s -X POST "https://maxaeo.ai/geoapi/geodata/developer/task/get" \ -H "Authorization: Bearer $MAXAEO_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"task_id\": \"$TASK_ID\"}") STATUS=$(echo "$RESP" | jq -r 'if .ret == 1 then .data.ent.status else "error" end') echo "Status $STATUS" case "$STATUS" in succeeded|failed|canceled) break ;; error) echo "$RESP"; break ;; esac sleep 15 done # 3. Print the answer echo "$RESP" | jq -r '.data.ent.answer' ``` ## Next steps [Section titled “Next steps”](#next-steps) [Platforms, Priority & Country](../guides/platforms/)IDs for all 18 platforms, how to choose between P1 and P2, and the country parameter. [Error Handling](../guides/errors/)Request-level and task-level error codes, plus retry strategies. [Production Checklist](../guides/production-checklist/)Before moving from a script to production, check security, reliability, and cost. [Connect via MCP](../mcp/)Call it from your AI client without writing code.