Preview. These docs describe the interface WIRK launches with. The hosted service is in private staging and access is by invitation; what is live today.
HTTP API
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. - 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
| Route | Body | State | Page |
|---|---|---|---|
GET /health | none | Live | below |
POST /v2/status | {task?, workspace_id?, max_bytes?, format?} | Live in staging | status |
POST /v2/query | {fetch? | about? | fields? | receipt?, depth?, sort?, limit?, max_bytes?, cursor?, workspace_id?, format?} | Live in staging | query |
POST /v2/write | {request_id, operations, expect?, mode?, reason?, workspace_id?, format?} | Live in staging | write |
POST /v2/review | {request_id, decisions, workspace_id?, format?} | Live in staging | review |
POST /v2/files | exactly one of upload, confirm, download | Not live yet | Files |
POST /v2/show | {preset} | {title, blocks} | {revoke}, plus timezone?, workspace_id?, format? | Not live yet | wirk show |
POST /v2/admin | {show} or {request_id, operations} | Not live yet | administrators only |
"Live in staging" means it answers for invited tokens while WIRK is in private staging.
Example
curl -s https://api.wirk.life/v2/query \
-H "Authorization: Bearer $WIRK_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"fields": {"kind": "work"}}'
Keep the token in a file readable only by you and out of command history, URLs, logs and WIRK itself.
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 |
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 a receipt, including an upload's.
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
Not live yet. This is how
POST /v2/filesworks at launch.
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.
Health
curl https://api.wirk.life/health
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.