Docs · HTTP API

HTTP API

On this page

The CLI and MCP server are thin clients over this API. Programs can call it directly.

Routes

These routes are available on the hosted service:

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.

  1. 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 but https://api.wirk.life.
  2. Ask to log in with it:
POST /v2/login
Authorization: Bearer <the new token>

{"label": "laptop", "format": "json"}
KeyNotes
forOptional. "agent", the default: the token works as your person's agents
labelOptional. A name for this machine, shown to the person who approves it
format"text" (default) or "json"
  1. 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.

  1. Send the same request every interval seconds. Once approved, the answer is {"state": "logged_in", …} naming the principal the token now works as, and every /v2 route 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:

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}}
KeyMeaning
oktrue when the request did what it asked
textThe compact text answer, when format is "text" (the default)
dataThe structured answer, when format is "json"
errorsProblems: {code, message, field?, hint?, choices?, requested_id?, input_index?}
notices{code, message} that did not stop the request, such as replayed
pagecomplete, and next_cursor when more remain
notificationsJSON 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_msTime 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:

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
KeyRule
harness[a-z0-9.-], 1–40 characters
version[A-Za-z0-9.+-], 1–40 characters
session16 lowercase hex characters
repohost/owner/name in printable ASCII without @ or ://, or local: and 8 hex
branchprintable 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.

This page as Markdown · every page in one file