wp-durable API guide (for Claude sessions and other agents)

Status: Stage 1 service, accepted 2026-10-10. Written by session piinchrome-48 from the deployed source (src/server.ts). The service code is the source of truth; this guide tells a caller how to use it without reading the code.

What this service is

One always-on Railway container running a pi-durable agent with ONE root conversation. You submit a text input; the agent answers with text. Every answer is persisted; the process can restart or be redeployed without losing submissions. In Stage 1 the agent has NO tools: it cannot browse, fetch URLs, run commands, or read files. It only reasons and writes text.

Authentication

Every route except GET /healthz, GET /auth, and GET / (a 302 redirect to /auth) requires:

Authorization: Bearer <WP_DURABLE_TOKEN>

The token is on this machine at /Users/steven/DevTemp/wpDurable/.secrets/wp-durable-token (mode 0600). Load it into a shell variable; never print it, never paste it into a message, a transcript, a commit, or a URL.

WP_URL=https://wp-durable-production.up.railway.app
WP_TOKEN=$(cat /Users/steven/DevTemp/wpDurable/.secrets/wp-durable-token)

A wrong or missing token returns 401 {"error":"Missing or invalid bearer token"}.

Before you submit: check health

curl -s "$WP_URL/healthz"
# {"ok":true,"storage":"ok","credential":"present","provider":"openai-codex","model":"gpt-6-sol","rootConversationId":1,"uptimeSeconds":123}

Submit a task

curl -s -X POST "$WP_URL/v1/submit" \
  -H "Authorization: Bearer $WP_TOKEN" \
  -H "content-type: application/json" \
  -d '{"content":"<your instruction>","requestId":"<unique id>"}'
# 202 {"submissionId":13,"conversationId":1,"status":"running"}
# status is "queued" instead when the conversation is busy with an earlier submission

Body fields:

Field Rule
content Non-empty string. The whole task text. Max request body 64 KB.
requestId Non-empty string, at most 200 characters. Unique per task. Exactly-once key, scoped to the conversation: resubmitting the same requestId returns the ORIGINAL submission (same submissionId, same answer) and does not run the model again.
conversationId Optional non-negative integer. Omit it (= root conversation 1). Only existing conversations are accepted; there is no route to create one yet. Unknown id returns 404; a negative or non-integer value returns 400.

requestId convention: <your-session-name>-<topic>-<UTC timestamp>, for example x402apify-98-summary-20261010T0640Z. Reuse the same id only when you are retrying the same task.

Poll for the answer

curl -s -H "Authorization: Bearer $WP_TOKEN" "$WP_URL/v1/submissions/13"
# {"submissionId":13,"conversationId":1,"requestId":"...","status":"done","answer":"pong"}

status lifecycle: queued → running → done or unanswered.

Reference poll loop (bash):

wp_ask() {  # usage: wp_ask "<requestId>" "<content>"  -> prints the answer
  local rid="$1" content="$2" sid status body
  body=$(python3 -c 'import json,sys; print(json.dumps({"content":sys.argv[1],"requestId":sys.argv[2]}))' "$content" "$rid")
  sid=$(curl -s -X POST "$WP_URL/v1/submit" -H "Authorization: Bearer $WP_TOKEN" \
        -H "content-type: application/json" -d "$body" | python3 -c 'import json,sys; print(json.load(sys.stdin)["submissionId"])')
  for _ in $(seq 1 60); do
    status=$(curl -s -H "Authorization: Bearer $WP_TOKEN" "$WP_URL/v1/submissions/$sid")
    case "$status" in *'"status":"done"'*|*'"status":"unanswered"'*) break;; esac
    sleep 3
  done
  printf '%s\n' "$status" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("answer") or ("UNANSWERED: "+str(d.get("error"))))'
}

Inspect state

curl -s -H "Authorization: Bearer $WP_TOKEN" "$WP_URL/v1/conversations/1"
# {"conversationId":1,"status":"idle"|"busy","lastAnswer":"...","usage":{"models":{...},"tools":{}},"entries":5}

curl -s -H "Authorization: Bearer $WP_TOKEN" "$WP_URL/v1/tasks"
# {"count":0,"tasks":[]}   # live pi-durable tasks (model requests, tool calls, compaction); each task has id, kind, conversationId, status, phase

usage is cumulative token and cost accounting for the conversation; compare before and after a submission to confirm a model call happened (an exactly-once replay leaves it unchanged).

Errors

All errors are JSON {"error": "<message>"}.

Status Meaning
400 Bad body: not JSON, empty content, bad requestId, bad conversationId.
401 Missing or wrong bearer token.
404 Unknown route, submission, or conversation.
413 Request body over 64 KB. Split the task or shorten the content.
500 Internal error; the message is redacted. Retry once, then report it.
502 Sign-in could not start (auth routes only).
503 Storage check failed; the service exits and Railway restarts it. Retry after 30 s.

Rules for callers

  1. Do not call POST /v1/auth/codex/start or redeploy the service. Sign-in is Steven's action.
  2. Do not store the token anywhere except the file above. Do not log it.
  3. Do not change the code in /Users/steven/DevTemp/wpDurable; it is owned by the wpdurable-* session. Report bugs or feature needs to that session or to piinchrome-48 via SendMessage.
  4. The agent has no tools in Stage 1. Do not ask it to fetch, browse, or run things; it will answer from the model's knowledge only.
  5. Shared conversation: write self-contained instructions. State the output format you want.

Limits and planned changes