---
title: "Drive It Over HTTP"
description: "Talk to the dispatcher over eve's stable HTTP session API: start a durable session, stream the NDJSON event log, and send a follow-up to that session."
canonical_url: "https://vercel.com/academy/building-agents-with-eve/drive-it-over-http"
md_url: "https://vercel.com/academy/building-agents-with-eve/drive-it-over-http.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-11-03T03:33:28.773Z"
content_type: "lesson"
course: "building-agents-with-eve"
course_title: "Building Agents with eve"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# Drive It Over HTTP

# Drive It Over HTTP

The TUI (terminal user interface) is a comfortable place to test an agent, but no customer is ever going to open your terminal. A real dispatcher gets reached over the wire: from a web page, a Slack message, a phone. Before we wire up any of those surfaces, it's worth seeing the thing they all sit on top of.

Every eve app exposes the same HTTP session API, regardless of the interface built on top. The TUI and the dashboard are both clients of the routes that start sessions, stream events, and send follow-ups. Slack gets its own webhook route, but it drives the same durable sessions underneath.

So let's skip the surfaces for a second and talk to the dispatcher the way every surface does, with plain `curl`.

## Outcome

You start a durable session with the dispatcher over HTTP, stream its reply as events, and send a follow-up to the same session.

## Hands-on exercise

With `npx eve dev` running, open a second terminal. There's nothing to write this lesson, you're learning the contract every channel speaks, so we drive it by hand.

**Start a session.** Send the customer's first message:

```bash
curl -X POST http://127.0.0.1:2000/eve/v1/session \
  -H 'content-type: application/json' \
  -d '{"message":"what does a basic tune-up cost?"}'
```

The response should look like this:

```json
{"ok":true,"sessionId":"wrun_01...","deliveryId":"delivery_01..."}
```

That's an HTTP `202 Accepted`: eve took the message and started the turn in the background. It is not the dispatcher's answer. Copy the **`sessionId`** from this response; you use it to stream the answer and to address the next message to this conversation. The `deliveryId` names this one message, so a client can match it to the stream later.

If zsh prints `%` immediately after the closing brace, that is just your shell prompt touching curl's newline-free JSON output; it is not part of the response.

\*\*Note: Accepted is a handoff, not the answer\*\*

The first request returns a `202` quickly with a session handle while the durable turn runs in the background. Seeing a `sessionId` and `deliveryId` with no reply text is expected. Attach to the stream with the returned `sessionId` to read what the dispatcher says.

**Stream the session.** Point at the stream route with the id from the JSON response. `-N` tells curl not to buffer the events:

```bash
curl -N http://127.0.0.1:2000/eve/v1/session/<sessionId>/stream
```

You get newline-delimited JSON. Each line is a **commit**, `{"at": ..., "facts": [...]}`, holding every fact one state change produced, or a **progress** record carrying streamed text as it's typed. The facts tell the whole story: the message arriving, the turn starting, the `lookup_service` call going out and settling, the finished reply text, and the turn settling. The TUI renders this same log as the `⚙` lines you saw earlier.

The stream stays open after the turn ends, sending an occasional `{"$eve":"heartbeat"}` line, because the session can take more messages. Press Ctrl-C when you've seen enough.

**Send a follow-up.** Once the stream shows `turn.settled`, post the next message to that same session ID:

```bash
curl -X POST http://127.0.0.1:2000/eve/v1/session/<sessionId> \
  -H 'content-type: application/json' \
  -d '{"message":"and how long does it take?"}'
```

The dispatcher retained the session history, so it already knows you're asking about the tune-up.

## Try It

The first `POST` returns something like this:

```text
{"ok":true,"sessionId":"wrun_01...","deliveryId":"delivery_01..."}
```

The raw commit lines are long, so let's read only the fact types. Pipe the stream through `jq` (install it with your package manager if you don't have it):

```bash
curl -sN http://127.0.0.1:2000/eve/v1/session/<sessionId>/stream \
  | jq -r '.facts[]?.type'
```

```text
delivery.admitted
session.started
delivery.consumed
turn.started
model.requested
model.started
call.requested
call.started
call.settled
usage.recorded
model.settled
model.requested
model.started
content.completed
usage.recorded
model.settled
turn.settled
delivery.settled
```

Your list may run a little longer or shorter, since the model decides how many steps it needs. Read it top to bottom: the message is admitted, the turn starts, the model calls `lookup_service` (`call.requested` through `call.settled`), a second model run writes the reply (`content.completed`), and the turn settles. That `turn.settled`, with `outcome: "completed"`, is the agent telling you it's done with this turn and ready for the next message. The same durable machinery will let a turn pause for human approval in Section 3 and pick up days later.

\*\*Note: Send one turn at a time\*\*

For predictable ordering, wait for `turn.settled` before sending the next message to the same session. A message sent mid-turn steers the turn that's already running instead of starting a new one, which is rarely what you want while you're learning the contract.

Getting a `401`? The dev server accepts local requests, but make sure you're hitting `127.0.0.1`/`localhost` and not a deployed URL. Getting a `404` from the stream route, or a `409` with `session_not_active` from the follow-up route? Check that you copied the complete `sessionId` from the first response. A `409` with `session_not_ready` means you sent the follow-up before the session's inbox finished starting. That's expected right after the first `POST`; wait a second and send it again.

## Done-When

- [ ] `POST /eve/v1/session` returns `202` with a `sessionId` and `deliveryId`.
- [ ] Streaming the session prints NDJSON commit lines, including `call.requested` for `lookup_service` and `turn.settled`.
- [ ] A follow-up `POST` to that session ID continues the conversation without repeating context.
- [ ] You can explain why the first `POST` returns a handle instead of the assistant's answer.

## Solution

There's no code to write here. The agent exposed these routes when you ran it, and every channel uses the same contract:

| Route                            | What It Does                                                                |
| -------------------------------- | --------------------------------------------------------------------------- |
| `POST /eve/v1/session`           | Start a durable session; returns `202` with a `sessionId` and `deliveryId`. |
| `GET /eve/v1/session/:id/stream` | Stream the run as NDJSON events (reconnectable).                            |
| `POST /eve/v1/session/:id`       | Send a follow-up to the same durable session.                               |

Hold onto this picture. In Section 4 you'll put a web dashboard and Slack in front of the dispatcher, and both drive the same durable sessions. `useEveAgent` calls these same routes from the browser. The Slack channel receives events on its own webhook route.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
