InferMux

InferMux REST API

One OpenAI-compatible endpoint in front of every model InferMux routes to — plus a small Bearer-authenticated JSON API for the rest of this app.

Pointing an AI agent at this API?

Hand it the LLM-ready Markdown version — self-contained instructions an agent can follow with just a URL and an API key.

Open /docs/llm/api.md

Getting started

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, /models, /audio/transcriptions and /audio/speech 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. Scope, expiry and cap can be edited later (API keys → Edit) without changing the token; the new rules apply from the next request. A scoped key gets 403 for any other model, and GET /models lists only what it may call. 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). Dave comes with a subscription. A plan includes one or more AI profiles (e.g. "Dave Pro") — admin-defined bundles of models (LOW / MEDIUM / HIGH coding models, a ROUTER that picks the tier per prompt, VISION for images, optional TTS / STT). Users only ever see the profile name, never the models. Each profile has a token allowance per day, week and month (set per plan); a used-up allowance can be reset with a free reset an admin granted or bought from credits. Sign-in: dave login runs a device flow (POST /api/v1/dave/auth/start, approve in the browser at /dave/link, POST /api/v1/dave/auth/poll) and receives a Dave key — it opens Dave sessions and calls the /api/v1/dave/* endpoints, and is refused (403 dave_key) everywhere else. Transport: one WebSocket at https://infermux.net/api/v1/dave/ws (ws:// or wss://), authenticated by sending Authorization: Bearer <key> 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":2,"client":{…},"workspace":{"root":"/abs/path","name":"proj"},"profile":"optional-profile-slug"} and gets {"type":"ready","sessionId":…,"profile":{"slug","name"},"profiles":[…],"tools":[…],"billing":{"mode":"subscription","plan":…}} (no subscription → a fatal error with code no_subscription). Then: client → {"type":"prompt","id":"p1","text":"…"} / steer / abort / profile.set / tool.result / tool.stream / pong / bye; server → event (tier, text_delta, thinking_delta, tool_start, tool_end, message_end, …), profile, 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" against the profile allowance (never credits). GET /api/v1/dave returns the gateway URL, protocol version, tool list, client downloads and your profiles; the full protocol is in src/lib/dave/protocol.ts and docs/DAVE.md.

Login widget — browser extensions (redirect flow)

A browser extension gets a scoped router key for the signed-in user through the login widget's redirect flow, built for chrome.identity.launchWebAuthFlow / browser.identity.launchWebAuthFlow (interactive: true). Open https://infermux.net/widget/connect?widget=<widget id>&redirect_uri=<uri>&state=<opaque>: - widget — the login widget id (Dashboard → Login widget). - redirect_uri — https://<extension-id>.chromiumapp.org/… (Chrome/Edge, from chrome.identity.getRedirectURL()) or https://<id>.extensions.allizom.org/… (Firefox). Its origin must be on the widget's allowed origins exactly; an empty list allows no redirect flow at all. Any other value (http, another host, userinfo, a fragment, an unlisted extension) shows an error page and never redirects. - state — required, up to 512 characters, echoed back unchanged. Generate a random value per attempt and compare it on return. The user signs in (existing InferMux accounts only — the flow never creates one). A key is minted in their account with the widget's scope, lifetime (keyTtlDays) and per-key spend cap, and the browser is sent to <redirect_uri>#key=sk_mr_…&expiresAt=<ISO 8601>&email=<address>&baseUrl=https://infermux.net/api/v1&state=<state>. Everything rides in the URL fragment (never the query), so the key never reaches a server log; parse it with new URLSearchParams(new URL(responseUrl).hash.slice(1)). Use baseUrl as the OpenAI-compatible base URL. On "Disconnect", call POST /api/v1/widget/revoke with the key as Bearer token. Sign-ins share the widget.login rate limit with the embedded widget.

Content type

Responses are JSON unless noted (file download and /audio/speech return raw bytes). Request bodies are JSON (Content-Type: application/json) except file upload and /audio/transcriptions, which are 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, /audio/*, /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).

Your base URL is https://infermux.net. Create API keys under Profile → API Keys.

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: Bearer tokenAccess: Any valid API key.

Request

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: Bearer tokenAccess: Any valid API key (scoped to the key owner).

Request

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: Bearer tokenAccess: Any valid API key (admin keys see all users; others see themselves).
NameInTypeReq.Description
limitqueryintegernoPage size, 1–100 (default 10). Admin only; ignored for non-admins.
offsetqueryintegernoRows to skip (default 0). Admin only.

Request

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: Bearer tokenAccess: Admin API keys only (others get 403).
NameInTypeReq.Description
emailbodystringyesNew user email.
namebodystringyesNew user display name.
rolebodystringno'user' (default) or 'admin'.

Request

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: Bearer tokenAccess: Any valid API key (the file is owned by the key owner).
NameInTypeReq.Description
fileformfileyesThe file to upload (multipart field name must be "file").

Request

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: Bearer tokenAccess: Any valid API key.
NameInTypeReq.Description
idpathstringyesFile id returned by the upload endpoint.

Request

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: Bearer tokenAccess: Any valid API key (only the owner may delete).
NameInTypeReq.Description
idpathstringyesFile id to delete.

Request

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: Bearer tokenAccess: Any valid key. A scoped router key sees only the models it is allowed to use.
NameInTypeReq.Description
searchquerystringnoSubstring match on id, name or description.
categoryquerystringnoComma-separated tags to require, e.g. `programming,vision`.
input_modalityquerystringnoComma-separated required input modalities, e.g. `image`.
freequerybooleanno`true` returns only zero-cost models.
toolsquerybooleanno`true` returns only models supporting tool calling.
min_contextqueryintegernoMinimum context window in tokens.
max_prompt_pricequerynumbernoMaximum prompt price in USD per million tokens.
sortquerystringno`name` (default), `newest`, `price-asc`, `price-desc`, `context-desc`, `context-asc`.
limitqueryintegernoPage size (default: all matches).
offsetqueryintegernoRows to skip (default 0).
include_collectionsquerybooleanno`false` omits your collections from the list.

Request

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: Bearer tokenAccess: Any valid key, subject to its model/collection scope, expiry and spend cap.
NameInTypeReq.Description
modelbodystringyesModel id, or `collection/{slug}`.
messagesbodyarrayyesOpenAI messages. Content may be a string or an array of `text`/`image_url` parts.
streambodybooleanno`true` streams `text/event-stream` chunks, ending with a `router.summary` frame and `[DONE]`.
toolsbodyarraynoTool definitions. Auto-route only considers tool-capable models when present.
temperaturebodynumbernoPassed through to the upstream model, like any other sampling parameter.

Request

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.
POST
/api/v1/audio/transcriptions

Transcribe audio (OpenAI-compatible speech-to-text)

Turns an audio file into text, exactly like OpenAI's `/audio/transcriptions`. `model` must be a transcription model — they are listed by `GET /models?category=transcription` (e.g. `qwen/qwen3-asr-0.6b`, `openai/whisper-large-v3-turbo`). Same rules as chat: the key must be scoped to the model, and the call is charged to your credits at the upstream price plus markup. Transcription models are priced per second of audio (`pricing.prompt` on `GET /models` is USD per second); the upstream's exact cost is used when it reports one.

Auth: Bearer tokenAccess: Any valid key, subject to its model scope, expiry and spend cap.
NameInTypeReq.Description
fileformfileyesThe audio (mp3, wav, m4a, ogg, webm, flac, …), max 25 MB.
modelformstringyesA transcription model id. Collections are not accepted here.
languageformstringnoSpoken language hint (ISO-639-1, e.g. `en`).
promptformstringnoOptional context/vocabulary hint, passed upstream.
response_formatformstringno`json` (default) → `{ text, usage }`; `verbose_json` adds `language`, `duration` and `segments`; `text` returns the transcript as `text/plain`. `srt`/`vtt` are not supported (400).
temperatureformnumbernoSampling temperature, passed upstream.

Request

curl -X POST https://infermux.net/api/v1/audio/transcriptions \
  -H "Authorization: Bearer sk_your_key_here" \
  -F model=qwen/qwen3-asr-0.6b \
  -F file=@recording.mp3

Response

{
  "text": "Hello from InferMux.",
  "usage": { "seconds": 1.7, "cost": 0.000005661 },
  "router": {
    "requested": "qwen/qwen3-asr-0.6b",
    "resolved": "qwen/qwen3-asr-0.6b",
    "charged_usd": 0.000005661
  }
}
  • `usage.cost` and `router.charged_usd` are what *you* were charged (upstream cost + markup); `usage.seconds` is the audio duration billed.
  • Every response (including `response_format=text`) also carries `X-Usage-Cost` (USD charged) and `X-Usage-Seconds` headers.
  • `403 model_forbidden` = key not scoped to the model; `402` = out of credits / key spend cap; `400 unsupported_request` = not a transcription model.
POST
/api/v1/audio/speech

Generate speech (OpenAI-compatible text-to-speech)

Turns text into audio, exactly like OpenAI's `/audio/speech`: send JSON, get the raw audio bytes back with the matching `Content-Type` (`audio/mpeg` for mp3). `model` must be a text-to-speech model — listed by `GET /models?category=speech` (e.g. `hexgrad/kokoro-82m`). The key must be scoped to the model. Speech models are priced per input character (`pricing.prompt` on `GET /models` is USD per character): the charge is characters × that price (plus any per-request fee), because the upstream reports no cost for speech. Models priced per *output* audio token cannot be metered up front and are refused with 400.

Auth: Bearer tokenAccess: Any valid key, subject to its model scope, expiry and spend cap.
NameInTypeReq.Description
modelbodystringyesA text-to-speech model id. Collections are not accepted here.
inputbodystringyesThe text to speak, up to 10,000 characters.
voicebodystringnoVoice id; model-specific (e.g. `af_heart` for kokoro).
response_formatbodystringno`mp3` (default), `opus`, `aac`, `flac`, `wav` or `pcm` — whichever the model supports.
speedbodynumbernoPlayback speed multiplier, e.g. `1.0`.

Request

curl -X POST https://infermux.net/api/v1/audio/speech \
  -H "Authorization: Bearer sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"model":"hexgrad/kokoro-82m","input":"Hello from InferMux.","voice":"af_heart","response_format":"mp3"}' \
  -o hello.mp3

Response

(binary audio body, streamed)
Content-Type: audio/mpeg
X-Usage-Cost: 0.0000124000
X-Usage-Characters: 20
X-Router-Model: hexgrad/kokoro-82m
  • `X-Usage-Cost` is the USD you were charged (upstream cost + markup); `X-Usage-Characters` the characters billed. The charge is made once the upstream accepts the request — before the audio streams — so it is already in the headers.
  • The audio is passed through as a stream. Latency is the model's: `mistralai/voxtral-mini-tts-2603` (voice `en_paul_neutral`) answers in seconds, `hexgrad/kokoro-82m` can take ~50 s and may time out at the edge. No default voice is applied — send one that fits the model.
  • Errors come back as JSON in the OpenAI error envelope, never as audio — check the status code.
  • Any extra body fields (e.g. `instructions`) are passed through to the 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: Bearer tokenAccess: Any valid key (collections-scoped keys see only the collections they may use).

Request

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: Bearer tokenAccess: Any valid key (scoped to the key owner).
NameInTypeReq.Description
windowquerystringnoOne of `hour`, `day`, `week`, `month`, `year`, `ever`. Omit for all windows at once.

Request

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 Dave plan and, for every AI profile it includes, the token limit, usage, what is left and exactly when it resets — per day, week and month.

Auth: Bearer tokenAccess: Any valid key, including Dave keys (scoped to the key owner).

Request

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 },
  "subscription": { "id": "sub_...", "status": "active", "started_at": "2026-10-01T09:00:00.000Z", "current_period_end": "2026-11-01T09:00:00.000Z" },
  "profiles": [
    {
      "slug": "dave-pro", "name": "Dave Pro", "exhausted": false,
      "allowances": [
        { "window": "day", "token_limit": 2000000, "tokens_used": 90200, "tokens_remaining": 1909800, "resets_at": "2026-10-10T00:00:00.000Z" },
        { "window": "week", "token_limit": null, "tokens_used": 402000, "tokens_remaining": null, "resets_at": "2026-10-12T00:00:00.000Z" },
        { "window": "month", "token_limit": 40000000, "tokens_used": 1402000, "tokens_remaining": 38598000, "resets_at": "2026-11-01T00:00:00.000Z" }
      ]
    }
  ]
}
  • `subscribed: false` means no Dave; direct API calls are billed to prepaid credits.
  • A `token_limit` of `null` is unlimited. Windows are UTC-aligned (day, ISO week from Monday, calendar month).
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), your plan and the AI profiles it includes (names only — never the models behind them).

Auth: Bearer tokenAccess: Any valid key, including Dave keys (scoped to the key owner).

Request

curl https://infermux.net/api/v1/dave \
  -H "Authorization: Bearer <dave key>"

Response

{
  "protocol": 2,
  "gateway": { "url": "wss://example.com/api/v1/dave/ws", "path": "/api/v1/dave/ws", "auth": "Authorization: Bearer <key from `dave login`> on the upgrade request, or (browsers) subprotocols ["dave.v2", "dave.bearer.<key>"]" },
  "tools": ["read", "write", "edit", "ls", "find", "grep", "bash"],
  "subscription": { "plan": "Pro" },
  "profiles": [
    { "slug": "dave-lite", "name": "Dave Lite", "description": "Fast and frugal.", "default": true },
    { "slug": "dave-pro", "name": "Dave Pro", "description": "Our strongest setup.", "default": false }
  ],
  "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 login --server <BASE_URL>", "binary": "dave login --server <BASE_URL> && dave" }
}
  • The WebSocket itself is documented under "Dave — the hosted coding agent" above; it is not an HTTP endpoint.
POST
/api/v1/dave/auth/start

Start `dave login`

Begins the device sign-in flow. Returns a secret `device_code` (keep it in the client) and a short `user_code`; send the user to `verification_path_complete` (on this host) where they sign in and approve the code. Then poll `/dave/auth/poll`. Approval requires an active Dave subscription.

Auth: NoneAccess: Anyone (rate limited per IP).
NameInTypeReq.Description
clientbodyobjectno`{ name, host, platform }` — shown on the approval page so the user recognises the device.

Request

curl -X POST https://infermux.net/api/v1/dave/auth/start \
  -H "Content-Type: application/json" \
  -d '{"client":{"name":"dave 0.2.0","host":"my-laptop","platform":"darwin/arm64"}}'

Response

{
  "device_code": "9f2c…(64 hex)",
  "user_code": "KJ4F-Q2RD",
  "verification_path": "/dave/link",
  "verification_path_complete": "/dave/link?code=KJ4F-Q2RD",
  "expires_in": 600,
  "interval": 3
}
POST
/api/v1/dave/auth/poll

Poll `dave login`

Poll every `interval` seconds with the `device_code`. Returns `pending` until the user approves, then `approved` with the Dave key — handed out exactly once — or `denied` / `expired`.

Auth: NoneAccess: Anyone holding the device code (rate limited per IP).
NameInTypeReq.Description
device_codebodystringyesFrom `/dave/auth/start`.

Request

curl -X POST https://infermux.net/api/v1/dave/auth/poll \
  -H "Content-Type: application/json" \
  -d '{"device_code":"9f2c…"}'

Response

{ "status": "approved", "key": "sk_mr_…", "key_expires_at": "2027-01-07T09:00:00.000Z" }
  • Other answers: `{ "status": "pending", "interval": 3 }`, `{ "status": "denied" }`, `{ "status": "expired" }`.
  • The key is a Dave key: it works for the WebSocket and `/api/v1/dave/*`, and expires after 90 days. It shows up under API keys, where it can be revoked.
POST
/api/v1/dave/auth/logout

`dave logout`

Revokes the Dave key making the call. Other key kinds are left alone.

Auth: Bearer tokenAccess: Dave keys.

Request

curl -X POST https://infermux.net/api/v1/dave/auth/logout \
  -H "Authorization: Bearer <dave key>"

Response

{ "revoked": true }
GET
/api/v1/dave/usage

Dave allowances and resets

What `dave usage` / `/usage` shows: your plan, and per AI profile the tokens used and the limit for each window with its reset time, whether it is used up and when it frees up again, the free resets you can redeem (per profile, and `free_resets` in total) and the price of a bought reset — plus your credit balance and `buy_resets_path`, the page on this site where resets are bought.

Auth: Bearer tokenAccess: Any valid key, including Dave keys (scoped to the key owner).

Request

curl https://infermux.net/api/v1/dave/usage \
  -H "Authorization: Bearer <dave key>"

Response

{
  "plan": { "id": "pln_…", "name": "Pro", "renewsAt": "2026-11-09T07:00:00.000Z" },
  "balance_usd": 7,
  "free_resets": 1,
  "buy_resets_path": "/dashboard/subscription#allowances",
  "profiles": [
    {
      "slug": "dave-pro", "name": "Dave Pro", "description": "Our strongest setup.", "default": false,
      "exhausted": true, "available_again_at": "2026-10-10T00:00:00.000Z", "last_reset_at": null,
      "windows": [
        { "window": "day", "limit": 1500, "used": 3750, "remaining": 0, "resets_at": "2026-10-10T00:00:00.000Z" },
        { "window": "week", "limit": null, "used": 3750, "remaining": null, "resets_at": "2026-10-12T00:00:00.000Z" },
        { "window": "month", "limit": 5000000, "used": 3750, "remaining": 4996250, "resets_at": "2026-11-01T00:00:00.000Z" }
      ],
      "free_resets": 1,
      "reset_price_usd": 3
    }
  ]
}
POST
/api/v1/dave/resets

Reset a profile allowance

Clears a profile's day, week and month usage for you. `mode: "auto"` (default) uses a free reset if you have one and otherwise buys one from your credits; `"free"` only uses a free reset; `"purchase"` always buys.

Auth: Bearer tokenAccess: Any valid key, including Dave keys (scoped to the key owner).
NameInTypeReq.Description
profilebodystringyesProfile slug or name.
modebodystringno`auto` | `free` | `purchase`.

Request

curl -X POST https://infermux.net/api/v1/dave/resets \
  -H "Authorization: Bearer <dave key>" -H "Content-Type: application/json" \
  -d '{"profile":"dave-pro","mode":"free"}'

Response

{
  "reset": true, "profile": "Dave Pro", "paid_with": "free_reset", "price_usd": 0,
  "windows": [{ "window": "day", "limit": 1500, "used": 0, "resets_at": "2026-10-10T00:00:00.000Z" }]
}
  • Errors: `402 insufficient_credit`, `404 unknown_profile`, `403 no_subscription`, `409 nothing_to_reset` / `not_for_sale` / `no_free_resets`.
GET
/api/v1/dave/sessions

Your Dave sessions

The caller's recent Dave sessions, most recent first: AI profile, 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: Bearer tokenAccess: Any valid key (scoped to the key owner).
NameInTypeReq.Description
limitquerynumbernoMax rows (1–100, default 20).

Request

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,
      "profile": "Dave Pro", "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"
    }
  ]
}
POST
/api/v1/widget/revoke

Revoke a login-widget key (log out / Disconnect)

Revokes the key making the call, if it was minted by a login widget (the embedded widget or the `/widget/connect` extension redirect flow). Use it for an app's "Log out" or an extension's "Disconnect". Expired or spent keys can still revoke themselves, and repeating the call is harmless. Other key kinds are left alone.

Auth: Bearer tokenAccess: The widget-minted key itself (`sk_mr_…`).

Request

curl -X POST https://infermux.net/api/v1/widget/revoke \
  -H "Authorization: Bearer sk_mr_widget_key"

Response

{ "revoked": true }
  • A key that was not minted by a widget returns `200 { "revoked": false, "reason": "…" }` and keeps working.
  • An unknown key returns `401`. Afterwards every call with the revoked key returns `401`.
  • Rate limited per IP (`widget.revoke`).
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: Bearer tokenAccess: Any valid key (scoped to the key owner).

Request

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"
  }
}