API docs

uw — the drafting page and the scoreboard

mount /uw · base https://zpe.vc/uw · v2.1.0 · 6 operations · 2 write · 0 need no account · beta

the document this app serves now. Contract: https://zpe.vc/uw/_/api/v1/openapi.json · sha256 9f319e10c028

GET /_/api/v1/state The scoreboard, the published snapshot's job list, and one job's two panes and history. uwState

One read for the page and for a client: the HTML at /uw/p and this answer are built from the same state, so the two cannot disagree about a number. The drafted body is included, because the client's job is to show it. A key reads exactly what the owner's page reads: the answer's `scope` names `reads: "publisher"` and the account whose shelf answered.

Parameters

nameinrequiredtypenotes
jobquerynostringA job's short ref. Omitted, the newest live proposal answers.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200The read.object
401No door session and no key: sign in at auth.zpe.vc, or send the fleet key as a bearer token.object
403A key without the `uw` grant, or a key on a method this surface only writes.object
503The door did not answer, or nobody has published a snapshot yet.object

Example

The scoreboard, the published snapshot's job list, and one job's two panes and history., with a token from the door.

GET /_/api/v1/state
curl -X GET 'https://zpe.vc/uw/_/api/v1/state?job=string' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "ok": false,
  "schema": "string",
  "published_at": "2026-10-31T09:00:00Z",
  "snapshot_id": 0,
  "window": {},
  "scoreboard": {},
  "corpus": {},
  "jobs": [
    {}
  ],
  "job": null,
  "agent": null,
  "owner": null,
  "history": [
    {}
  ],
  "against_agent": null,
  "scope": {
    "account_id": null,
    "organization_id": null,
    "organization_name": null,
    "role": "string",
    "via": "session",
    "reads": "organisation"
  }
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/save Append his version of one job's letter, once per idempotency key. uwSaveDraft

The only write on this surface. It appends a row and never updates one: a version that can be edited is a version that can be made to agree with whatever happened afterwards. The page's own button posts the same fields as a form and is answered with a 303 back to the pane; a client posting JSON is answered 201 with the row it wrote.

Parameters

nameinrequiredtypenotes
Idempotency-KeyheadernostringThe caller's retry handle. The same key twice replays the first answer and writes nothing.

Request body application/json · required

fieldtyperequirednotes
job_refstringyesA short ref the snapshot lists. Any other ref is refused.
bodystringyesThe letter, as plain text.
notestringnoWhat changed and why, in one line.
idempotency_keystringnoThe same handle as the header, for a client that cannot set one.

Responses

statusmeaningbody
201The version this call appended, and what it changed against the suggestion.object
303The form's answer: the pane, with the saved version printed under the button.
400The body names no job this snapshot lists, or carries no letter.object
401No door session.object
403A bearer key: a save names the person who wrote it, and this route refuses a key.object

Example

Append his version of one job's letter, once per idempotency key., with a token from the door.

POST /_/api/v1/save
curl -X POST 'https://zpe.vc/uw/_/api/v1/save' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "job_ref": "string",
  "body": "string",
  "note": "string",
  "idempotency_key": "string"
}'
201 application/json
{
  "id": "string",
  "version": null,
  "words": 0,
  "at": "2026-10-31T09:00:00Z",
  "against_agent": null
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/drafts The drafts this session may read, oldest first. uwDrafts

The session's own shelf: the organisation the door named and the role it gave decide which rows answer, so a second organisation sees none of the first's. A client reading this sees exactly the versions the page would draw. A KEY reads the publisher's shelf, which is the owner's: the same rows his page draws. `make pull` uses the token-only pull at `/_/publish/drafts`, which is scoped to the deployment's own publisher.

Parameters

nameinrequiredtypenotes
jobquerynostringOne job's short ref. Omitted, every job's drafts answer.
sincequerynointegerOnly rows with an id greater than this, for a pull that keeps its place.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200The rows.object
401No door session.object

Example

The drafts this session may read, oldest first., with a token from the door.

GET /_/api/v1/drafts
curl -X GET 'https://zpe.vc/uw/_/api/v1/drafts?job=string&since=0' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "ok": false,
  "drafts": [
    {
      "id": 0,
      "job_ref": "string",
      "body": "string",
      "words": 0,
      "note": null,
      "account_id": "string",
      "organization_id": null,
      "created_at": "2026-10-31T09:00:00Z"
    }
  ]
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/events This account's record after one event's id, oldest first, one page at a time. uwEvents

THE RECORD, READ FORWARD. Every state change writes one row to this account's outbox in the same transaction as the change it describes, and this read walks those rows by the event's own id: a keyset, never an offset. `since` names a row rather than a position, so an event written between two pages is in the page it belongs in instead of shifting one out of it, and a reader that follows `next_cursor` sees every event once and none twice. Ordering is by id, which is monotonic, so two events written inside the same second — or the same minute — keep the order they were written in. `next_cursor` is null when this page is the last one the record holds; pass it back as `since` to continue. A `since` that is not an id this account holds is REFUSED, because a cursor silently read as "the beginning" makes a consumer re-read the record and never learn that it had. The record is the account's own: a session reads what it wrote, and a key reads the publisher's shelf. An event carries ids, refs and the numbers that moved, never a letter's body.

Parameters

nameinrequiredtypenotes
limitquerynointegerHow many events one page carries, 1 to 200. Omitted, 50.
sincequerynointegerAn event's own id: the answer begins after it. `0` starts at the oldest event this account holds, and is the only value that is not looked up.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200One page of the record, oldest first.object
400`since` is not an id this account holds, or `limit` is outside 1 to 200, or either is not a whole number.object
401No door session and no key: sign in at auth.zpe.vc, or send the fleet key as a bearer token.object
403A key without the `uw` grant.object

Example

This account's record after one event's id, oldest first, one page at a time., with a token from the door.

GET /_/api/v1/events
curl -X GET 'https://zpe.vc/uw/_/api/v1/events?limit=50&since=0' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "ok": false,
  "events": [
    {
      "id": 0,
      "kind": "proposal.changed",
      "subject_ref": "string",
      "at": "2026-10-31T09:00:00Z",
      "payload": null
    }
  ],
  "next_cursor": null
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

GET /_/api/v1/send One send request's state, for the page and for a client polling it. uwSendRequestState

The row the page writes is the row the page polls. `state` is `pending`, `claimed`, `done` or `refused`, and it is written only by the runner: a client that reads `done` is reading what the machine reported, never a guess made here. A refused row carries the sentence that refused it (`result.reason`) and no preview. A KEY reads the publisher's rows, which is how the fleet puts the exact letter and Upwork's preview in front of the owner before anything is sent.

Parameters

nameinrequiredtypenotes
idquerynointegerOne request's id. Omitted, the newest for `job` answers.
jobquerynostringA job's short ref: the newest request for it.

Request body

None: this operation takes its arguments in the path and the query.

Responses

statusmeaningbody
200The request, as the queue holds it.object
401No door session.object
404This account wrote no request at that address.object

Example

One send request's state, for the page and for a client polling it., with a token from the door.

GET /_/api/v1/send
curl -X GET 'https://zpe.vc/uw/_/api/v1/send?id=0&job=string' \
  -H 'Authorization: Bearer $ZPE_TOKEN'
200 application/json
{
  "ok": false,
  "request": {
    "id": 0,
    "kind": "prepare",
    "state": "pending",
    "job_ref": "string",
    "requested_by": "string",
    "at": "2026-10-31T09:00:00Z",
    "claimed_at": null,
    "run_by": null,
    "rate": null,
    "body": null,
    "terms": null,
    "preview": null,
    "result": null,
    "prepare_id": null,
    "wait_seconds": 0
  }
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.

POST /_/api/v1/send Queue a send request: a prepare (Upwork's preview) or a confirm (the submit). uwSendRequest

THE WORKER CANNOT SEND, AND THAT IS THE DESIGN. The Upwork credential lives on the machine that owns the ledger, so this route records an INTENTION. A `prepare` asks the machine for Upwork's preview — the exact text, the Connects, and any screening questions — and a `confirm` names the prepare row whose preview the person saw. A runner on the machine claims the oldest pending row, calls Upwork, and writes `done` or `refused` back; the state is asynchronous by construction and is answered within about a minute (`wait_seconds` names the wait). Nothing here submits anything. A confirm carries no letter: the text a person authorised is the preview's, and a body posted with a confirm is refused.

Parameters

No parameters.

Request body application/json · required

fieldtyperequirednotes
kind"prepare" | "confirm"yes
job_refstringyesA short ref the snapshot lists.
ratenumbernoThe bid, in whole USD.
bodystringnoThe letter. Omitted, the newest version saved here is prepared.
answersarray of objectnoScreening answers, as Upwork takes them.
estimated_durationstringnoHow long the work will take. One of the snapshot's `send_terms.durations`; required with `payment_mode`.
payment_modestringnoHow it is paid. One of the snapshot's `send_terms.payment_modes`; required with `estimated_duration`.
milestonesarray of objectnoFor the milestone payment mode: `{description, amount, due_date?}` rows whose amounts add up to the bid.
prepare_idintegernoThe prepare row this confirm belongs to. Required for `confirm`.
room_refstringnoThe room that asked, when a room asked: the channel writes it to `send_origin` in the same batch as the request, so which room a send came from is a row and not a recollection. Absent on the drafting page, which names no room.

Responses

statusmeaningbody
201Queued. The row's id, its state (`pending`), and the wait the page prints.object
303The page's own form answer: the pane, with this request's state drawn under it.
400The ref is not in the published snapshot, the rate is missing, half a fixed-price pair is named, there is no letter, or a confirm carries a body.object
401No door session.object
403A bearer key: a send request names the person who asked, and this route refuses a key. Queue a prepare with the owner's own session.object

Example

Queue a send request: a prepare (Upwork's preview) or a confirm (the submit)., with a token from the door.

POST /_/api/v1/send
curl -X POST 'https://zpe.vc/uw/_/api/v1/send' \
  -H 'Authorization: Bearer $ZPE_TOKEN' \
  -H 'content-type: application/json' \
  -d '{
  "kind": "prepare",
  "job_ref": "string",
  "rate": 0,
  "body": "string",
  "answers": [
    {}
  ],
  "estimated_duration": "string",
  "payment_mode": "string",
  "milestones": [
    {}
  ],
  "prepare_id": 0,
  "room_ref": "string"
}'
201 application/json
{
  "ok": false,
  "id": 0,
  "kind": "prepare",
  "state": "pending",
  "job_ref": "string",
  "wait_seconds": 0
}

The example is generated from the schema in the contract: the shape is the document's, the values are placeholders.