> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keenable.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Agents

> A web research agent you call over HTTP or MCP — send a question, get back an answer the agent found with Keenable search and fetch

This service runs a **web research agent** for you. You send a question; the agent searches
the web with Keenable, reads the pages it needs, and returns an answer with the URLs it
cites — as text, or as JSON that matches a schema you give it. Every tool call and message
the agent used is saved, so you can read how it got to the answer.

It is a **separate service** from the [Keenable MCP server](https://docs.keenable.ai/mcp-server.md), at its own URL:

```
https://agents.keenable.ai
```

There are three ways to call it:

| Interface | Use it when |
| - | - |
| [`POST /v1/answer`](#answers) | You want one request that waits and returns the answer. |
| [`POST /agent/runs`](#runs) | You want to start a run, poll it or stream its events, and read its trajectory. |
| [MCP at `/mcp`](#mcp) | Your agent should hand research to this agent as a tool. |

The runs API follows the shape of the [Exa Agent API](https://exa.ai/docs/agent/quickstart),
so a client written for it needs few changes. It does not take `input`, `dataSources` or
`budget`, and has no `stop` endpoint — use [`cancel`](#manage-runs). The run `error` field
and the [trajectory](#manage-runs) are additions.

## Authentication

Send your Keenable API key in `X-API-Key`, or as `Authorization: Bearer <key>`, on every
request. Create a key in the [console](https://app.keenable.ai/console) — see
[authentication](https://docs.keenable.ai/authentication.md).

```bash theme={"system"}
export KEENABLE_API_KEY=keen_<your_key>
```

The agent's web searches and page fetches are billed to that key, the same as if you made
them yourself (see [credits](https://docs.keenable.ai/credits.md)). Each run belongs to your organization; other
organizations get `404` for it.

## Answers

`POST /v1/answer` starts a run and waits for the answer:

```bash theme={"system"}
curl -s https://agents.keenable.ai/v1/answer \
  -H "X-API-Key: $KEENABLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"query": "Who is the CEO of Nvidia?", "effort": "low"}'
```

The request and response of this endpoint use snake\_case keys. Unknown fields get `400`.

| Field | Description |
| - | - |
| `query` | The question, up to 50,000 characters. Required. |
| `effort` | `minimal`, `low`, `medium`, `high`, `auto` (default) or `ultra`, as for runs. `mode` is an alias. See [effort](#effort). |
| `response_format` | Optional. `{"type": "json_schema", "json_schema": {"schema": {...}}}` with a JSON Schema for an object. |
| `system_prompt` | Optional extra instructions. |
| `stream` | `true` to get server-sent events instead of one response. |

The response has:

| Field | Description |
| - | - |
| `answer` | The answer text. |
| `effort` | The effort level of the run. |
| `model` | The model the run used. |
| `output` | With a schema: the parsed object. |
| `citations` | The URLs the answer cites, as `{"url", "title"}`. |
| `search_results` | URL, title, published date and snippet from each search the agent made. |
| `usage` | `input_tokens`, `output_tokens`. |
| `cost_dollars` | `total`, `agent_compute` (model tokens) and `search` (search and fetch calls). |

With a schema:

```bash theme={"system"}
curl -s https://agents.keenable.ai/v1/answer \
  -H "X-API-Key: $KEENABLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "query": "Who is the CEO of Nvidia?",
    "effort": "low",
    "response_format": {"type": "json_schema", "json_schema": {"schema": {
      "type": "object",
      "required": ["ceo", "source_url"],
      "properties": {"ceo": {"type": "string"}, "source_url": {"type": "string"}}
    }}}
  }'
```

With `"stream": true`, the response is a stream of `data:` items, each with a `type`:

| `type` | Payload |
| - | - |
| `sources` | `sources` — the results of one search. |
| `search` | `query` — the agent ran a search. |
| `fetch` | `url` — the agent read a page. |
| `text` | `delta` — model text, sent in small batches as the model writes it. A batch can come from a step that later calls a tool, so use `done` for the final answer. |
| `done` | `result` — the full response, as without streaming. The last item. |
| `error` | `code`, `message`, `retryable` — the run failed. The last item. |

Without streaming, a run that does not complete returns `502`; see [errors](#errors).

## Runs

`POST /agent/runs` starts a run and returns `202` with the run at once, with status `queued`:

```bash theme={"system"}
curl -s https://agents.keenable.ai/agent/runs \
  -H "X-API-Key: $KEENABLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"query": "What is the latest stable Python release?", "effort": "medium"}'
```

The runs API uses camelCase keys. Unknown fields get `400`.

| Field | Description |
| - | - |
| `query` | The question, up to 50,000 characters. Required. |
| `systemPrompt` | Optional extra instructions, up to 10,000 characters. |
| `effort` | `minimal`, `low`, `medium`, `high`, `auto` (default) or `ultra`. See [effort](#effort). |
| `outputSchema` | Optional JSON Schema for an object, up to 20,000 characters. The agent returns matching JSON in `output.structured`; if it cannot, the run fails. |
| `previousRunId` | Optional ID of one of your completed runs. The new run continues it: the agent sees up to 8 earlier questions and answers. |
| `metadata` | Optional map of strings. The run stores it and returns it in `request`. |

Poll the run until `status` is `completed`, `failed` or `cancelled`:

```bash theme={"system"}
curl -s https://agents.keenable.ai/agent/runs/agent_run_<id> \
  -H "X-API-Key: $KEENABLE_API_KEY"
```

```json theme={"system"}
{
  "id": "agent_run_8e98a42588bf47ef90582afe5c0db29f",
  "object": "agent_run",
  "status": "completed",
  "stopReason": "schema_satisfied",
  "createdAt": "2026-09-30T22:28:19.327356Z",
  "completedAt": "2026-09-30T22:28:27.367625Z",
  "request": {
    "query": "Who founded Keenable?",
    "systemPrompt": null,
    "effort": "low",
    "outputSchema": null,
    "previousRunId": null,
    "metadata": {"team": "docs"}
  },
  "output": {
    "text": "Keenable was founded in 2025 by ...",
    "structured": null,
    "grounding": [
      {
        "field": "text",
        "citations": [
          {"url": "https://pulse2.com/keenable-raises-26-million-seed-...", "title": "Keenable Raises $26 Million Seed ..."}
        ]
      }
    ]
  },
  "usage": {"inputTokens": 5195, "outputTokens": 350, "searches": 1, "fetches": 1, "compactions": 0, "steps": 2},
  "costDollars": {"total": 0.00895425, "agentCompute": 0.00095425, "search": 0.008},
  "error": null
}
```

| Field | Description |
| - | - |
| `status` | `queued`, `running`, `completed`, `failed` or `cancelled`. |
| `stopReason` | `schema_satisfied` (the agent answered), `budget_reached` (the agent used all its steps, then answered without tools), `error` or `cancelled`. `null` until the run ends. |
| `request` | The request, with all defaults filled in. |
| `output` | `text`, `structured` (with `outputSchema`), and `grounding`: the URLs the answer cites, per field (`text` or `structured`). |
| `usage` | `inputTokens`, `outputTokens`, `searches`, `fetches`, `compactions` (times the agent summarized its history to stay inside the model's context) and `steps`. |
| `costDollars` | `total`, `agentCompute` (model tokens) and `search` (search and fetch calls). |
| `error` | `null`, or `{"code", "message", "retryable"}` for a failed run. See [errors](#errors). |

While a run is in progress, `usage` and `costDollars` show its state at the last saved step.

### Continue a run

Pass `previousRunId` to ask a follow-up question. The run must be yours and `completed`:

```bash theme={"system"}
curl -s https://agents.keenable.ai/agent/runs \
  -H "X-API-Key: $KEENABLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"query": "And when was it founded?", "previousRunId": "agent_run_<id>"}'
```

### Manage runs

| Endpoint | Result |
| - | - |
| `GET /agent/runs` | Your runs, newest first: `{"object": "list", "data", "hasMore", "nextCursor"}`. `limit` is 1–100 (default 20). For the next page, send `cursor=<nextCursor>`. |
| `GET /agent/runs/{id}` | The run. |
| `POST /agent/runs/{id}/cancel` | Cancels a queued or running run and returns it. A run that has ended comes back unchanged. |
| `DELETE /agent/runs/{id}` | Deletes the run and its events. Returns `{"id", "object": "agent_run.deleted", "deleted": true}`. |
| `GET /agent/runs/{id}/events` | The saved events of the run, with `limit` and `cursor` as above. See [stream events](#stream-events). |
| `GET /agent/runs/{id}/trajectory` | `id`, `messages` (the model's context) and `events`: every tool call, tool result and message the agent used. |

### Stream events

Add `Accept: text/event-stream` to the POST to get events as they happen:

```bash theme={"system"}
curl -sN https://agents.keenable.ai/agent/runs \
  -H "X-API-Key: $KEENABLE_API_KEY" \
  -H 'Content-Type: application/json' \
  -H 'Accept: text/event-stream' \
  -d '{"query": "Who founded Keenable?", "effort": "low"}'
```

| Event | When |
| - | - |
| `agent_run.created` | The run is queued. |
| `agent_run.started` | The run starts. |
| `agent_run.tool_call` | Once per tool call. |
| `agent_run.compacted` | The agent summarized its history. |
| `agent_run.text` | Answer text. |
| `agent_run.completed` | The run ended; the data is the full run. The last event. |
| `agent_run.failed` | The run failed; the data is `{"id", "status", "error"}`. The last event. |
| `agent_run.cancelled` | The run was cancelled. The last event. |

When no event comes for 15 seconds, the stream sends a `: keep-alive` comment. If the service
restarts during a run, the run continues: the stream sends `agent_run.started` again, and tool
calls from the step that did not end can come again.

`GET /agent/runs/{id}/events` returns the saved events as
`{"object": "list", "data": [{"id", "event", "data", "createdAt"}], "hasMore", "nextCursor"}`.
With `Accept: text/event-stream`, it sends the saved events and closes; it does not wait for
new ones. Send `Last-Event-ID: <id>` to get only the events after that one.

## Effort

Effort sets the maximum number of steps, the model, and how much the model reasons before
each reply. A step is one model call plus up to 5 parallel tool calls. When the steps run
out, the agent answers with what it found and the run's `stopReason` is `budget_reached`.
All three interfaces take the same values, and the default is `auto`.

| `effort` | Steps | Model | Reasoning |
| - | - | - | - |
| `minimal` | 4 | small | none |
| `low` | 7 | small | low |
| `medium` | 10 | small | high |
| `auto` (default) | 10 | small | high |
| `high` | 14 | large | high |
| `ultra` | 22 | large | max |

`xhigh` is no longer a level. A request with `xhigh` runs as `high`.

The large model costs more per token, so `costDollars.agentCompute` is higher from `high` up.
Searches and fetches cost the same at every effort level.

## MCP

The MCP server is at `https://agents.keenable.ai/mcp` (streamable HTTP). It has three tools:

| Tool | Arguments | Returns |
| - | - | - |
| `agent_run` | `query`, `effort`, `output_schema`, `system_prompt`, `previous_run_id` | The new run. |
| `agent_get` | `run_id` | The run status and output. |
| `agent_trajectory` | `run_id` | The saved tool calls and messages. |

`agent_run` returns at once. Call `agent_get` until the run ends.

```bash Claude Code theme={"system"}
claude mcp add keenable-agents \
  --transport http https://agents.keenable.ai/mcp \
  --scope user \
  --header "X-API-Key: keen_<your_key>"
```

```toml Codex theme={"system"}
[mcp_servers.keenable-agents]
url = "https://agents.keenable.ai/mcp"
http_headers = { "X-API-Key" = "keen_<your_key>" }
```

```json JSON theme={"system"}
{
  "mcpServers": {
    "keenable-agents": {
      "type": "http",
      "url": "https://agents.keenable.ai/mcp",
      "headers": {
        "X-API-Key": "keen_<your_key>"
      }
    }
  }
}
```

## Limits

Each organization can have **20 runs queued or running** at a time. A new run past that limit
gets `429`; retry when one of your runs ends. The MCP `agent_run` tool returns the limit as a
tool error.

## Errors

Every error body has the same shape:

```json theme={"system"}
{"error": {"type": "NOT_FOUND", "code": "RUN_NOT_FOUND", "message": "Run not found"}}
```

A request that fails validation also has `error.detail`, a list of `{"loc", "msg", "type"}`.

| Status | `type` | `code` | Meaning |
| - | - | - | - |
| `400` | `INVALID_REQUEST` | `INVALID_REQUEST` | Bad request — for example an unknown field or `effort`, a query that is too long, or a bad `cursor` or `Last-Event-ID` |
| `400` | `INVALID_REQUEST` | `INVALID_OUTPUT_SCHEMA` | The schema is not a valid JSON Schema for an object, or is over 20,000 characters |
| `400` | `INVALID_REQUEST` | `PREVIOUS_RUN_NOT_COMPLETED` | The run in `previousRunId` did not complete |
| `401` | `AUTHENTICATION_ERROR` | `AUTHENTICATION_ERROR` | Missing, unknown or revoked API key |
| `404` | `NOT_FOUND` | `RUN_NOT_FOUND` | The run does not exist, or belongs to another organization |
| `404` | `NOT_FOUND` | `PREVIOUS_RUN_NOT_FOUND` | The run in `previousRunId` does not exist, or belongs to another organization |
| `429` | `RATE_LIMIT_ERROR` | `CONCURRENCY_LIMIT_REACHED` | 20 runs already queued or running |
| `500` | `SERVER_ERROR` | `SERVER_ERROR` | Unexpected server error |
| `502` | `SERVER_ERROR` | the run error code | `/v1/answer` without streaming: the run did not complete. The body also has `retryable` |
| `503` | `SERVER_ERROR` | `SERVER_ERROR` | The key check is down; retry |

A failed run has `error: {"code", "message", "retryable"}`. The `agent_run.failed` event and
the streamed `error` item carry the same fields. If `retryable` is `true`, the same request
can succeed later.

| Code | Retryable | Cause |
| - | - | - |
| `timeout` | yes | The run took too long. |
| `llm_unavailable` | yes | The model could not be reached, timed out or was rate limited. |
| `llm_error` | no | The model rejected the request. |
| `search_unavailable` | yes | Keenable search could not be reached or was rate limited. |
| `search_error` | no | Keenable search rejected the request — for example a bad key. |
| `schema_mismatch` | yes | The answer did not match the schema, even after one repair. |
| `internal_error` | no | Any other error. |

`GET /health` needs no key. The OpenAPI schema is at
[`/docs`](https://agents.keenable.ai/docs).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.