# Using the API

Start a server and drive it — with curl and the typed client side by side, one task at a time.

## Start a server

There is no hosted API. nikcli runs on your machine and the API is how everything else talks to it — the web app, the desktop app and the mobile app are all clients of these endpoints.

Terminal

bash 

```
nikcli serve --hostname 127.0.0.1 --port 4096
```

$ shell · paste into your terminal

Read the port it prints

If `4096` is taken the server falls back to an ephemeral port rather than failing. Take the URL from the output instead of hardcoding it.

## Your first request

Health is instance-less, so it answers before anything else is configured.

Terminal

bash 

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

$ shell · paste into your terminal

Then install the client, which is generated from the same contract this guide describes.

Terminal

bash 

```
npm install @nikcli-ai/sdk
```

$ shell · paste into your terminal

ts 

1

2

3

4

5

6

7

8

9

```
import { createNikcliClient } from "@nikcli-ai/sdk/httpapi"

const client = createNikcliClient({
  baseUrl: "http://127.0.0.1:4096",
  directory: process.cwd(),
})

const { data } = await client.global.health()
console.log(data)
```

Calls resolve with { data, error }

They do not reject by default. Guard on `data` — not on `error`, which is typed `unknown` and so does not narrow. Full explanation in [the result envelope](/docs/build/client#results).

## Authenticate

On loopback with no password set, nothing is required. Once the server is reachable from elsewhere, it is.

Terminal

bash 

```
NIKCLI_SERVER_PASSWORD=secret nikcli serve --hostname 0.0.0.0 --port 4096

curl -s -u nikcli:secret http://127.0.0.1:4096/session
curl -s -H "Authorization: Bearer $NIKCLI_TOKEN" http://127.0.0.1:4096/session

# for clients that cannot set headers, such as EventSource
curl -s "http://127.0.0.1:4096/session?token=$NIKCLI_TOKEN"
```

$ shell · paste into your terminal

ts 

1

2

3

4

```
const client = createNikcliClient({
  baseUrl: "http://127.0.0.1:4096",
  headers: { Authorization: `Bearer ${process.env.NIKCLI_TOKEN}` },
})
```

## Pick the project

One server hosts many projects, so most endpoints need to know which. Pass `directory` as a query parameter or as the `x-nikcli-directory` header — the client sets the header for you from its own `directory` option.

Terminal

bash 

```
curl -s "http://127.0.0.1:4096/project/current?directory=$PWD"
curl -s http://127.0.0.1:4096/project/current -H "x-nikcli-directory: $PWD"
```

$ shell · paste into your terminal

ts 

1

2

3

4

5

```
// the client default
await client.project.current()

// override for one call
await client.session.list({ directory: "/Users/me/other-project" })
```

## Create a session and prompt it

A session holds the conversation and the changes made in it. Create one, then send prompts to its id.

curl

bash 

```
SESSION=$(curl -s -X POST "http://127.0.0.1:4096/session?directory=$PWD" \
  -H 'content-type: application/json' \
  -d '{"title":"triage"}' | jq -r .id)

curl -s -X POST "http://127.0.0.1:4096/session/$SESSION/message?directory=$PWD" \
  -H 'content-type: application/json' \
  -d '{"parts":[{"type":"text","text":"Which tests are failing, and why?"}]}'
```

$ shell · paste into your terminal

SDK

ts 

1

2

3

4

5

6

7

8

9

10

```
const { data: session } = await client.session.create({ title: "triage" })

const { data: reply } = await client.session.prompt({
  sessionID: session!.id,
  parts: [{ type: "text", text: "Which tests are failing, and why?" }],
})

for (const part of reply!.parts) {
  if (part.type === "text") console.log(part.text)
}
```

`session.prompt` waits for the whole turn. To get output as it is produced, use `promptAsync` and the event feed — see [Streaming a response](/docs/guides/streaming).

parts, not a string

A prompt is an array of parts, so one message can mix text with file attachments, an agent reference or a subtask. `{ type: "text", text }` is the common case.

## Read what changed

Sessions edit files. Ask the session what it changed rather than diffing the working tree yourself — this scopes the answer to that session's work.

Terminal

bash 

```
curl -s "http://127.0.0.1:4096/session/$SESSION/diff?directory=$PWD"
curl -s "http://127.0.0.1:4096/session/$SESSION/message?directory=$PWD"
```

$ shell · paste into your terminal

ts 

```
const { data: diff } = await client.session.diff({ sessionID: session!.id })
const { data: messages } = await client.session.messages({ sessionID: session!.id, limit: 20 })
```

## Read and write files

File access goes through the instance, so paths are resolved inside the selected project rather than against the server's own working directory.

Terminal

bash 

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

curl -s -X PUT "http://127.0.0.1:4096/file/content?directory=$PWD" \
  -H 'content-type: application/json' \
  -d '{"path":"NOTES.md","content":"# Notes\n"}'

curl -s "http://127.0.0.1:4096/file/status?directory=$PWD"
```

$ shell · paste into your terminal

ts 

```
const { data: file } = await client.file.read({ path: "src/index.ts" })
await client.file.write({ path: "NOTES.md", content: "# Notes\n" })
const { data: status } = await client.file.status()
```

## Search the project

Three searches, matching what the agent's own tools use: text, filenames and symbols.

Terminal

bash 

```
curl -s "http://127.0.0.1:4096/find?pattern=TODO&directory=$PWD"
curl -s "http://127.0.0.1:4096/find/file?query=router&directory=$PWD"
curl -s "http://127.0.0.1:4096/find/symbol?query=createNikcliClient&directory=$PWD"
```

$ shell · paste into your terminal

ts 

```
const { data: matches } = await client.find.text({ pattern: "TODO" })
const { data: files } = await client.find.files({ query: "router" })
const { data: symbols } = await client.find.symbols({ query: "createNikcliClient" })
```

## Run a shell command

Running a command _inside_ a session records it in the transcript, so the agent sees the result on the next turn. That is usually what you want over shelling out yourself.

ts 

1

2

3

4

5

```
const { data: result } = await client.session.shell({
  sessionID: session!.id,
  agent: "build",
  command: "bun test",
})
```

agent is required here

`session.shell` takes an `agent`, because the command runs under that agent's permissions rather than unconditionally.

## Answer a permission request

If the agent asks for approval and nothing answers, the turn waits. An automated client has to decide — which is exactly why the endpoints exist.

ts 

1

2

3

4

5

6

7

8

```
const { data: pending } = await client.permission.list()

for (const request of pending ?? []) {
  await client.permission.reply({
    requestID: request.id,
    reply: "once", // "once" | "always" | "reject"
  })
}
```

Watching `permission.asked` on the event feed is the reactive version of the same thing, and is how the TUI and the mobile app do it.

## A complete script

Putting it together: open a session, ask it to do something, approve whatever it asks for, and print the resulting diff.

review.ts

ts 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

```
import { createNikcliClient } from "@nikcli-ai/sdk/httpapi"

const client = createNikcliClient({
  baseUrl: process.env.NIKCLI_URL ?? "http://127.0.0.1:4096",
  directory: process.cwd(),
  headers: process.env.NIKCLI_TOKEN
    ? { Authorization: `Bearer ${process.env.NIKCLI_TOKEN}` }
    : undefined,
})

const { data: session, error } = await client.session.create({ title: "automated review" })
if (!session) throw error

// approve anything this session asks for while it works
const approve = setInterval(async () => {
  const { data: pending } = await client.permission.list()
  for (const request of pending ?? []) {
    await client.permission.reply({ requestID: request.id, reply: "once" })
  }
}, 1000)

try {
  const { data: reply } = await client.session.prompt({
    sessionID: session.id,
    parts: [{ type: "text", text: "Review the staged changes and fix anything obviously broken." }],
  })

  for (const part of reply?.parts ?? []) {
    if (part.type === "text") console.log(part.text)
  }

  const { data: diff } = await client.session.diff({ sessionID: session.id })
  console.log(diff)
} finally {
  clearInterval(approve)
}
```

Approving everything is a choice, not a default

The loop above says yes to every request. That is reasonable in a sandbox or a throwaway CI container, and not reasonable on a machine that holds credentials. Filter on `request.permission` if the script runs anywhere real.

## Where to go next

| You want | Go to |
| --- | --- |
| To see what these actually return | [A guided tour with real responses](/docs/guides/api-tour) |
| Output as it is produced | [Streaming a response](/docs/guides/streaming) |
| The exact shape of a request or response | [API Reference](/docs/api) |
| Client options, errors and method naming | [Client](/docs/build/client) |
| To run nikcli in your own process | [Embedded SDK](/docs/build/sdk) |
| A client in another language | [The OpenAPI document](/docs/api/openapi.json) |

## Related

-   [Streaming a response The event feed, and printing tokens as they arrive.](/docs/guides/streaming)
-   [API Reference Every endpoint, generated from the server contract.](/docs/api)
-   [Client The typed client in depth.](/docs/build/client)
-   [Server & API What happens to a request inside the server.](/docs/server-api)

---

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