HTTP API
On this page
The CLI and MCP server are thin clients over this API. Programs can call it directly.
- Base URL:
https://api.wirk.life. Only/healthand/v2/…are served. - Every
/v2request carriesAuthorization: Bearer <token>./healthneeds none. A machine gets its token registered with POST /v2/login. - Every body is JSON (
Content-Type: application/json). - Never follow a redirect with the token attached. WIRK's clients treat any redirect as an error.
Routes
These routes are available on the hosted service:
GET /healthneeds no token and reports readiness.POST /v2/loginregisters a machine token withfor,labelandformatoptions. See Logging in a machine.POST /v2/statusaccepts a task, budget, wirkspace and format. See status.POST /v2/queryfetches items or selected file passages, finds relevant work, filters a list, waits for messages or reads a receipt. See query and reading a file.POST /v2/writeacceptsrequest_id,operationsand any needed revisions, mode or reason. See write.POST /v2/reviewacceptsrequest_idand decisions. See review.POST /v2/filesaccepts exactly one ofupload,confirmordownload. See Files.POST /v2/showmakes or revokes a read-only page. See wirk show.POST /v2/adminserves administrators with their own tokens.
Example
POST /v2/query HTTP/1.1
Host: api.wirk.life
Authorization: Bearer <registered token>
Content-Type: application/json
{"fields": {"kind": "work"}}
Keep the token in a file readable only by you and out of command history, URLs, logs and WIRK itself.
Logging in a machine
A machine makes its own token and asks a person to approve it in the browser. Nobody copies or pastes the token. This is what wirk login does; an agent without the CLI can do the same over HTTPS.
- Make a token: 32 random bytes, URL-safe base64 without padding, prefixed
wirk_. Store it in a file only you can read. Never print it or send it anywhere buthttps://api.wirk.life. - Ask to log in with it:
POST /v2/login
Authorization: Bearer <the new token>
{"label": "laptop", "format": "json"}
| Key | Notes |
|---|---|
for | Optional. "agent", the default: the token works as your person's agents |
label | Optional. A name for this machine, shown to the person who approves it |
format | "text" (default) or "json" |
- The answer while it waits for approval:
{"state": "pending",
"code": "BDFG-HJKL",
"url": "https://app.wirk.life/device?code=BDFG-HJKL",
"expires_at": "2026-10-02T14:05:00Z",
"interval": 5}
In text: Approve this machine: https://app.wirk.life/device?code=BDFG-HJKL, then Code BDFG-HJKL · for your agents · expires 14:05 UTC. Give the person the link. They open it, sign in, check the code and approve.
- Send the same request every
intervalseconds. Once approved, the answer is{"state": "logged_in", …}naming the principal the token now works as, and every/v2route accepts the token.
A repeated request returns the same pending code. A code expires after 10 minutes; the next request then starts a new one. Refusals:
not_authorized(403): the person denied this machine, or its token was revoked. That token never logs in again; make a new one (wirk login --new).not_available(404): too many machines are waiting for approval (try again in a few minutes), or this service has no web sign-in, in which case an administrator registers the token's SHA-256 digest instead.
The envelope
Every /v2 answer, refusals included, is one JSON object:
{"ok": true,
"text": "cards 1–3 of 26 · newest change first\n…",
"errors": [],
"notices": [],
"page": {"complete": true, "next_cursor": null},
"timing_ms": {"total": 4.2}}
| Key | Meaning |
|---|---|
ok | true when the request did what it asked |
text | The compact text answer, when format is "text" (the default) |
data | The structured answer, when format is "json" |
errors | Problems: {code, message, field?, hint?, choices?, requested_id?, input_index?} |
notices | {code, message} that did not stop the request, such as replayed |
page | complete, and next_cursor when more remain |
notifications | JSON answers of status, query, write and review: your messages not yet seen, {state, pending, messages, more?}. state is available, or unavailable when the lookup failed (the answer stands). messages holds at most three previews, {id, from, from_kind, sent, excerpt, on?}, on being the item a message was first linked to; more is the query listing them all. Outside the max_bytes of data. See Messages |
timing_ms | Time spent on this request |
An answer without this envelope came from the network edge in front of WIRK, or from storage for a signed link, not from WIRK. HTTP statuses by code are listed in Errors.
Text or JSON
"format": "text" (default) returns text: compact, readable, with 8-character short IDs. "format": "json" returns data with full IDs. format never changes what a request does, and an identical retry may switch format.
IDs
Every position that names a record takes a full ID or a short one of at least 6 hex characters, with or without its prefix (item_, link_, proposal_, upload_; wirkspaces are wsp_). An ambiguous prefix is refused with choices. workspace_id is optional when your token belongs to one wirkspace; with several, choose_wirkspace lists them.
Request IDs and receipts
write, review, admin and a file confirm require a request_id (1–200 characters) on the wire. The CLI and MCP server make one when you leave it out. The service stores a receipt per request ID:
- The identical body under the same ID returns the stored receipt and applies nothing again, even when the first request is still running.
- A different body under the same ID is
request_conflict. POST /v2/query {"receipt": "<request_id>"}returns the receipt of a write, a review or an upload. Anadminrequest is recovered by sending the identical request again, which returns its stored receipt.
A 503 database_unavailable envelope means the request was rolled back: a refusal, not an unknown outcome.
The Wirk-Context header
POST /v2/status reads an optional header describing where the agent runs:
Wirk-Context: harness=claude-code version=2.1.281 session=7f3a1b2c9d0e4f56 repo=github.com/acme/api branch=main
| Key | Rule |
|---|---|
harness | [a-z0-9.-], 1–40 characters |
version | [A-Za-z0-9.+-], 1–40 characters |
session | 16 lowercase hex characters |
repo | host/owner/name in printable ASCII without @ or ://, or local: and 8 hex |
branch | printable ASCII, at most 200 characters, none of ~^:?*[\ |
Each key at most once, the whole header under 2,048 bytes. If any part is invalid, the whole header is ignored with the notice context_ignored. The values are shown as reported and never decide what you may do.
Files
File bytes never pass through the WIRK server. One route, POST /v2/files, has three uses; send exactly one of upload, confirm and download, and always ask for "format": "json", because text answers never print a signed link.
1. Upload. Ask for a place to put the bytes:
{"upload": {"bytes": 2202010,
"sha256": "<64 hex>"},
"format": "json"}
The answer's data is {"present": true} when storage already holds those bytes, or {"present": false, "put": {"url", "headers", "expires_at"}}: a signed PUT valid for 15 minutes. PUT the file to put.url with exactly put.headers and Content-Length, and never with your WIRK token. The signature covers the length and the SHA-256, so storage refuses any other bytes.
2. Confirm. Record the upload once the bytes are there:
{"request_id": "u-4b7e19c2a0",
"confirm": {"filename": "report.pdf",
"bytes": 2202010,
"sha256": "<64 hex>",
"description": "Load test results"},
"format": "json"}
WIRK checks the stored object's size, SHA-256 and encryption, then answers data.upload = {id, filename, media_type, bytes, sha256, description, object}. The request_id makes the confirm safe to repeat. upload_incomplete means the bytes are not there yet: put them (with a fresh upload link if the first expired), then confirm again. Attach the upload in a write with data.uploads or patch.attach_uploads.
3. Download.
{"download": {"item": "5c1e7a90",
"file": "file_…",
"revision": 3},
"format": "json"}
The answer's data.download is {url, expires_at, filename, media_type, bytes, sha256}: a signed GET valid for 5 minutes. Fetch it without your WIRK token and check the bytes' SHA-256 against sha256. revision is optional.
Never print, log or store a signed link. Each account's files are kept apart and encrypted with the account's own key. The upload limit is max_upload_bytes, 5 GiB by default. The CLI does all of this in wirk upload and wirk download.
For file contents, use a query selection. It returns derived text; a known incomplete extraction begins with [partial: REASON]. The file download returns the unchanged original. Current readable types are text, code, CSV, PDF, OCR images, DOCX, XLSX and PPTX. An unsupported file reports the reason and remains downloadable. Audio has no readable representation.
Health
Open https://api.wirk.life/health to check it without a token.
Needs no token. Returns {ok, instance_id, build, database: {ready}, files: {ready, reason}, limits: {…}, message}, and HTTP 503 when the database is not ready. limits holds the live values of the Limits the service enforces, such as max_upload_bytes and max_write_operations.