# A guided tour with real responses

Every request on this page was run against a real nikcli server, and every response is what actually came back.

## A throwaway server

The fastest way to learn this API is to hit it. Point `NIKCLI_TEST_HOME` somewhere disposable and the server keeps its sessions, config and database there instead of in your real one — so you can create and delete freely.

Terminal

bash 

```
mkdir -p ~/demo && cd ~/demo && git init -q
printf 'export function greet(name: string) {\n  // TODO: localise this\n  return `Hello, ${name}!`\n}\n' > greet.ts
printf '# demo\n' > README.md
git add -A && git commit -qm initial

# an isolated home, so nothing here touches your real sessions or config
NIKCLI_TEST_HOME=~/demo/.nikcli-home \
  nikcli serve --hostname 127.0.0.1 --port 4199
```

$ shell · paste into your terminal

Everything below is copied from a real run

The responses on this page were captured from exactly that setup. The only edit is cosmetic: the long temporary paths were rewritten to `/Users/me/demo`, and two very long arrays are marked where they were trimmed.

## Is it alive

Health is instance-less — it needs no project and no auth on loopback, which makes it the right thing to poll while waiting for a server to come up.

Terminal

bash 

```
curl -s http://127.0.0.1:4199/global/health
```

$ shell · paste into your terminal

response

json 

```
{"healthy":true,"version":"local","revision":"local"}
```

`version` reads `local` here because this server was run from a source checkout; an installed build reports its release.

## Where am I

`/path` answers the question every integration asks first: which directories is this instance actually using? Note `home` pointing at the throwaway — that is the isolation working.

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/path?directory=$PWD"
```

$ shell · paste into your terminal

response

json 

1

2

3

4

5

6

7

```
{
  "home": "/Users/me/demo/.nikcli-home",
  "state": "/Users/me/demo/.nikcli-home/state",
  "config": "/Users/me/demo/.nikcli-home/config",
  "worktree": "/Users/me/demo",
  "directory": "/Users/me/demo"
}
```

`/project/current` resolves the project for the directory you selected. The `id` is a stable hash of the worktree, so it is the same on every call and across restarts.

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/project/current?directory=$PWD"
```

$ shell · paste into your terminal

response

json 

1

2

3

4

5

6

7

8

```
{
  "id": "c6d9c4f98cc34c8bfb0a40b3a60eca6f624750e6",
  "worktree": "/Users/me/demo",
  "canonical": "/Users/me/demo",
  "vcs": "git",
  "time": { "created": 1789656731762, "updated": 1789656731762 },
  "sandboxes": []
}
```

request + response

bash 

```
curl -s "http://127.0.0.1:4199/vcs?directory=$PWD"
{"branch":"main"}
```

$ shell · paste into your terminal

## What can it do

The tool registry is the honest answer to "what can the agent actually do here". This is the real list from a default install — and the endpoint a plugin author checks to confirm a registered tool arrived.

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/experimental/tool/ids?directory=$PWD"
```

$ shell · paste into your terminal

response

json 

1

2

3

4

5

6

```
["invalid","question","bash","monitor","read","tree","glob","grep","edit","write",
 "multiedit","task","delegation","context_collect","context_related","context_diagnostics",
 "memory_search","generate_image","artifact","webfetch","todowrite","todoread","create_goal",
 "get_goal","update_goal","websearch","codesearch","repo_clone","repo_overview","skill",
 "apply_patch","lsp","plan_exit","plan_enter","speak","voice","opentui","plugin","advisor",
 "delegator","search_tools","code_mode","browser_control","computer","herdr"]
```

`/agent` returns each agent with its resolved permission table, which is where those tools get gated. Trimmed heavily here — the real payload is long.

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/agent?directory=$PWD"
```

$ shell · paste into your terminal

response (trimmed)

json 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

```
[
  {
    "name": "build",
    "mode": "primary",
    "native": true,
    "permission": [
      { "permission": "*",             "pattern": "*",            "action": "allow" },
      { "permission": "browser_control","pattern": "*",           "action": "ask"   },
      { "permission": "computer",      "pattern": "*",            "action": "ask"   },
      { "permission": "read",          "pattern": "*",            "action": "allow" },
      { "permission": "read",          "pattern": "*.env",        "action": "ask"   },
      { "permission": "read",          "pattern": "*.env.example","action": "allow" }
      /* … trimmed … */
    ]
  }
  /* … more agents … */
]
```

Permission rules are ordered and last-match-wins

Notice `read: *` allowed, then `read: *.env` set to ask, then `read: *.env.example` allowed again. Reading the list top to bottom is the only way to predict what a given path will do.

## Sessions

A fresh project has none:

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/session?directory=$PWD"
```

$ shell · paste into your terminal

response

json 

```
[]
```

Creating one returns the whole session, not just an id:

Terminal

bash 

```
curl -s -X POST "http://127.0.0.1:4199/session?directory=$PWD" \
  -H 'content-type: application/json' \
  -d '{"title":"api tour"}'
```

$ shell · paste into your terminal

response

json 

1

2

3

4

5

6

7

8

9

10

```
{
  "id": "ses_f50250f8fffexTvasNDaQeiLXP",
  "slug": "swift-eagle",
  "projectID": "c6d9c4f98cc34c8bfb0a40b3a60eca6f624750e6",
  "directory": "/Users/me/demo",
  "title": "api tour",
  "version": "local",
  "time": { "created": 1789656756337, "updated": 1789656756337 },
  "skills": []
}
```

`slug` is a human-readable handle generated alongside the id — `swift-eagle` is easier to say out loud than `ses_f50250f8fffexTvasNDaQeiLXP`. `projectID` matches what `/project/current` returned, which is how a session stays bound to its project.

## Files and search

File listing is project-relative, and carries `ignored` so a client can grey out what git is not tracking.

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/file?path=.&directory=$PWD"
```

$ shell · paste into your terminal

response

json 

1

2

3

4

```
[
  { "name": "greet.ts",  "path": "greet.ts",  "absolute": "/Users/me/demo/greet.ts",  "type": "file", "ignored": false },
  { "name": "README.md", "path": "README.md", "absolute": "/Users/me/demo/README.md", "type": "file", "ignored": false }
]
```

Reading returns content as a tagged object rather than a bare string:

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/file/content?path=greet.ts&directory=$PWD"
```

$ shell · paste into your terminal

response

json 

1

2

3

4

```
{
  "type": "text",
  "content": "export function greet(name: string) {\n  // TODO: localise this\n  return `Hello, ${name}!`\n}"
}
```

Text search returns ripgrep's own match structure — line number, byte offset and the span of each match within the line, which is enough to highlight without re-searching client side.

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/find?pattern=TODO&directory=$PWD"
```

$ shell · paste into your terminal

response

json 

1

2

3

4

5

6

7

8

9

```
[
  {
    "path":  { "text": "greet.ts" },
    "lines": { "text": "  // TODO: localise this" },
    "line_number": 2,
    "absolute_offset": 38,
    "submatches": [{ "match": { "text": "TODO" }, "start": 5, "end": 9 }]
  }
]
```

Filename search is fuzzy, which is worth knowing before you treat it as a filter:

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/find/file?query=greet&directory=$PWD"
```

$ shell · paste into your terminal

response

json 

```
["greet.ts","README.md"]
```

Both files matched "greet"

`README.md` came back too. `/find/file` ranks by fuzzy match rather than filtering by substring, so treat the order as meaningful and the tail as noise.

## The empty shapes

Worth seeing once, because "empty" is not spelled the same way everywhere. Most collections return a bare array; loops and missions return an object with the collection inside it.

real responses, idle project

text 

1

2

3

4

5

6

7

8

9

10

11

```
GET /session/{id}/message   ->  []
GET /session/{id}/todo      ->  []
GET /session/{id}/diff      ->  []
GET /file/status            ->  []
GET /permission             ->  []
GET /question               ->  []
GET /pty                    ->  []
GET /lsp                    ->  []
GET /experimental/workspace ->  []
GET /loop                   ->  {"loops":[],"runtimes":[]}
GET /mission                ->  {"missions":[],"runtimes":[]}
```

That asymmetry is not an accident — the loop and mission payloads carry `runtimes` alongside the definitions — but it will catch you if you assume every list endpoint returns an array.

## The event feed

The feed is plain server-sent events. Connecting to an idle instance gets you the handshake and then silence until something happens.

Terminal

bash 

```
curl -sN "http://127.0.0.1:4199/event?directory=$PWD"
```

$ shell · paste into your terminal

response

text 

```
data: {"type":"server.connected","properties":{}}
```

`server.connected` arriving is the confirmation your subscription is live — which is exactly the moment it is safe to submit a prompt without losing the first tokens. See [Streaming a response](/docs/guides/streaming).

## What errors look like

A declared domain error carries its name and data:

Terminal

bash 

```
curl -s -w '\nHTTP %{http_code}\n' "http://127.0.0.1:4199/session/ses_doesnotexist?directory=$PWD"
```

$ shell · paste into your terminal

response

json 

```
{"name":"NotFoundError","data":{"message":"Session not found: ses_doesnotexist"}}
HTTP 404
```

A body that fails schema validation reports the exact path that was wrong:

Terminal

bash 

```
curl -s -w '\nHTTP %{http_code}\n' \
  -X POST "http://127.0.0.1:4199/session/ses_x/message?directory=$PWD" \
  -H 'content-type: application/json' -d '{}'
```

$ shell · paste into your terminal

response

json 

1

2

3

4

5

6

7

8

9

10

11

```
{
  "data": {},
  "error": [{
    "expected": "array",
    "code": "invalid_type",
    "path": ["parts"],
    "message": "Invalid input: expected array, received undefined"
  }],
  "success": false
}
HTTP 400
```

That error array is the schema validator's own output, so `path` points at the field — here, a missing `parts` on a prompt.

Two responses that are not errors, and surprise people:

Terminal

bash 

```
curl -s -w '\nHTTP %{http_code}\n' "http://127.0.0.1:4199/file/content?path=nope.ts&directory=$PWD"
```

$ shell · paste into your terminal

response

json 

```
{"type":"text","content":""}
HTTP 200
```

A missing file reads as empty, not as 404

`/file/content` answers 200 with `content: ""` for a path that does not exist. If your code distinguishes "empty file" from "no file", check with `/file` first — the status code will not tell you.

Terminal

bash 

```
curl -s "http://127.0.0.1:4199/nope?directory=$PWD"
```

$ shell · paste into your terminal

response — HTTP 200

html 

1

2

3

4

5

```
<!doctype html>
<html lang="en">
  <head>
    <title>NikCLI</title>
    ...
```

An unknown path returns the web app, not a 404

Unmatched non-`/mobile` paths fall through to the hosted web app so it can be served from the same origin. So a typo in an endpoint gives you an HTML page with status 200 — if a client suddenly starts failing to parse JSON, check the path before anything else.

## The whole tour as a script

Run this in the demo project with the server up, and you get every response on this page in one go.

tour.sh

bash 

```
#!/usr/bin/env bash
# tour.sh — run the whole thing against a throwaway server
set -euo pipefail

BASE="http://127.0.0.1:4199"
DIR="$PWD"

say() { printf '\n\033[1m== %s\033[0m\n' "$1"; }
get() { curl -s -m 20 "$BASE$1"; echo; }

say "health";        get "/global/health"
say "paths";         get "/path?directory=$DIR"
say "project";       get "/project/current?directory=$DIR"
say "vcs";           get "/vcs?directory=$DIR"
say "tools";         get "/experimental/tool/ids?directory=$DIR"
say "files";         get "/file?path=.&directory=$DIR"
say "grep TODO";     get "/find?pattern=TODO&directory=$DIR"

say "create session"
SESSION=$(curl -s -X POST "$BASE/session?directory=$DIR" \
  -H 'content-type: application/json' -d '{"title":"api tour"}' | jq -r .id)
echo "$SESSION"

say "session";       get "/session/$SESSION?directory=$DIR"
say "messages";      get "/session/$SESSION/message?directory=$DIR"
say "diff";          get "/session/$SESSION/diff?directory=$DIR"

say "errors"
curl -s -w ' <- HTTP %{http_code}\n' "$BASE/session/ses_nope?directory=$DIR"
```

$ shell · paste into your terminal

Then throw it away

Because the server ran with `NIKCLI_TEST_HOME` pointed at `~/demo/.nikcli-home`, deleting that directory removes every session and every row this tour created. Nothing touched your real state.

## Related

-   [Using the API The same endpoints, with the SDK alongside curl.](/docs/guides/api)
-   [API Reference Every endpoint, field by field.](/docs/api)
-   [Streaming a response What arrives on the feed once work starts.](/docs/guides/streaming)
-   [Building an interface Turning these responses into a client.](/docs/guides/custom-interface)

---

Source: https://nikcli-ai.dev/docs/guides/api-tour
