Research API
The Research API is available on the Space Analyst plan and above. It is available on personal AstraEye Labs accounts only, not on an account provided through a school or another institution.
What it returns is the written prose of your own reports: the executive summary, the discussion, the conclusion, the prepared questions and answers and, where a report has them, its methodology and its test. It never returns charts, tables or data fields. By default it can only read. With the SAGE chat permission and a daily credit limit you set, it can also talk to SAGE and let SAGE act for you, including running research, within that limit.
Claude and ChatGPT can read your research and discuss it with you. With Claude Code, or any AI assistant setup that can run commands on your computer, use a personal token:
- Create a token in Settings → Research API.
- Keep it on your computer as an environment variable named
ASTRAEYE_TOKEN, so it is never pasted into the conversation. - Ask your assistant, for example:
Using curl with the header "Authorization: Bearer $ASTRAEYE_TOKEN", list my reports from https://astraeyelabs.io/api/v1/research/reports, fetch the latest one, and let's discuss today's market research results.
Your token works only on your own account, and with the SAGE chat permission it can also spend your Astra Credits, so keep it to yourself. Do not paste it into a chat or share it, and revoke it in settings if it is ever exposed. What your assistant reads is for your own research: the sources and disclosures in each report stay with it, and the content is informational, not advice. Terms Section 14.1 sets out exactly what is permitted.
By default the Research API is read-only: it provides programmatic access to your own research projects, to the written prose of each of your finished reports, and to the catalog of analyses and research templates AstraEye offers, with the plan each is on. If a token has the SAGE chat permission (sage:chat) and a positive daily credit limit, it can also talk to SAGE in your SAGE conversation and let SAGE act for you there as it does in the app, spending Astra Credits within that limit. It can never set up or change a schedule, change your settings, security, billing or email, or place or route an order.
Use of this API is governed by Section 14.1 of the Terms of Service, which grants access for your own personal, non-commercial research and for no other purpose. Sections below cite the governing provision wherever a technical rule carries a corresponding contractual obligation.
Tokens are issued from Settings → Research API. The token value is displayed once at creation and is not recoverable afterwards. One token may be active per account, and each token expires 90 days after issue. A connection to an AI assistant such as Claude or ChatGPT is separate: it does not count as this token and does not use one.
Under Terms Section 14.1 the token is personal to you. It must be kept confidential and must not be shared, published, sold, transferred, or sublicensed. You are responsible for all activity conducted with it. If a token is exposed, revoke it in settings and issue a replacement, which invalidates the previous token immediately.
Authenticate every request to the endpoints below with your token as a bearer credential. A connection to an AI assistant signs in through that assistant instead; its credentials do not work on these endpoints, and your token does not work for a connection.
curl https://astraeyelabs.io/api/v1/research/projects \ -H "Authorization: Bearer $ASTRAEYE_TOKEN"
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
| Authorization | header | string | required | none | Bearer <token>. Requests without a valid credential return 401. |
Automated access is permitted only through this API and within the published limits stated below, in accordance with Terms Section 14.
Returns the research projects belonging to the authenticated account, ordered newest first.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
| status | query | string | optional | active | Lifecycle state to return. Pass "all" for every project including finished and archived. Any other value is passed through and interpreted as a single lifecycle state. |
| cursor | query | string | optional | none | Opaque cursor from a previous response's nextCursor. Returns the page immediately after it. Omit for the first page. |
| Field | Type | Description |
|---|---|---|
| hasMore | boolean | True when more projects exist beyond this page. |
| nextCursor | string | null | Pass as cursor to retrieve the next page. Null on the last page. |
| projects[].id | string | Project identifier. Use as researchId when listing reports. |
| projects[].title | string | Project title as shown in the Lab. |
| projects[].status | string | Current lifecycle state. |
| projects[].createdAt | string (ISO 8601) | When the project was created. |
| projects[].updatedAt | string (ISO 8601) | When the project last changed. |
| projects[].runCount | integer | Number of runs the project has performed. |
| projects[].scheduled | boolean | Whether the project has a schedule attached. |
{
"hasMore": false,
"nextCursor": null,
"projects": [
{
"id": "…",
"title": "Crypto majors, weekly",
"status": "active",
"createdAt": "2026-08-01T09:14:22.000Z",
"updatedAt": "2026-08-24T09:14:22.000Z",
"runCount": 12,
"scheduled": true
}
]
}Returns at most 100 projects per page, ordered newest first. Continue by passing nextCursor as cursor until hasMore is false.
Returns an index of finished reports, ordered by generation time, newest first. A report corresponds to one run group, the unit shown on the results page. Only reports in a ready state are returned; pending, generating, failed, and degraded groups are omitted because they carry no dependable summary.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
| researchId | query | string | optional | none | Restrict the result to a single project. Accepts a projects[].id value. Omit to list reports across all projects. |
| cursor | query | string | optional | none | Opaque cursor from a previous response's nextCursor. Returns the page immediately after it. Omit for the first page. |
| Field | Type | Description |
|---|---|---|
| hasMore | boolean | True when more reports exist beyond this page. |
| nextCursor | string | null | Pass as cursor to retrieve the next page. Null on the last page. |
| reports[].id | string | Report identifier. Use to retrieve the summary. |
| reports[].researchId | string | Identifier of the project the report belongs to. |
| reports[].generatedAt | string (ISO 8601) | null | The single point in time at which the report was generated, and the field the list is ordered by. This is not a range. Null where a generation timestamp was not recorded. |
| reports[].availableLanguages[] | string[] | Languages this report can be read in, "en" first. Pass one as the language parameter when retrieving the summary, methodology or test. A report with no purchased translation lists "en" alone. |
{
"hasMore": false,
"nextCursor": null,
"reports": [
{ "id": "…", "researchId": "…", "generatedAt": "2026-08-24T09:31:00.000Z",
"availableLanguages": [ "en", "ru" ] }
]
}Returns at most 100 reports per page. Continue by passing nextCursor as cursor until hasMore is false. The cursor resumes right after the last report you received, so a report generated between two requests cannot push an older one across a page boundary and hide it. Terms Section 14.1 prohibits using repeated or scheduled requests to accumulate content into a dataset or to reconstruct an underlying provider dataset.
Returns the analyses AstraEye can run: the same list as the analysis catalog, with the id SAGE understands. An assistant can use it to name an analysis precisely when it asks SAGE what an analysis measures or asks SAGE to run one. Each analysis states the plan it is on and whether your plan includes it, and the response states your own plan. Prices are not listed: they depend on the run, and SAGE states them when asked.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
| scientist | query | string | optional | none | Only this scientist's analyses, by scientist id, such as atlas. |
| assetClass | query | string | optional | none | Only analyses for "crypto", "equity" or "universal" (both). |
| detail | query | string | optional | summary | "summary" returns the one-line summary of each analysis. "full" adds the full description, which is long; combine it with scientist or assetClass. |
| Field | Type | Description |
|---|---|---|
| count | integer | Number of analyses returned. |
| yourPlan | object | null | Your current plan. Null where it cannot be stated, such as an account with no plan. |
| yourPlan.id | string | The plan id. |
| yourPlan.name | string | The plan name members see on the subscribe page, such as Space Analyst. |
| yourPlan.rank | integer | The plan's position in AstraEye's plan order. A higher number is a higher plan; compare yourPlan.rank with an item's plan.rank. |
| analyses[].id | string | The analysis id, such as ta_momentum_scan. Use it, or the name, with SAGE. |
| analyses[].name | string | The analysis name as the catalog shows it. |
| analyses[].scientist | string | The scientist that runs it, such as atlas. |
| analyses[].assetClass | "crypto" | "equity" | "universal" | What it analyses; universal covers both. |
| analyses[].summary | string | null | One-line description. |
| analyses[].description | string | null | Full description of what it does. Present only with detail="full". |
| analyses[].plan | object | null | The lowest plan that includes this analysis: id, name and rank, as in yourPlan. Null where it cannot be stated. |
| analyses[].includedInYourPlan | boolean | null | Whether your plan includes this analysis. Null where it cannot be determined, which is not the same as false. |
{
"count": 1,
"yourPlan": { "id": "analyst", "name": "Space Analyst", "rank": 1 },
"analyses": [
{ "id": "ta_momentum_scan", "name": "Momentum Scan", "scientist": "atlas",
"assetClass": "universal", "summary": "…", "description": "…",
"plan": { "id": "freelancer", "name": "Freelancer", "rank": 0 },
"includedInYourPlan": true }
]
}With SAGE chat, an assistant can pass an analysis by name or id straight to SAGE, for example: "Ask SAGE to explain what the Momentum Scan measures" or "Ask SAGE to run a Momentum Scan on my watchlist". SAGE quotes the price first.
Returns one analysis in full, by its id from the list: its name, scientist, asset class, full description and methodology, meaning how it works, the public theory it rests on and what that theory cannot support. Load it when a conversation needs more than the one-line summary. Like the list, it states the plan the analysis is on, whether your plan includes it, and your own plan.
| Field | Type | Description |
|---|---|---|
| analysis.id, name, scientist, assetClass, summary | string | As in the list. summary may be null. |
| analysis.description | string | null | Full description of what it does. |
| analysis.methodology | string | null | How the analysis works and the limits of its method. Null where none is published. |
| analysis.plan, includedInYourPlan | object | null; boolean | null | As in the list. |
| yourPlan | object | null | Your current plan: id, name and rank, as in the list. |
An id that is not in the catalog returns 404 not_found.
Returns the research templates AstraEye offers: prebuilt combinations of analyses you can start a research project from, as the template gallery shows them. Each names the scientists and analyses it combines, the plan it is on and whether your plan includes it, and the response states your own plan. An assistant can use it to find the template that covers a question. Prices are not listed; SAGE states them when asked.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
| assetClass | query | string | optional | none | Only templates whose asset class is exactly "crypto", "equity" or "universal" (both). |
| scientist | query | string | optional | none | Only templates that include this scientist, by scientist id, such as atlas. |
| analysis | query | string | optional | none | Only templates that include this analysis, by its id from the analysis list. |
| Field | Type | Description |
|---|---|---|
| count | integer | Number of templates returned. |
| yourPlan | object | null | Your current plan: id, name and rank, as in List analyses. |
| templates[].id | string | The template id. Pass it to Retrieve a template. |
| templates[].name | string | The template name as the gallery shows it. |
| templates[].summary | string | null | One-line description. |
| templates[].category | string | null | The gallery category, such as technical, fundamental or sentiment. |
| templates[].assetClass | "crypto" | "equity" | "universal" | What it analyses; universal covers both. |
| templates[].scientists[] | { id, name } | The scientists whose analyses it combines, by id and name. |
| templates[].analyses[] | { id, name } | The analyses it runs, by analysis id and the name the template gives it. Each id can be read with Retrieve an analysis. |
| templates[].plan | object | null | The lowest plan that includes this template: id, name and rank. Null where it cannot be stated. |
| templates[].includedInYourPlan | boolean | null | Whether your plan includes this template. Null where it cannot be determined, which is not the same as false. |
{
"count": 1,
"yourPlan": { "id": "analyst", "name": "Space Analyst", "rank": 1 },
"templates": [
{ "id": "tmpl_v2_equity_fundamentals", "name": "Equity Deep Fundamentals",
"summary": "…", "category": "fundamental", "assetClass": "equity",
"scientists": [{ "id": "felix", "name": "FELIX" }],
"analyses": [{ "id": "fundamental_cash_flow_quality", "name": "Cash Flow Quality" }],
"plan": { "id": "trader", "name": "Galactic Merchant", "rank": 2 },
"includedInYourPlan": false }
]
}A template that is not included in your plan is still listed, with includedInYourPlan false, so an assistant can tell you which plan includes it.
Returns one template in full, by its id from the list: its full description, typical cadence and duration, and the analyses it runs, in order, each with the analysis id that Retrieve an analysis reads. Analyses in the same phase run side by side, grouped into workstreams, and a later phase starts once every earlier phase has finished. Within a workstream, a later stage starts once its earlier stages have finished.
| Field | Type | Description |
|---|---|---|
| yourPlan | object | null | Your current plan: id, name and rank, as in the list. |
| template.id, name, summary, category, assetClass, scientists, plan, includedInYourPlan | as in the list | As in the list. |
| template.description | string | null | Full description of what the template is for. |
| template.complexity | string | null | The gallery's complexity label, such as starter or advanced. |
| template.recommendedCadence | string | null | How often the template is meant to be run, such as Daily or Weekly. |
| template.estimatedMinutes | integer | null | Typical time for a run to finish, in minutes. |
| template.analysisCount | integer | Number of analyses the template runs. |
| template.analyses[].id, name, scientist | string | The analysis, by id, name and scientist. |
| template.analyses[].phase | integer | null | 1 for the first phase. Every earlier phase finishes before a later one starts. |
| template.analyses[].workstream | string | null | The group of analyses this one runs in within its phase. |
| template.analyses[].stage | integer | null | 1 for the first stage of its workstream. A workstream's earlier stages finish before a later one starts; analyses in the same stage run together. |
| template.analyses[].stageLabel | string | null | The label the template gives that stage. |
An id that is not a current template returns 404 not_found.
Returns the written summary of a single finished report belonging to the authenticated account.
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
| id | path | string | required | none | Report identifier from reports[].id. A report that does not belong to the account returns 404. |
| Name | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
| language | query | string | optional | en | The language to return the prose in. Responses are English unless this is passed, so an existing integration is unaffected. Accepts any value from the report's availableLanguages, and a regional form such as en-US resolves to its base language. A language the report does not have returns 400 unsupported_language rather than quietly returning English. |
| Field | Type | Description |
|---|---|---|
| report.id | string | Report identifier. |
| report.researchId | string | Identifier of the project the report belongs to. |
| report.generatedAt | string (ISO 8601) | null | When the report was generated. |
| report.language | string | The language of the prose in this response. This is what was actually served, which is not always what was requested - see requestedLanguage below. |
| report.availableLanguages[] | string[] | Every language this report can be read in, "en" first. |
| report.coverage | "complete" | "partial" | Whether every piece of prose in this response reached report.language. A translation frequently settles partially, so "partial" is ordinary rather than exceptional: read the per-item language field to see which pieces did. |
| report.requestedLanguage | string (present only on a fallback) | The language that was asked for, when it could not be served and English was returned instead. Absent on an ordinary response, so its presence is the signal. |
| report.languageFallbackReason | string (present only on a fallback) | Why the requested language was not served: family_not_translated (this part of the report was not translated), no_translated_units (nothing in this part reached that language), unit_metadata_unavailable (the per-item language record could not be read or verified, so the prose is not presented as translated), or translated_artifact_unreadable. |
| report.summary.title | string | null | Summary title. |
| report.summary.titleLanguage | string | null | The language this piece of prose was actually written in. Null where it is not recorded, which is not the same as English - the language is unknown, and the field says so rather than guessing. |
| report.summary.executiveSummary | string | null | Opening prose summary. |
| report.summary.executiveSummaryLanguage | string | null | The language of the opening prose, on the same terms as titleLanguage. |
| report.summary.sections[] | object | Ordered sections, each { id, heading, body, scientistId, language }. Only body is always present; the others may be null. |
| report.summary.conclusion | string | null | Closing prose. |
| report.summary.conclusionLanguage | string | null | The language of the closing prose, on the same terms as titleLanguage. |
| report.summary.questions[] | object | Prepared questions and answers, each { id, question, answer, scientistId, language }. scientistId and language may be null. |
| report.summary.disclosures | object | Informational notice, provenance statement, source list, notAdvice flag, and whether SAGE declined the advice part of the run. See Disclosures below. |
{
"report": {
"id": "…",
"researchId": "…",
"generatedAt": "2026-08-24T09:31:00.000Z",
"language": "ru",
"availableLanguages": [ "en", "ru" ],
"coverage": "partial",
"summary": {
"title": "…",
"titleLanguage": "ru",
"executiveSummary": "…",
"executiveSummaryLanguage": "en",
"sections": [ { "id": "…", "heading": "…", "body": "…", "scientistId": "…",
"language": "ru" } ],
"conclusion": "…",
"conclusionLanguage": null,
"questions": [ { "id": "…", "question": "…", "answer": "…", "scientistId": "…",
"language": "ru" } ],
"disclosures": {
"informationalNotice": "…",
"provenanceDisclosure": "…",
"sources": { "notice": "…", "url": "…/data-sources", "credits": [ "…" ] },
"notAdvice": true,
"adviceDeclined": false
}
}
}
}Returns how each analysis in the report was carried out, in written prose. Many reports have no methodology section; those return 404 not_found. Takes the same id and language parameters as the summary.
| Field | Type | Description |
|---|---|---|
| methodology.reportId, researchId, generatedAt | string; generatedAt string | null | As on the summary. |
| methodology.language, availableLanguages, coverage | string, string[], "complete" | "partial" | The same language fields as the summary, including requestedLanguage and languageFallbackReason on a fallback. |
| methodology.analyses[] | object | Each { analysisType, title, scientistId, theorySource, sections[] { heading, body }, language }. Only body is always present; the others may be null. An analysis with no written method is left out. theorySource says whether the method comes from a written explanation of the analysis or from the run's own findings. |
Returns the multiple-choice test written from the report, where it has one; a report without a test returns 404 not_found. Takes the same id and language parameters as the summary. Answers stay hidden until you submit that scientist's part of the test in AstraEye.
| Field | Type | Description |
|---|---|---|
| quiz.reportId, researchId, generatedAt | string; generatedAt string | null | As on the summary. |
| quiz.language, availableLanguages, coverage | string, string[], "complete" | "partial" | The same language fields as the summary, including requestedLanguage and languageFallbackReason on a fallback. |
| quiz.submittedScientists[] | string[] | The scientists whose part of the test you have submitted. "*" means the whole test was submitted at once. |
| quiz.questions[] | object | Each { id, scientistId, source, question, choices[], correctIndex, explanation, answerAvailable, language }. scientistId, source and language may be null. While that part of the test is open, answerAvailable is false and correctIndex and explanation are null; explanation can also be null after it is submitted. |
Responses contain written prose only: the summary presented on the results page, and the report's methodology and test where it has them. Charts, tables, figures, numeric series, screener output, and underlying market data are not exposed through this API.
Content returned is AstraEye Labs derived research output. It is not raw or redistributed market data, and receipt of it conveys no license, right, title, or interest in any data provider's data. Terms Section 12 continues to govern provider-sourced material, and Section 14.1 confirms that this API is not a data-export feature within the meaning of any provision of Section 12 governing the export of a provider's data. The restrictions in Section 12 applicable to derived output, including the prohibitions on model training and on reconstructing an underlying provider dataset, apply in full.
Terms Section 14.1 authorizes you to make your own research summaries available to an artificial-intelligence assistant, agent, or other tool that you personally operate and control, for your own personal research purposes. That authorization is expressly limited and does not permit:
- using the content to develop, train, fine-tune, evaluate, or improve any model;
- retaining or accumulating the content beyond what your personal use requires, or assembling it into a dataset, index, archive, or corpus;
- using repeated or scheduled requests to reconstruct or approximate any underlying provider dataset;
- making the content available to any other person, including through a shared workspace, assistant, channel, or published output; or
- using the content to build, market, operate, or support any product or service.
You are responsible for the tool you select and for its handling of content you transmit to it. Before transmitting content, satisfy yourself that the tool's terms and data practices are consistent with your obligations, including that it does not train on, retain, or redistribute what it receives.
Each report carries a disclosures object containing an informational notice, a provenance statement, and the source list, provided as structured fields so a client can present them correctly.
adviceDeclined is true when the run was read as a request for advice and SAGE declined that part before giving facts. It is reported as a field rather than as a sentence inside the summary prose, so a client can surface it in its own right; the summary itself always begins with findings.
Under Terms Section 14.1 these must not be removed, altered, obscured, suppressed, or detached from the content they accompany, and must be preserved wherever the content is displayed or read back. AstraEye Labs research is informational and educational only. It is not financial, investment, legal, or tax advice, is not a recommendation or solicitation, and is not personalized to your circumstances. Terms Sections 3 and 4 apply in full to all content returned by this API.
A token created with the SAGE chat permission (sage:chat) can also send text to your own SAGE conversation and read the replies. Messages sent this way appear in your SAGE chat on the web and in the app. Each reply is a SAGE answer and costs Astra Credits as one does, up to the daily credit limit you set when creating the token; limits reset at 00:00 UTC. SAGE can also act for you; see the next section. When a run finishes, the result arrives in the same conversation.
| Field | Type | Description |
|---|---|---|
| clientMessageId | string (UUID) | A new id you generate for each message. Resending the same id returns the same turn instead of asking again; reusing it for a different message returns 409. |
| text | string | Your message: up to 1,000 characters, and not blank. |
| conversationId | string (UUID), optional | A conversation returned by this API. Omitted, your current SAGE conversation is used. |
Waits up to 50 seconds for the reply. Returns 200 with { turnId, conversationId, status: "completed", reply: { role, text } }, or 202 with status: "generating" when the answer is still being written.
The state of one turn: generating, completed (with reply, and pendingAction and actions as on the reply) or failed.
Messages in a conversation, oldest first. Parameters: conversationId (optional), cursor (from nextCursor, to receive only newer messages) and limit (1 to 50, default 20). Each message has id, role, text, createdAt, ordinal and kind: run_delivery (with runGroupId) for a finished run's result, chat otherwise. delivery: "unavailable" means finished runs could not be checked on this call; try again shortly.
SAGE chat includes letting SAGE act for you, the way it does in the app.
Through Claude or ChatGPT, start with "Ask SAGE" and say it the way you would in the app. For example:
- Understand your research: "Ask SAGE what my latest report says about Bitcoin's momentum", or "how the Momentum Scan reached its result".
- Choose what to run: "Ask SAGE which analysis would show who is holding ETH", or "what the Crypto Perpetuals Positioning template would tell me about BTC, and what it costs".
- Run it: "Ask SAGE to run a Momentum Scan on the Dow 30 now". The results arrive in your SAGE conversation when it finishes.
- Read first, then ask: "Read my latest report, then ask SAGE what its most important finding is". Your assistant reads your reports itself and brings the question to SAGE.
- Keep your watchlist in order: "Ask SAGE to add Solana to my watchlist", or "to remove Litecoin from it".
- Shape your projects: "Ask SAGE to create an empty project called Weekly Crypto", then "add a Momentum Scan to it", "make it cover my watchlist", "turn on the visual brief", "write a short description", "rename it", or "stop the run in it".
- Learn the concepts: "Ask SAGE what the RSI measures".
- In your language: ask in English, Russian or Spanish, and SAGE answers in the same language.
Name the assets, analyses and projects you mean, and when SAGE asks a question, answer it in your next message. SAGE explains research and never tells you what to buy or sell; schedules and support requests are handled in AstraEye on the web or in the SAGE app.
When your own message asks for it outright:
- starting a research run you ask to run now;
- adding an asset to your watchlist;
- refreshing your connected exchange balances;
- creating an empty research project you name;
- setting up a research project without running it.
A run on assets you do not track yet adds them to your watchlist first.
Anything SAGE only infers or suggests, and everything it proposes itself, is described first and happens only when your next message says yes to it:
- a run it suggests (on assets you do not track yet, the add and the run are proposed together, and the exact price follows your yes);
- changes to your research projects:
- adding analyses, as a new step or a new workflow, or removing them;
- changing what a project covers: your watchlist, your holdings, chosen assets or the wider market;
- turning report options on or off: the visual brief, prepared questions, audio, methodology and the test;
- renaming a project, changing its description, archiving or restoring it;
- removing watchlist assets;
- stopping a run.
Setting up, changing or removing a schedule is never available through the API; schedules are managed in AstraEye.
When SAGE is waiting for an answer, the reply asks in words and also carries pendingAction: { id, kind, summary, priceCredits?, expiresAt }, where kind is one of:
- change: project or watchlist changes;
- run: a research run at the exact price in priceCredits;
- choice: which of several assets you meant.
Answer with an ordinary message: yes confirms, no declines, and a choice is answered by naming the asset you meant. A question is answered by the very next message or not at all, so confirm only what you have just been asked. A proposal that includes a run is confirmed twice: the changes, then the price. A reply that did something carries actions, a list of { type, status, detail }.
Through a connected assistant such as Claude or ChatGPT, SAGE acts exactly as it does for a token: what a message asks for outright happens at once, and what SAGE proposes happens when the next message agrees. Whether the assistant asks you first is set in the assistant. Claude asks you before each message it sends to SAGE unless you allow it to send without asking in its connector settings; if you do, SAGE acts as soon as Claude sends the request, and that choice is yours. In ChatGPT, its own settings decide when it checks with you. Whatever you choose, nothing through a connection can spend more than the daily credit limit you set for it.
- A run's price is held before it is launched, against this access's daily credit limit and your account's total API spending today.
- It is charged only when the Research Lab accepts the run.
- If the Research Lab rejects it, or it expires or is cancelled before it starts, the held credits are released. If the outcome cannot be confirmed, they stay held for that day.
- If the price does not fit in what is left of the limit, nothing starts and SAGE says so.
- Stopping a run does not refund the part that did not run: the credits it was charged stay spent.
- For each run started this way, AstraEye sends an in-app notice and an email with what it cost, and its results arrive in your SAGE conversation when it finishes.
| Operation | Short window | Daily |
|---|---|---|
| List projects, reports, analyses or templates, or read one analysis or template (per token or connection) | 60 per 5 minutes | 2,000 |
| Retrieve a report summary, methodology or test (per token / per account) | 40 / 60 per 5 minutes | 300 / 400 |
| Send a SAGE message (per token / per account) | 60 / 90 per 5 minutes | 1,000 / 1,500 |
| Check a SAGE turn (per token / per account) | 120 / 180 per 5 minutes | 5,000 / 8,000 |
| Read SAGE messages (per token / per account) | 60 / 90 per 5 minutes | 2,000 / 3,000 |
Listing is limited per token, or per connection for a connected assistant. Report reads (summary, methodology and test) share one allowance, and SAGE operations have the ones in the table. Both are limited per token or connection and also per account, across your token and any assistant you have connected. A request exceeding a limit returns 429 with a Retry-After header in seconds; honour that interval before retrying. Through a connected assistant, a limit comes back as an error in the tool result instead. These are the published limits referred to in Terms Section 14, and AstraEye Labs may adjust them under Section 14.1.
Errors are returned as { "error": { "code": "…", "message": "…" } }. A failure is never represented as an empty result set.
| Status | Code | Condition |
|---|---|---|
| 401 | invalid_token | The credential is missing, malformed, unrecognized, revoked, or expired. Issue a new token in settings. |
| 400 | unsupported_language | The language parameter names a language this report is not available in. The message lists the languages that are, and the same list is on the report index as availableLanguages. |
| 403 | tier_required | The account's plan does not include the Research API. The message names the plans that do. |
| 403 | account_ineligible | The account is not currently eligible, for example where it is suspended or a legal disclosure requires acceptance in the browser. |
| 404 | not_found | The Research API is not enabled for this account, the requested report does not belong to it, or the report has no summary, methodology or test to return. |
| 422 | unavailable_for_export | The report cannot be delivered through the API because part of it is not written prose. It is available in full on the results page in the browser. |
| 429 | rate_limited | A rate limit was exceeded, or SAGE is busy for this account. Honour the Retry-After header when it is present. |
| 503 | upstream_unavailable | The research service is temporarily unreachable. Retry after a short interval. |
| 400 | invalid_request | The SAGE message body is not exactly { clientMessageId, text, conversationId? }, or a parameter is out of range. |
| 400 | invalid_cursor | The cursor does not belong to this conversation. |
| 402 | daily_limit_reached | This access's daily credit limit, counted against your account's total API spending today, is reached or is 0. It resets at 00:00 UTC. |
| 402 | insufficient_credits | The account does not have enough Astra Credits for a SAGE reply. |
| 403 | insufficient_scope | The token was not created with the sage:chat permission. |
| 404 | conversation_not_found | The conversation does not exist or is not yours. |
| 404 | turn_not_found | The turn does not exist or is not yours. |
| 409 | idempotency_conflict | This clientMessageId was already used for a different message. |
| 503 | credits_unavailable | The Astra Credit balance could not be checked, so nothing was charged. Retry shortly with a new clientMessageId. |
| 503 | account_unavailable | The account cannot use SAGE right now. Sign in to AstraEye to review it. |
| 502 | sage_unavailable | SAGE could not answer. Send the message again with a new clientMessageId. |
One token may be active per account. Issuing a replacement invalidates the previous token immediately. Tokens expire 90 days after issue, and a notice is sent in advance of expiry. Settings displays recent API activity for the account; where activity is not recognized, revoke the token there. Token creation and revocation each generate a security notification, and resetting your password or signing out of all sessions also revokes the token and ends every assistant connection. Connections are otherwise separate from the token, and each can be disconnected on its own in Settings.
AstraEye Labs may limit, suspend, or discontinue the Research API, revoke any token, and adjust rate limits and quotas, including where it reasonably believes Terms Section 14.1 has been breached or where required by a data provider's terms. Breach of Section 14.1 is a breach of the Terms of Service and may result in termination under Section 21.