Research API
Start an asynchronous research job: AlterLab searches, reads sources and returns a cited answer within the budget you set. Planning and synthesis run on your own LLM key.
Bring your own key
byok_key_required with HTTP 400, nothing is stored and you are not charged. Model tokens are billed by your provider, never by AlterLab.Overview
- Jobs are asynchronous:
POSTreturns 202 with a job id; poll the job, stream events, or register a webhook. - Every job runs under a hard budget (cost, tool calls, runtime, depth, sources). When a cap is reached the job ends as
completed_partialwith a coverage report. - Job statuses:
awaiting_approval,queued,running,completed,completed_partial,failed,cancelled. - Responses never name the search sources behind a result. Auth uses the same
X-API-Keyheader as every other endpoint (scope:scrape; write for create, read for fetch).
POST /api/v1/research
/api/v1/researchStart a research job. Returns 202 with the job, its resolved budget and, when a webhook is set, the webhook secret (returned once).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
| query | string | Required | Research question, 1-4000 characters. |
| effort | string | Optional | lite, standard, deep, exhaustive or frontier. See Effort Tiers.Default: standard |
| max_cost_microcents | integer | Optional | Hard cap on platform spend (1 microcent = one millionth of a dollar). Lowers the effort tier cap. |
| max_tool_calls | integer | Optional | Cap on searches plus fetches. |
| max_runtime_seconds | integer | Optional | Wall-clock cap. |
| max_depth | integer | Optional | Maximum follow-up rounds, 1-10. |
| max_sources | integer | Optional | Maximum sources read, 1-500. |
| output | string | Optional | json (structured answer), report (cited prose) or both.Default: json |
| citation_format | string | Optional | numbered, mla, apa or chicago.Default: numbered |
| output_length | string | Optional | short, standard or long.Default: standard |
| criteria | string[] | Optional | Up to 20 free-text criteria (1-500 characters each). The result reports each as met or unmet. |
| min_sources | integer | Optional | Independent sources required per field, 1-10.Default: 1 |
| sources | object | Optional | { include_domains (soft boost), exclude_domains (hard), seed_urls, only_these_urls (read only the seed URLs, no discovery searches), categories (web, news, academic, code, discussions) }. |
| output_schema | object | Optional | JSON Schema (object) for the structured answer. |
| files | object[] | Optional | Up to 50 caller files, { name, text }, searched as sources and cited as file:// spans. Requires tools.file_search. |
| tools | object | Optional | { file_search (default true), code_execution (default true), mcp (up to 5 remote HTTPS MCP servers, each { name, url, allowed_tools }) }. Each call is budgeted, metered and refunded if empty. |
| plan_first | boolean | Optional | Return a plan and wait for approval before any spend.Default: false |
| webhook | object | Optional | { url (HTTPS), secret (optional, 16-256 chars) }. Fired on terminal status. |
| provider | string | Optional | BYOK provider for planning and synthesis. |
| model | string | Optional | BYOK model override. |
Request Example
curl -X POST https://api.alterlab.io/api/v1/research \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "How do vector databases handle filtered search?",
"effort": "standard",
"max_cost_microcents": 20000
}'Response Example
{
"id": "9f1c2b7e-3a5d-4c8e-9b1a-2d6f7e8a9b0c",
"status": "queued",
"effort": "standard",
"budget": {
"max_cost_microcents": 20000,
"max_tool_calls": 40,
"max_runtime_seconds": 120,
"max_depth": 2,
"max_sources": 20
},
"cost": { "platform_microcents": 0, "model_microcents": 0 },
"tool_calls": 0,
"created_at": "2026-11-04T12:00:00Z"
}Unknown request fields are rejected. The create call returns 400 invalid_webhook_url when the webhook URL fails validation.
GET /api/v1/research
Lists your research jobs, newest first. Query parameters: limit (1-100, default 20) and cursor (the next_cursor from the previous page). The response is { "items": [...], "next_cursor": "..." | null }. A malformed cursor returns 422 invalid_cursor.
GET /api/v1/research/{id}
/api/v1/research/{id}Fetch a job. Terminal jobs include result, coverage, stop_reason and the final cost.
Response Example
{
"id": "9f1c2b7e-3a5d-4c8e-9b1a-2d6f7e8a9b0c",
"status": "completed",
"effort": "standard",
"tool_calls": 31,
"stop_reason": "answered",
"cost": { "platform_microcents": 14300, "model_microcents": 0 },
"result": { "...": "answer and citations" },
"coverage": { "...": "what was and was not read" },
"finished_at": "2026-11-04T12:01:12Z"
}Unknown ids return 404 research_not_found. The result and coverage objects are free-form JSON; read them defensively.
POST /api/v1/research/{id}/cancel
Cancels a job that has not finished. Returns the job with status cancelled. A job that already finished returns 409 research_already_finished.
DELETE /api/v1/research/{id}
Permanently deletes a finished job together with its events and queued webhook deliveries. Returns 204 with no body. A job that is still active returns 409 research_active; cancel it first. Unknown ids return 404 research_not_found.
POST /api/v1/research/{id}/rerun
Starts a new job from the stored request of an existing one and returns it with status 202, exactly like POST /api/v1/research. The body is optional: { "overrides": { ... } } replaces top-level request fields, and a null value clears one. The stored request never holds the webhook, so pass webhook in overrides to attach one. The merged request is validated again: invalid overrides return 422 invalid_rerun_overrides. The new job is billed like any other submission.
curl -X POST https://api.alterlab.io/api/v1/research/JOB_ID/rerun \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"overrides": {"effort": "deep"}}'GET /api/v1/research/{id}/events
Server-sent events: status, progress, thinking_summary, plan and a final research.<status> event, after which the stream closes. Each event id is monotonic per job. Reconnect with the Last-Event-ID header (or ?last_event_id=) to receive only the events you missed. Events are kept for the life of the job.
curl -N https://api.alterlab.io/api/v1/research/JOB_ID/events \
-H "X-API-Key: YOUR_API_KEY" \
-H "Last-Event-ID: 12"Plan Steering
With plan_first: true the job waits in awaiting_approval and nothing is spent until you approve.
GET /api/v1/research/{id}/plan: read the plan.PUT /api/v1/research/{id}/plan: replace it with body{ "plan": { ... } }.POST /api/v1/research/{id}/approve: start the job; an optionalplanin the body replaces the plan first.
Plan endpoints on a job that is not awaiting approval return 409 research_not_awaiting_approval; an approval that raced its expiry returns 409 research_plan_expired.
Webhooks
On terminal status AlterLab POSTs the job to your HTTPS URL with these headers: X-AlterLab-Event, X-AlterLab-Timestamp and X-AlterLab-Signature: sha256=<hex>, where the signature is HMAC-SHA256 of {timestamp}.{body} with your secret. The secret is returned once, in the create response, unless you supplied your own.
import hashlib, hmac
def verify(secret: str, timestamp: str, body: bytes, header: str) -> bool:
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", header)Effort Tiers and Budgets
| Effort | Searches | Pages | Depth | Tool calls | Runtime cap | Cost cap | Typical |
|---|---|---|---|---|---|---|---|
| lite | 2 | 5 | 1 | 10 | 30 s | $0.005 | 5-30 s |
| standard | 6 | 20 | 2 | 40 | 120 s | $0.050 | 30-120 s |
| deep | 15 | 60 | 4 | 120 | 600 s | $0.100 | 2-10 min |
| exhaustive | 40 | 200 | 6 | 400 | 1800 s | $0.450 | 10-30 min |
| frontier | 120 | 500 | 10 | 1500 | 7200 s | $1.200 | 30 min-2 h |
Your own max_* values can only lower a tier cap. The cost cap covers platform spend only.
Pricing
| Action | Cost | Notes |
|---|---|---|
| Search run by a job | $0.001 per search (1,000 microcents) | Same flat price as a unified search query |
| Page fetch or extraction | Scrape tier price of the URL | Priced per step; the job total is in cost.platform_microcents |
| Model tokens | Not billed by AlterLab | Run on your own key and billed by your provider |
New accounts get up to 5,000 free requests. See pricing.
Errors
| HTTP | error | Meaning |
|---|---|---|
| 400 | byok_key_required | No LLM key on the account. Nothing stored or charged. |
| 400 | invalid_webhook_url | Webhook URL failed validation. |
| 404 | research_not_found | Unknown job id for this account. |
| 409 | research_already_finished | Cancel on a job that already finished. |
| 409 | research_not_awaiting_approval | Plan read, edit or approve on a job that is not awaiting approval. |
Examples
import time
import requests
API = "https://api.alterlab.io/api/v1/research"
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
job = requests.post(
API,
headers=HEADERS,
json={"query": "How do vector databases handle filtered search?", "effort": "standard"},
).json()
while job["status"] in ("awaiting_approval", "queued", "running"):
time.sleep(3)
job = requests.get(f"{API}/{job['id']}", headers=HEADERS).json()
print(job["status"], job["cost"]["platform_microcents"])
print(job["result"])Python, Node and MCP
The SDKs wrap every endpoint above. Research runs on your own model key; without one the call returns byok_key_required and nothing is charged.
from alterlab import AlterLab
client = AlterLab(api_key="YOUR_API_KEY")
job = await client.research(
"Which EU countries ban fur farming?",
effort="standard",
max_cost_microcents=50_000,
)
job = await client.wait_for_research(job["id"])
print(job["status"], job["cost"], job["result"])
# plan_first jobs: client.get_research_plan(id), update_research_plan(id, plan), approve_research(id)
# progress: await client.research_events(id, last_event_id="12")In the hosted MCP server, the alterlab_research tool starts a job (or fetches one with job_id), waits within the tool time budget and returns the job. A job still running is returned with its id so the model can call again; nothing is cancelled.