---
title: "eve (HTTP & web)"
description: The default HTTP API for an agent, covering session routes, auth, and customization.
url: "https://eve.dev/docs/channels/eve"
docs_index: /llms.txt
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The eve channel is the framework's default HTTP API under `/eve/v1`. The terminal UI, [`useEveAgent`](/docs/guides/frontend), `curl`, and SDK clients use it to start sessions, send messages, and stream events. It also serves the health, agent info, callback, and task input routes.

Each deployment serves its own API with its own host and authentication policy. `eve.dev` hosts framework documentation only; it is not a shared API, authorization server, MCP server, or A2A server.

eve supplies the channel when `agent/channels/eve.ts` doesn't exist. Most apps never write this file. Add it only to change the defaults, usually who may call the routes:

```ts title="agent/channels/eve.ts"
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc } from "eve/channels/auth";

export default eveChannel({
  auth: [vercelOidc(), localDev()],
});
```

## Routes

The default eve channel serves these routes:

- `GET /eve/v1/health` (check whether the application is reachable)
- `GET /eve/v1/info` (inspect the agent)
- `POST /eve/v1/session` (create a session, optionally sending its first message)
- `POST /eve/v1/session/:sessionId` (send a follow-up, respond to an input request, or prepare an idle session with an empty body)
- `POST /eve/v1/session/:sessionId/cancel` (cancel the in-flight turn)
- `POST /eve/v1/session/:sessionId/clear` (clear the session's model history)
- `POST /eve/v1/session/:sessionId/compact` (compact the session's context)
- `POST /eve/v1/session/:sessionId/end` (retire the session)
- `GET /eve/v1/session/:sessionId/stream` (stream events as NDJSON)

Create a session, then put its returned ID in every follow-up, control, and stream path.

`POST /eve/v1/session/:sessionId` with no body or `{}` prepares an existing session to answer from the serving deployment. It checks the channel's auth policy, but calls no `onMessage` and sends no message. Preparation keeps the session ID, stream, history, and expiry, and doesn't interrupt running work. It returns `{ ok: true, status }`:

| `status`            | HTTP | Meaning                                                            |
| ------------------- | ---- | ------------------------------------------------------------------ |
| `ready`             | 200  | The session already runs on the serving deployment.                |
| `accepted`          | 202  | A request to move the session is queued; the move may not be done. |
| `unsupported`       | 200  | The World or the session can't move to the serving deployment.     |
| `no_active_session` | 200  | No running session has this ID.                                    |

A non-empty body is a message or answer, validated in full. eve rejects empty or invalid messages, an empty `answers` array, and payloads with neither input. See [Prewarming](../guides/frontend/use-eve-agent#prewarming) for client usage and deployment routing.

`GET /eve/v1/health` is public and returns `{ ok: true, status: "ready", workflowId: string }`. `GET /eve/v1/info` returns agent-info version 5. It checks the channel's auth policy, then serves only operator callers: `service` and `runtime` callers, and, when the channel's auth includes `localDev()`, callers it admits on a development server. Other callers, such as `user` or `anonymous` ones, get `403` with code `info_forbidden`, because the payload includes authored instructions, skill and schedule Markdown, and server paths. The TypeScript client throws `HealthResponseError` or `AgentInfoResponseError` on a malformed payload, and `ClientError` on a non-success response from either route.

### Request validation

Session creation, follow-up and input-response requests, and session controls reject unknown top-level JSON fields with `400 Bad Request`. The error names the unknown fields and lists the route's accepted fields. Field names are case-sensitive (`outputschema` is not `outputSchema`), and each route has its own set: `operationId` is accepted on session creation but rejected on follow-ups. A rejected request creates no session, sends no message, and applies no control. Correct the field or upgrade the server; don't strip behavior-changing options and retry.

Request bodies can't contain eve's own attachment markers (`eve-url:`, `eve-sandbox:`, and `chat-sdk-file:`). A request that does returns `400 Bad Request`.

Compact and clear accept no body fields; omit the body or send `{}`. Cancel accepts `turnId`, and end accepts `reason`. The unknown-field check applies to top-level fields, not to open payload objects such as `clientContext`. Delegated requests with a `callback` also accept and ignore an `activityObserver` field. Authentication and value validation still apply, including permission checks for forwarded caller identities and tool stubs.

Follow-up and input-response requests accept an `idempotencyKey` of 1 to 256 characters for retries that can't tell whether the first request was accepted. A repeat with the same key returns the original receipt and `deliveryId` without running the input again. A channel's [retried deliveries](./custom#retried-deliveries) differ: they get a new receipt whose result is `ignored`. The session remembers its last 1,024 keys, so use a new key for new input.

### Start and continue a session

Create a conversation session before its first message with a bodyless request:

```bash
curl -X POST https://<deployment>/eve/v1/session
# {"ok":true,"sessionId":"wrun_A"}
```

The create request requires route auth and fixes the session's audience and timeout. Fields that apply to one turn, such as `clientContext` and `outputSchema` need a message on the create request. See [Start a session](../concepts/sessions-runs-and-streaming#start-a-session) for the initialization boundary, first-turn identity, readiness response, and retry behavior.

Include an initial message to create the session and start its first turn in one request:

```bash
curl -X POST https://<deployment>/eve/v1/session \
  -H "Content-Type: application/json" \
  -d '{"message":"What is the weather in Paris?"}'
# {"ok":true,"sessionId":"wrun_A","deliveryId":"delivery_A"}
```

An authenticated caller that may retry a create request can pass an `operationId` so the session is created once. Once that session is active, the same `operationId` from the same caller returns it without sending the input again. Simultaneous requests can receive different session IDs, but only one runs the first turn. Retry after startup to get the final session ID. Anonymous callers can't use `operationId`. An `operationId` is free again once its session can no longer resume.

```bash
curl -X POST https://<deployment>/eve/v1/session \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"message":"What is the weather in Paris?","operationId":"order-4213-research"}'
# {"ok":true,"sessionId":"wrun_A","deliveryId":"delivery_A"}
```

A follow-up request accepts exactly one of `message` or `answers`. Use `answers` to answer a pending approval or question:

```bash
curl -X POST https://<deployment>/eve/v1/session/wrun_A \
  -H "Content-Type: application/json" \
  -d '{"answers":[{"interactionId":"req_A","optionId":"approve"}]}'
```

A follow-up message joins the running turn by default; see [Overlapping messages](/docs/channels#overlapping-messages). Set `turnPolicy: "queue"` on `eveChannel(...)` to make follow-ups wait for running turns to finish. `answers` only answer the requests they name.

| Session state                         | Response                                                                                         |
| ------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Unknown or terminal                   | `409` with `{"code":"session_not_active","error":"The session is no longer active.","ok":false}` |
| Workflow exists but inbox isn't ready | `409 session_not_ready`                                                                          |
| Built by another eve version          | `409 session_stranded` with the `eveVersion` that built it                                       |

### Send files

A message can mix text and file parts. A file part's `data` is base64, a `data:` URL, or a URL:

```bash
curl -X POST https://<deployment>/eve/v1/session/wrun_A \
  -H "Content-Type: application/json" \
  -d '{"message":[{"type":"text","text":"Summarize this report."},{"type":"file","mediaType":"application/pdf","filename":"report.pdf","data":"data:application/pdf;base64,JVBERi0xLjQK..."}]}'
```

`uploadPolicy` sets the size cap and allowed media types; omit it for 25 MB and every type. An inline file that breaks the policy returns `413` for size or `415` for type. Inline bytes also count toward the hosting platform's request body limit: 4.5 MB on Vercel Functions, so about 3.3 MB of file before base64.

For larger files, upload from the browser to your own storage and send the URL in the file part. Add `fetchFile` to load your URLs after eve accepts the message, so the bytes never pass through the request:

```ts title="agent/channels/eve.ts"
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc } from "eve/channels/auth";

export default eveChannel({
  auth: [vercelOidc(), localDev()],
  uploadPolicy: { maxBytes: 100 * 1024 * 1024 },
  async fetchFile(url) {
    if (!url.startsWith("https://uploads.example.com/")) return null;
    const response = await fetch(url, {
      headers: { authorization: `Bearer ${process.env.UPLOADS_TOKEN}` },
    });
    if (!response.ok) throw new Error(`Upload fetch returned HTTP ${response.status}`);
    return Buffer.from(await response.arrayBuffer());
  },
});
```

`fetchFile` follows the [custom channel contract](./custom#file-uploads): return bytes to save the file to the sandbox, or `null` for a URL that isn't yours. eve downloads a public `https:` URL itself and passes any other URL to the model as a note. `uploadPolicy` also checks fetched and downloaded files by their actual bytes and detected media type. A violating file reaches the model as a note instead of failing the request. See [Inbound attachments](../sandbox#inbound-attachments) for how eve stores files and shows them to the model.

### Stream events

Stream a session as newline-delimited JSON from `GET /eve/v1/session/:sessionId/stream`. The [session protocol](../concepts/sessions-runs-and-streaming#stream-a-session) defines the event set, envelopes, cursors, and reconnection behavior.

Following a stranded session's stream returns `409 session_stranded`, and the TypeScript client reports `ClientSessionStrandedError` without retrying. Pass `follow=false` to read the recorded history instead. The response ends at the last recorded event and carries the `x-eve-stream-tail-index` header. It doesn't change the session, so it works before you end the session.

Ending a stranded session tries to append these events, then closes the stream so existing readers finish:

1. `delivery.admitted` with `source: { control: "reset" }`, then `delivery.settled`, for the end request.
2. `session.ended` with `outcome: "failed"`, error code `session_stranded`, and cause `{ deliveryId }`.

A session started by an eve version before v27 stores v26 events, and the stream route serves them as v27 lines. Translation covers messages, reasoning, tool calls and results, approvals, questions, sign-ins, subagents, clears, and compactions.

- Streamed text previews aren't translated, so a running v26 session shows each part once it completes.
- Cursors count stored lines. A stored event with nothing to translate, such as a streamed delta, arrives as a `position` marker.
- When no running deployment can add to the stream, a follow ends at the last event with `stream.ended`, so clients stop reconnecting.

The World keeps an ended session's stream only for its retention period. After that, reading it returns `410 session_expired`, and the TypeScript client fails with a `ClientError` whose `code` is `"session_expired"`, without retrying.

### Cancel a turn

Post to `/eve/v1/session/:sessionId/cancel` to cancel the active turn. Include the observed `turnId` so a late request can't cancel a newer turn. Cancellation is asynchronous; confirm it on the stream as `turn.settled` with `outcome: "cancelled"`.

See [Cancel the in-flight turn](../concepts/sessions-runs-and-streaming#cancel-the-in-flight-turn) for receipts, HTTP status codes, subagent cancellation, and race behavior.

### Clear context

Post to `/eve/v1/session/:sessionId/clear` to cancel a running turn and every working task, then remove model-message history. The session ID, system prompt, tools, skills, state, limits, and sandbox stay. On a stranded session, clear returns `409 session_stranded` and changes nothing; use `end` instead. See [Compact, clear, and end](../concepts/sessions-runs-and-streaming#compact-clear-and-end).

### Compact context

Post to `/eve/v1/session/:sessionId/compact` to summarize context without sending a user message. The operation waits for a running turn to finish and reports its result on the stream. See [Compact, clear, and end](../concepts/sessions-runs-and-streaming#compact-clear-and-end).

<a id="reset-a-session" />

### End a session

Post to `/eve/v1/session/:sessionId/end` to end that session for good. Ending doesn't create a replacement; create a new session for a fresh conversation. On a stranded session, eve cancels the workflow run directly. See [Compact, clear, and end](../concepts/sessions-runs-and-streaming#compact-clear-and-end).

## Authentication

The `auth` option decides who can call `/eve/v1/info` and the session routes. The built-in helpers cover development and trusted infrastructure:

- `localDev()` accepts requests during local development.
- `vercelOidc()` lets the local CLI and your team's other deployments call a deployed agent.

Neither admits browser users or external clients in production. For a public app, wire the channel to your own auth: Clerk, Auth.js, OIDC/JWT verification, an API-key verifier, or any custom `AuthFn`. Use Vercel OIDC only when Vercel-issued deployment tokens are part of your trust model.

`eve init` scaffolds `agent/channels/eve.ts` with `vercelOidc()`, `localDev()`, and `placeholderAuth()`, which returns a setup-focused 401 in production until you replace it with real auth. Without the file, eve uses the same `[vercelOidc(), localDev(), placeholderAuth()]` policy, which rejects production requests without a valid Vercel OIDC token.

`/eve/v1/info` also requires an operator caller. To let your own operator credential read it, return `principalType: "service"` from its `AuthFn`, as `jwtHmac()` and `jwtEcdsa()` do, or return `allowAgentInfo: true` from an `EveAuthFn`. See [Agent info access](../guides/auth-and-route-protection#agent-info-access). Keep end-user strategies returning `"user"` without the grant, so end users can't read it.

For the full auth model and helper list, see [Auth & route protection](../guides/auth-and-route-protection).

## Web Chat with Sign in with Vercel

To let your team sign in to a browser chat, run `eve add channel/web` or select **Web Chat** from the terminal UI's channel picker. Then choose **Next.js**, a hosting layout, and **Sign in with Vercel**. Sign in with Vercel is available only for the Next.js Web Chat app. Setup creates a Vercel App on the linked project that only members of the project's team can sign in to.

For a new agent, initialize it first, then link a project and add Web Chat:

```bash
eve init my-agent --non-interactive
cd my-agent
eve link
eve add channel/web
```

`eve init --channel-web-nextjs` scaffolds the app without authentication; sign-in setup runs only through `eve add channel/web` or the channel picker.

The generated Next.js app signs users in with Better Auth. The agent's eve channel accepts the same sign-in, and still accepts Vercel OIDC and local development requests. In an agent workspace, setup configures only the selected agent.

Setup registers `/api/auth/callback/vercel` as a project redirect URI, so Vercel validates callbacks against the project's deployment domains. The app uses the `authorization_code` grant, `client_secret_post` authentication, and the `openid`, `email`, and `profile` scopes. Setup writes `VERCEL_APP_CLIENT_ID`, `VERCEL_APP_CLIENT_SECRET`, and `BETTER_AUTH_SECRET` to production and preview as sensitive values, and never prints them or writes them to source. Run `eve deploy` after setup to publish the app. Local development runs without browser sign-in.

For automation, supply the setup answers explicitly:

```bash
eve add channel/web --non-interactive \
  --answer 'web-hosting="vercel"' \
  --answer 'web-authentication="vercel"'
```

Setup needs a linked project and permission to manage Vercel Apps and project environment variables. If your role can't create apps, a team owner can complete setup. The team picker lists eligible teams first and disables teams marked `needs permission` or `couldn't verify` until you sign in or retry.

Setup reuses compatible existing apps and credentials, and stops with an error on conflicting app settings or credentials. It preserves authored channel and UI files unless you pass `--overwrite`.

| What you see                                    | What to do                                                                      |
| ----------------------------------------------- | ------------------------------------------------------------------------------- |
| Setup reports a missing prerequisite            | Complete it, then resume with `eve add channel/web --skip-install`.             |
| The team has reached its Vercel App limit       | Remove an unused app in team settings and retry.                                |
| The generated app name or slug is already taken | Rename the Vercel project in its settings and retry.                            |
| The app has reached its client-secret limit     | Remove an unused secret in the app's settings; setup never removes one for you. |

Choose **Configure authentication myself** (`web-authentication=custom`) to keep the current channel policy. The default placeholder rejects production browser requests until you configure your own authentication.

## CORS

The eve channel sends no CORS headers by default. When browser clients call the channel directly, pass `cors: true` for permissive CORS with preflight handling, or an options object to narrow origins, methods, and headers. Route auth still runs on the session requests.

```ts title="agent/channels/eve.ts"
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc } from "eve/channels/auth";

export default eveChannel({
  auth: [vercelOidc(), localDev()],
  cors: {
    origin: "https://app.example.com",
    methods: ["GET", "POST"],
    allowHeaders: ["authorization", "content-type"],
  },
});
```

## Audience

The eve channel sets each new session's audience from its caller. The audience controls whether traces may capture session content, not who may call the endpoint:

| Session creator                        | Default audience |
| -------------------------------------- | ---------------- |
| Anonymous caller                       | `unknown`        |
| `user`, `service`, or `runtime` caller | `private`        |
| Any other caller type                  | `unknown`        |

`user`, `service`, and `runtime` sessions are `private` because they belong to one identified party. For `unknown` sessions, traces record metadata but leave out content by default in preview and production. Set a constant `audience: "public"` only when every conversation on the endpoint is intentionally public:

```ts title="agent/channels/eve.ts"
import { vercelOidc } from "eve/channels/auth";
import { eveChannel } from "eve/channels/eve";

export default eveChannel({
  auth: [vercelOidc()],
  audience: "public",
});
```

`audience` also accepts a function that receives `caller`, channel, and deployment environment. The audience is fixed when the session is created, so a later turn from a different caller doesn't change it. For an audience that depends on channel state or verified application data, use a [custom channel audience](./custom#conversation-audience).

Audience also selects the default [run-data retention](../agent-config#run-data-retention). Outside development, private and unknown-audience sessions request deletion when they end, if the Workflow world supports deletion.

For a remote agent, a trusted caller can forward the original audience and its trace-content ceiling. See [Preserving trace content](../guides/remote-agents#preserving-trace-content).

## Customization

Use `onMessage` to add request-specific context before the agent sees the user message, and `events` to observe stream events from sessions this channel created:

```ts title="agent/channels/eve.ts"
import { eveChannel, defaultEveAuth } from "eve/channels/eve";
import { localDev, vercelOidc } from "eve/channels/auth";

export default eveChannel({
  auth: [vercelOidc(), localDev()],
  onMessage(ctx, message) {
    const callerId = ctx.eve.caller?.principalId ?? "anonymous";
    return {
      auth: defaultEveAuth(ctx),
      context: [`HTTP caller ${callerId} sent: ${message}`],
    };
  },
  events: {
    "turn.settled"(event, ctx) {
      console.log("eve turn settled", {
        outcome: event.data.outcome,
        sessionId: ctx.session.id,
      });
    },
  },
});
```

`onMessage` must return an auth result, so every accepted message starts or continues a session. Return `title` alongside `auth` to set the session title. It applies when the message creates the session, or is the first message to one created without a message.

### Turn context

`context` strings are saved as user messages in session history. For context that applies to one turn only, return `clientContext`. It replaces the request's [`clientContext`](../guides/client/messages) for every model call in the turn and isn't kept in session history.

`ctx.eve.clientContext` holds the request's `clientContext` as sent, or `undefined`. Use it to check or correct client-supplied state before the model sees it:

```ts title="agent/channels/eve.ts"
import { eveChannel, defaultEveAuth } from "eve/channels/eve";
import { vercelOidc } from "eve/channels/auth";

import { getVerifiedPlan } from "../lib/billing";

export default eveChannel({
  auth: [vercelOidc()],
  async onMessage(ctx) {
    const auth = defaultEveAuth(ctx);
    const plan = await getVerifiedPlan(auth);
    return { auth, clientContext: [`The user's plan is ${plan}.`] };
  },
});
```

- Omit `clientContext` to keep the request's context. Return `[]` to drop it.
- The returned value has the request's `ClientContextValue` shape, exported from `eve/channels/eve`: a string, an array of strings, or a JSON object.
- eve labels client-supplied entries `Client context:` for the model. Returned context is sent without the label.
- `onMessage` runs only for requests with a message. An answer keeps the context its request sent.

### Start from prior history

Pass `history` to bring in conversation eve didn't produce, such as a stored transcript or a branch of an earlier exchange. The model sees each entry as a real user or assistant turn.

Call `create()` on a channel address to create a session from stored history. No turn runs until a message arrives:

```ts title="agent/channels/app.ts"
import { defineChannel, POST } from "eve/channels";

export default defineChannel({
  routes: [
    POST("/conversations/:id/session", async (req, { from, params }) => {
      const auth = await authenticate(req);
      // Load only conversations this caller may read.
      const conversation = await db.conversations.findForUser(params.id!, auth);
      const { created, session } = await from(`conversation:${conversation.id}`).create({
        auth,
        history: conversation.messages.map((message) => ({
          id: message.id,
          role: message.role, // "user" | "assistant"
          text: message.body,
        })),
      });
      if (!created) {
        return Response.json({ error: "Conversation already has a session." }, { status: 409 });
      }
      return Response.json({ sessionId: session.id });
    }),
  ],
});
```

The client then resumes the session by id, for example with `useEveAgent({ initialSession: { sessionId, streamIndex: 0 }, resume: true })`. `created` is `false` when another session already owned the address or another `create()` or `send()` claimed it first. That session comes back unchanged, without your `history`. Use a new address for each new conversation or branch.

`create()` returns once the new session claims its address, so an immediate `send()` reaches it. It throws if no session claims the address within 15 seconds. On a [stranded](../guides/deployment/self-hosted-operations#recover-a-stranded-session) session, `create()` throws `SessionStrandedError` instead of replacing a conversation you may mean to import; call `end()` on the address, then `create()` again.

`send()` accepts the same `history` option and adds the entries right before that message and its `context`, whether it creates or continues the session. The eve channel's `onMessage` can return `history` too, which applies to every message it's returned for.

Each entry has a `role` of `"user"` or `"assistant"`, non-empty `text`, and an optional `id` unique within the call. eve rejects an entry with any other field, so map your stored messages to these fields. Invalid history throws an `InvalidSessionHistoryError`, exported from `eve/channels`, naming the entry and the broken rule. eve doesn't cap entries or text, so trim long conversations in your route. Tool calls, files, and reasoning are not supported.

eve never accepts `history` from a client request body. Resolve it on the server and authorize the caller against the stored conversation.

The stream records the entries in `data.history` on the `delivery.consumed` of the message that brought them. History from `create()` is recorded with the first accepted message. When one turn takes in several messages, the model reads all their history first. `transcriptOf()` from `eve/client` returns entries in that order as `{ role, text }` messages you can pass back as `history` to branch a session.

Frontend `messages` show imported entries before their message, with `metadata.imported` set and your `id` in `metadata.historyId`. Imported messages have no `metadata.turnId`, so a turn's reply never streams into one. Their message ids stay unique even when a later call reuses an `id`. Compaction, `clear()`, and handoffs treat them like any other history.

## Replace or disable the defaults

An authored `agent/channels/eve.ts` replaces the whole default eve channel. An `eveChannel(...)` replacement keeps the standard routes with your options. A `defineChannel(...)` replacement serves only the routes you declare, without the health, info, or callback routes.

Export `disableRoute()` to remove every `/eve/v1` route:

```ts title="agent/channels/eve.ts"
import { disableRoute } from "eve/channels";

export default disableRoute();
```

The default home page at `GET /` and `HEAD /` is a separate `home` channel. Author `agent/channels/home.ts` to replace it, or export `disableRoute()` there to remove it without affecting `/eve/v1`.

## Clients

The browser side of this API lives in the [Frontend](/docs/guides/frontend) docs, where `useEveAgent` drives the eve channel from React UI.

For scripts, server-to-server calls, evals, tests, and custom clients, use the [Client SDK](/docs/guides/client), which wraps the session routes, stream cursor, and reconnect loop.

## What to read next

- [Frontend](/docs/guides/frontend): drive the eve channel from browser UI with `useEveAgent`
- [Client SDK](/docs/guides/client): call the eve channel from TypeScript
- [Auth & route protection](../guides/auth-and-route-protection): the route auth policy
- [Sessions, runs & streaming](../concepts/sessions-runs-and-streaming): the routes this channel exposes

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)