# InferMux REST API — LLM Integration Guide

You are an autonomous agent that has been given a **base URL** and an **API key** for the service documented below. This document is self-contained: it tells you everything needed to call the API. Follow it literally.

- **Base URL:** `https://infermux.net`
- **API key:** provided to you separately; it starts with `sk_`.
- **All paths below are relative to the base URL** (e.g. `https://infermux.net/api/v1/health`).

> Generated from the app's single source of truth. If an endpoint
> behaves differently from what you read here, trust the live response
> and report the mismatch — the docs are meant to be authoritative.

## Essentials

### Base URL

All endpoints live under `https://infermux.net/api/v1`. `https://infermux.net` is the origin you were given (scheme + host, e.g. `https://example.com`). Do not add a trailing slash.

That same path is an **OpenAI-compatible base URL**: point any OpenAI SDK at `https://infermux.net/api/v1` with one of our keys and `/chat/completions` and `/models` work unchanged.

### Authentication

Every endpoint requires an API key sent as a Bearer token:
`Authorization: Bearer sk_your_key_here`. Keys always start with `sk_`. A missing or invalid key returns `401`.

There are two kinds. **Router keys** (`sk_mr_…`, created under API keys) can be scoped to specific models or collections, given an expiry and a spend cap — those are the ones to hand to an app or mint through the login widget. **Account keys** (`sk_…`, under Settings) authenticate their owner with no model restrictions.

### Routing & collections

The `model` field of a completion takes either an upstream model id (`anthropic/claude-3.5-sonnet`) or one of your collections as `collection/{slug}`. A collection is an ordered bucket of models; with auto-route on we try them in order, falling through on failure or rate limit. When a collection mixes vision and text models, an image request is analysed by the vision models first and the result — even an error — is passed to the text models, which write the final answer. The `router` block in the response says exactly what ran.

### Billing

Prices are the upstream router's prices plus a platform markup (2% by default, set per provider by an admin) and are exposed on `GET /models` in USD per token. Each call is charged to the key owner's prepaid credits unless an active subscription covers that model, in which case it draws from the plan's hourly/daily/weekly token allowance instead. `402` means out of credits or the key hit its spend cap. Free models cost nothing either way.

Note on subscriptions: a plan is primarily for **Dave** (the hosted coding agent, below). Unless the admin enabled "also cover direct API calls" on the plan, calls you make straight to `/chat/completions` with your own key are billed to credits even while you are subscribed.

### Dave — the hosted coding agent (WebSocket)

Dave is a coding agent whose reasoning loop runs on this server while every tool call (read/write/edit files, ls/find/grep, shell commands) is executed by the small `dave` client on the user's own machine, inside the project it was started in. The server never touches its own filesystem or shell on the agent's behalf; the client is the security boundary (refuses paths outside the workspace, asks before network / git commit / package installs, never runs sudo).

Transport: one WebSocket at `https://infermux.net/api/v1/dave/ws` (`ws://` or `wss://`), authenticated by sending `Authorization: Bearer sk_…` on the upgrade request (`401`/`402`/`429` are returned as plain HTTP before the upgrade). Frames are JSON. The client first sends `{"type":"hello","protocol":1,"client":{…},"workspace":{"root":"/abs/path","name":"proj"},"model":"optional/model-id"}` and gets `{"type":"ready","sessionId":…,"model":{…},"tools":[…],"billing":{…}}`. Then: client → `{"type":"prompt","id":"p1","text":"…"}` / `steer` / `abort` / `tool.result` / `tool.stream` / `pong` / `bye`; server → `event` (text_delta, thinking_delta, tool_start, tool_end, message_end, …), `tool.request` (`{"callId","method":"fs.read|fs.write|fs.edit|fs.list|fs.glob|fs.grep|proc.exec","params":{…}}`), `tool.cancel`, `prompt.done`, `ping`, `error`, `bye`. Sending `hello` with `"resume":"<sessionId>"` re-attaches to a session after a dropped connection. Model calls made by the loop are metered as `source: "dave"` and are what a subscription covers. `GET /api/v1/dave` returns the gateway URL, protocol version, tool list, client downloads and the models your key may use; the full protocol is in `src/lib/dave/protocol.ts` and `docs/DAVE.md`.

### Content type

Responses are JSON unless noted (file download returns raw bytes). Request bodies are JSON (`Content-Type: application/json`) except file upload, which is `multipart/form-data`.

### Rate limiting

Requests are rate limited per API key. When you exceed a limit you get `429` (or `403` if the limit is configured to block) with an `error` message and, when applicable, a `Retry-After` header (seconds). Back off and retry.

### Errors

The model-router endpoints (`/models`, `/chat/completions`, `/collections`, `/usage`, `/subscription`, `/credits`) return the OpenAI error envelope: `{ "error": { "message": "...", "type": "...", "code": "..." } }`. The older endpoints return a flat `{ "error": "..." }`. Both use matching HTTP status codes (`400` bad input, `401` unauthenticated, `402` out of credits, `403` forbidden/out of scope, `404` not found, `413` payload too large, `429` rate limited, `500` server error).

## Quick start

```bash
# 1. Verify your key works
curl https://infermux.net/api/v1/health -H "Authorization: Bearer sk_your_key_here"
# A 200 with {"status":"healthy",...} means you are authenticated.
```

## Endpoints

### GET /api/v1/health

**Health check** — Confirms the API is up and your key is valid. Handy as a first call to verify credentials and connectivity.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid API key.

**Request**

```bash
curl https://infermux.net/api/v1/health \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "status": "healthy",
  "timestamp": "2026-07-19T12:00:00.000Z",
  "uptime": 1234.56,
  "version": "1.0.0",
  "apiKey": "My key",
  "userId": "usr_...",
  "message": "API is running successfully"
}
```

---

### GET /api/v1/stats

**Account & API usage stats** — Returns the calling user together with API-usage counters (requests today / this week / this month, error rate, API-key count).

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid API key (scoped to the key owner).

**Request**

```bash
curl https://infermux.net/api/v1/stats \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "user": { "id": "usr_...", "email": "you@example.com", "name": "You", "role": "user", "createdAt": "..." },
  "apiStats": {
    "totalApiKeys": 2,
    "requestsToday": 14,
    "requestsThisWeek": 98,
    "requestsThisMonth": 412,
    "errorRate": "1.20%",
    "errorCount": 5
  },
  "meta": { "timestamp": "...", "apiKey": "My key" }
}
```

---

### GET /api/v1/users

**List users** — Lists users. A regular key returns only its own user record; an admin key returns all users with pagination.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid API key (admin keys see all users; others see themselves).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | integer | no | Page size, 1–100 (default 10). Admin only; ignored for non-admins. |
| `offset` | query | integer | no | Rows to skip (default 0). Admin only. |

**Request**

```bash
curl "https://infermux.net/api/v1/users?limit=20&offset=0" \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "users": [
    { "id": "usr_...", "email": "you@example.com", "name": "You", "role": "user", "emailVerified": null, "createdAt": "..." }
  ],
  "meta": { "limit": 20, "offset": 0, "total": 1, "apiKey": "My key" }
}
```

---

### POST /api/v1/users

**Create user (scaffold)** — Admin-only endpoint scaffold for creating a user. Ships as a stub in this starter — it validates input and echoes it back rather than persisting. Fill in real creation logic before relying on it.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Admin API keys only (others get 403).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `email` | body | string | yes | New user email. |
| `name` | body | string | yes | New user display name. |
| `role` | body | string | no | 'user' (default) or 'admin'. |

**Request**

```bash
curl -X POST https://infermux.net/api/v1/users \
  -H "Authorization: Bearer sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"email":"new@example.com","name":"New User","role":"user"}'
```

**Response**

```
{
  "message": "User creation endpoint - implementation needed",
  "requestedData": { "email": "new@example.com", "name": "New User", "role": "user" },
  "apiKey": "My key"
}
```

> This is a template stub — no user is actually created yet.

---

### POST /api/v1/files

**Upload a file** — Uploads a file and stores its raw bytes. Use this instead of a form/Server Action for any real upload (Server Actions cap the body at ~1MB; this endpoint does not). Send `multipart/form-data` with a single `file` field.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid API key (the file is owned by the key owner).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `file` | form | file | yes | The file to upload (multipart field name must be "file"). |

**Request**

```bash
curl -X POST https://infermux.net/api/v1/files \
  -H "Authorization: Bearer sk_your_key_here" \
  -F "file=@./photo.png"
```

**Response**

```
{
  "id": "fil_...",
  "filename": "photo.png",
  "url": "/api/v1/files/fil_..."
}
```

> Default max size is 100MB (configurable via MAX_FILE_SIZE). Oversized uploads return 413.
>
> The returned `url` is the Bearer-gated download endpoint below.

---

### GET /api/v1/files/:id

**Download / preview a file** — Streams the raw file bytes with the stored Content-Type. Because it is Bearer-gated you cannot put it directly in an `<img src>`; fetch it with the token and build an object URL client-side.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid API key.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | File id returned by the upload endpoint. |

**Request**

```bash
curl https://infermux.net/api/v1/files/fil_your_file_id \
  -H "Authorization: Bearer sk_your_key_here" \
  --output downloaded-file
```

**Response**

```
Raw binary body with the stored `Content-Type` and `Content-Disposition: inline; filename="..."`. Returns `404 { "error": "File not found" }` if unknown.
```

---

### DELETE /api/v1/files/:id

**Delete a file** — Deletes a file owned by the calling key.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid API key (only the owner may delete).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | File id to delete. |

**Request**

```bash
curl -X DELETE https://infermux.net/api/v1/files/fil_your_file_id \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{ "deleted": true }   // { "deleted": false } with status 404 if not found / not owned
```

---

### GET /api/v1/models

**List available models** — Every model this key may use, in the OpenAI `list` shape, with prices already including the platform markup. Your collections are listed too, as `collection/{slug}` pseudo-models you can pass straight to /chat/completions.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid key. A scoped router key sees only the models it is allowed to use.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `search` | query | string | no | Substring match on id, name or description. |
| `category` | query | string | no | Comma-separated tags to require, e.g. `programming,vision`. |
| `input_modality` | query | string | no | Comma-separated required input modalities, e.g. `image`. |
| `free` | query | boolean | no | `true` returns only zero-cost models. |
| `tools` | query | boolean | no | `true` returns only models supporting tool calling. |
| `min_context` | query | integer | no | Minimum context window in tokens. |
| `max_prompt_price` | query | number | no | Maximum prompt price in USD per million tokens. |
| `sort` | query | string | no | `name` (default), `newest`, `price-asc`, `price-desc`, `context-desc`, `context-asc`. |
| `limit` | query | integer | no | Page size (default: all matches). |
| `offset` | query | integer | no | Rows to skip (default 0). |
| `include_collections` | query | boolean | no | `false` omits your collections from the list. |

**Request**

```bash
curl "https://infermux.net/api/v1/models?category=vision&free=true" \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "object": "list",
  "data": [
    {
      "id": "collection/free-vision",
      "object": "model",
      "owned_by": "you",
      "name": "Free vision models",
      "collection": { "id": "col_...", "auto_route": true }
    },
    {
      "id": "anthropic/claude-3.5-sonnet",
      "object": "model",
      "owned_by": "openrouter",
      "name": "Anthropic: Claude 3.5 Sonnet",
      "context_length": 200000,
      "architecture": { "input_modalities": ["text", "image"], "output_modalities": ["text"] },
      "categories": ["vision", "tools", "long-context"],
      "pricing": { "prompt": "0.00000306", "completion": "0.0000153", "request": "0", "image": "0" }
    }
  ],
  "meta": { "total": 2, "returned": 1, "collections": 1 }
}
```

> Prices are USD per token and already include the markup — this is what you are charged.
>
> The catalogue is mirrored from the upstream router every 3 hours.

---

### POST /api/v1/chat/completions

**Chat completion (OpenAI-compatible)** — The main endpoint. Accepts a standard OpenAI chat-completions body. `model` is either an upstream model id (`anthropic/claude-3.5-sonnet`) or one of your collections as `collection/{slug}`. With auto-route on, a collection is walked in order and falls through to the next model on failure or rate limit; if the collection mixes vision and text models, an image request goes to the vision models first and their result — even an error — is handed to the text models, which produce the answer. Set `stream: true` for a standard SSE stream.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid key, subject to its model/collection scope, expiry and spend cap.

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `model` | body | string | yes | Model id, or `collection/{slug}`. |
| `messages` | body | array | yes | OpenAI messages. Content may be a string or an array of `text`/`image_url` parts. |
| `stream` | body | boolean | no | `true` streams `text/event-stream` chunks, ending with a `router.summary` frame and `[DONE]`. |
| `tools` | body | array | no | Tool definitions. Auto-route only considers tool-capable models when present. |
| `temperature` | body | number | no | Passed through to the upstream model, like any other sampling parameter. |

**Request**

```bash
curl -X POST https://infermux.net/api/v1/chat/completions \
  -H "Authorization: Bearer sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"model":"collection/free-vision","messages":[{"role":"user","content":"What is in this image?"}]}'
```

**Response**

```
{
  "id": "gen-...",
  "object": "chat.completion",
  "model": "meta-llama/llama-3.3-70b-instruct",
  "choices": [{ "index": 0, "message": { "role": "assistant", "content": "..." }, "finish_reason": "stop" }],
  "usage": { "prompt_tokens": 42, "completion_tokens": 128, "total_tokens": 170 },
  "router": {
    "requested": "collection/free-vision",
    "resolved": "meta-llama/llama-3.3-70b-instruct",
    "collection": "free-vision",
    "auto_route": true,
    "attempts": 2,
    "stages": ["vision", "primary"],
    "charged_usd": 0.00021,
    "covered_by_subscription": false
  }
}
```

> The extra `router` block is additive — every standard OpenAI field is still present.
>
> Charges come from your prepaid credits unless an active subscription covers the model.
>
> `402` means out of credits or the key hit its spend limit; `403` means the key is not scoped to that model.

---

### GET /api/v1/collections

**List your collections** — Your collections and their ordered members. Pass the `model` value of any of them to /chat/completions to route through it.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid key (collections-scoped keys see only the collections they may use).

**Request**

```bash
curl https://infermux.net/api/v1/collections \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "object": "list",
  "data": [
    {
      "id": "col_...",
      "model": "collection/free-vision",
      "name": "Free vision models",
      "slug": "free-vision",
      "auto_route": true,
      "models": [
        { "position": 0, "id": "qwen/qwen-2-vl-7b-instruct", "vision": true, "free": true, "available": true },
        { "position": 1, "id": "meta-llama/llama-3.3-70b-instruct", "vision": false, "free": true, "available": true }
      ]
    }
  ]
}
```

---

### GET /api/v1/usage

**Token & spend statistics** — Tokens in/out, request counts and spend for the hour, day, week, month, year and all-time, each with the instant its window resets. With `?window=` you get one window plus a bucketed series and the top models by spend.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid key (scoped to the key owner).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `window` | query | string | no | One of `hour`, `day`, `week`, `month`, `year`, `ever`. Omit for all windows at once. |

**Request**

```bash
curl "https://infermux.net/api/v1/usage?window=day" \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "usage": {
    "day": {
      "window": "day",
      "requests": 128,
      "successes": 126,
      "errors": 2,
      "promptTokens": 402113,
      "completionTokens": 88420,
      "totalTokens": 490533,
      "costUsd": 1.284,
      "since": "2026-07-19T00:00:00.000Z",
      "resetsAt": "2026-07-20T00:00:00.000Z"
    }
  },
  "credits": { "balanceUsd": 18.72, "lifetimeToppedUpUsd": 20, "lifetimeSpentUsd": 1.28 }
}
```

---

### GET /api/v1/subscription

**Subscription allowances** — Your active plan and, for each of the hour / day / week windows, the token limit, how much is used, how much is left and exactly when it resets.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid key (scoped to the key owner).

**Request**

```bash
curl https://infermux.net/api/v1/subscription \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "subscribed": true,
  "plan": { "id": "pln_...", "name": "Pro", "price_usd": 50, "scope": "all" },
  "subscription": { "id": "sub_...", "status": "active", "current_period_end": "2026-08-19T12:00:00.000Z" },
  "allowances": [
    { "window": "hour", "token_limit": 600000, "tokens_used": 12500, "tokens_remaining": 587500, "resets_at": "2026-07-19T13:00:00.000Z" },
    { "window": "day",  "token_limit": 6000000, "tokens_used": 90200, "tokens_remaining": 5909800, "resets_at": "2026-07-20T00:00:00.000Z" },
    { "window": "week", "token_limit": 30000000, "tokens_used": 402000, "tokens_remaining": 29598000, "resets_at": "2026-07-20T00:00:00.000Z" }
  ]
}
```

> `subscribed: false` means usage is billed against prepaid credits instead.
>
> `plan.covers_direct_api` tells you whether the plan pays for direct `/chat/completions` calls too, or only for Dave sessions.

---

### GET /api/v1/dave

**Dave gateway info** — Everything a `dave` client (or someone setting one up) needs: the WebSocket gateway URL and protocol version, the tools the hosted agent can call, download links for the client binaries (when CI has built them), how this key's sessions are billed, the default model and the tool-capable models the key may run the agent on.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid key (scoped to the key owner).

**Request**

```bash
curl https://infermux.net/api/v1/dave \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "protocol": 1,
  "gateway": { "url": "wss://example.com/api/v1/dave/ws", "path": "/api/v1/dave/ws", "auth": "Authorization: Bearer <sk_mr_… or sk_…> on the upgrade request" },
  "tools": ["read", "write", "edit", "ls", "find", "grep", "bash"],
  "billing": { "mode": "subscription", "plan": "Pro", "covers_direct_api": false },
  "default_model": "openai/gpt-4o-mini",
  "models": [{ "id": "openai/gpt-4o-mini", "name": "OpenAI: GPT-4o-mini", "context_length": 128000 }],
  "downloads": [{ "arch": "macosx-arm", "label": "macOS (Apple silicon)", "filename": "dave", "url": "https://…/dave" }],
  "downloads_available": true,
  "downloads_note": null,
  "install": { "source": "bun run dave/src/main.ts --server <BASE_URL> --key <sk_…>", "binary": "dave login --server <BASE_URL> --key <sk_…> && dave" }
}
```

> The WebSocket itself is documented under "Dave — the hosted coding agent" above; it is not an HTTP endpoint.

---

### GET /api/v1/dave/sessions

**Your Dave sessions** — The caller's recent Dave sessions, most recent first: model, workspace name/root as reported by the client, client version/platform, prompt and tool-call counters (including how many calls the client refused) and status.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid key (scoped to the key owner).

**Parameters**

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `limit` | query | number | no | Max rows (1–100, default 20). |

**Request**

```bash
curl "https://infermux.net/api/v1/dave/sessions?limit=10" \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "object": "list",
  "data": [
    {
      "id": "ses_…", "status": "ended", "live": false,
      "model": "openai/gpt-4o-mini", "thinking_level": "medium",
      "workspace": { "root": "/home/me/project", "name": "project" },
      "client": { "version": "0.1.0", "platform": "darwin/arm64" },
      "prompts": 3, "tool_calls": 17, "tool_calls_denied": 1,
      "started_at": "2026-08-20T09:00:00.000Z", "last_seen_at": "2026-08-20T09:12:41.000Z",
      "ended_at": "2026-08-20T09:12:41.000Z", "end_reason": "client_bye"
    }
  ]
}
```

---

### GET /api/v1/credits

**Credit balance** — Prepaid balance and lifetime totals for the key owner, plus this key's own scope, spend and spend limit.

- **Auth:** `Authorization: Bearer sk_...` required
- **Access:** Any valid key (scoped to the key owner).

**Request**

```bash
curl https://infermux.net/api/v1/credits \
  -H "Authorization: Bearer sk_your_key_here"
```

**Response**

```
{
  "balance_usd": 18.72,
  "lifetime_topped_up_usd": 20,
  "lifetime_spent_usd": 1.28,
  "key": {
    "name": "Production app",
    "scope": "collections",
    "spend_limit_usd": 5,
    "spent_usd": 0.42,
    "expires_at": "2026-07-26T12:00:00.000Z"
  }
}
```

---

## Notes for automated callers

- Always send the `Authorization: Bearer sk_...` header; there is no cookie/session auth here.
- On `429`/`403` with a `Retry-After` header, wait that many seconds before retrying.
- Treat any non-2xx JSON `error` field as the human-readable failure reason.
- File downloads (`GET /api/v1/files/:id`) return raw bytes, not JSON.
