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.
- Base URL:
https://wp-durable-production.up.railway.app - Model:
openai-codex/gpt-6-sol(ChatGPT subscription, legacy Codex channel). No API key. - Root conversation id:
1. It is shared by every caller; all submissions append to the same transcript. pi-durable compacts older messages automatically once the context nears the model's window (272k tokens for gpt-6-sol), so do not rely on the agent remembering something you submitted long ago. Put the context you need into the submission itself.
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}
credentialshould bepresent.expiredonly means the stored access token's expiry time has passed; pi-ai refreshes it on the next model call, so submit anyway. Only whencredentialismissing, or a submission comes backunansweredwith an auth error, is a new sign-in needed. Do NOT try to sign in yourself. Tell Steven: he opens$WP_URL/auth, pastes the token, clicks "Generate new code", and enters the code athttps://auth.openai.com/codex/device. The code expires after 15 minutes.storagemust beok. If/healthzreturns 503, the service is restarting; wait and retry.GET /v1/auth/codex/statusreportingstate: "none"is normal after a redeploy: sign-in progress lives in memory, the credential lives on the volume. Trustcredentialin/healthz.
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.
done:answerholds the assistant text.unanswered:errorholds pi-durable's reason (for examplemodel_error) anddetail, when present, holds a redacted JSON string with more context. Typical causes: missing or revoked credential (check/healthz), provider outage, or the run was aborted.- Submissions run one after another in the root conversation. If several callers submit at once,
yours waits in
queued. - Poll every 3 seconds. A short question answers in 5 to 20 seconds. Give up and report after about 3 minutes.
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
- Do not call
POST /v1/auth/codex/startor redeploy the service. Sign-in is Steven's action. - Do not store the token anywhere except the file above. Do not log it.
- Do not change the code in
/Users/steven/DevTemp/wpDurable; it is owned by thewpdurable-*session. Report bugs or feature needs to that session or topiinchrome-48via SendMessage. - 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.
- Shared conversation: write self-contained instructions. State the output format you want.
Limits and planned changes
- One conversation, no tools, no scheduling, no file or image input: Stage 1 scope.
- Planned: tools (datavault, Apify), timers for recurring tasks, a route to create conversations,
and registration of this API as a Woodpecker HTTP API integration (
http_wp_durable). When those land, this guide will be updated in the same file.