Operator manual
The API
Base URL https://pressone.dev/api/v1. JSON in, JSON out. Everything a coding agent needs is one POST and one of three ways to get the answer back.
Auth
Send your key as a bearer token. Keys start with po_live_ and are shown once at checkout. Lost it? Write to the operator and the old one is revoked and a new one issued.
Authorization: Bearer po_live_...Ask a question
POST /questions
{
"source": "devin", // free text, read aloud: "Devin is blocked"
"question": "PR 212 is green. Merge or hold?", // 600 chars max, read aloud
"context": "Changes the billing webhook.", // optional, shown on the web page
"options": [ // 1 to 9, keys are single digits, default 1 yes 2 no
{"key": "1", "label": "Merge"},
{"key": "2", "label": "Hold"}
],
"callback_url": "https://your.app/hooks/pressone", // optional, https only
"ttl_seconds": 1800 // 60 to 86400, default 30 min
}
201
{
"id": "k97...", "status": "ringing",
"delivery": "phone" | "browser",
"answer_url": "https://pressone.dev/q/k97...",
"poll_url": "https://pressone.dev/api/v1/questions/k97...",
"wait_url": "https://pressone.dev/api/v1/questions/k97.../wait",
"expires_in": 1800
}If you have a phone on file and dial out is live for your account, we call it. Otherwise delivery is browser and the answer_url is where you get the question; put it in Slack, a PR comment, wherever you look at 2 am.
Wait for the answer
Three ways. Pick the one your harness can do.
GET /questions/:id # poll. status: pending | ringing | answered | expired | failed
GET /questions/:id/wait # long poll, holds up to 50 s. 200 when terminal, 202 {"timed_out":true} otherwise
# or give a callback_url and do nothing# a loop that works everywhere
until curl -sf "$WAIT_URL" -H "Authorization: Bearer $PRESSONE_API_KEY" -o /tmp/a -w '%{http_code}' | grep -q 200; do :; done
jq -r .answer.key /tmp/aThe answer
{
"id": "k97...", "status": "answered",
"answer": { "key": "1", "label": "Merge", "via": "phone" | "web" | "voice", "at": 1761720000000 }
}Callback
We POST to your callback_url when the question is answered: three attempts with backoff, any non 2xx counts as a miss, and the outcome shows up as callback.status on the poll endpoint so a missed delivery is never silent. Header X-PressOne-Event: question.answered. Body is the answer object above. Return any 2xx.
Phone
Add a number in E.164 and the line rings it first. Founders are getting dial out turned on in order of slot; browser delivery works for everyone today.
PATCH /api/account {"phone": "+15555550123"}
GET /api/account # plan, slot, phone, key prefixClaude Code
A Notification hook fires when Claude Code is waiting for permission or input. The script below asks PressOne and, on 1, writes yes back through the hook protocol. Full file in integrations/claude-code/ of the repo.
// .claude/settings.json
{ "hooks": { "Notification": [{ "matcher": "", "hooks": [{ "type": "command", "command": "pressone-claude-hook" }] }] } }Devin
Devin exposes status_enum = blocked and the last message on GET /v1/session/:id. The watcher in integrations/devin/ polls your sessions, turns a blocked message plus its numbered options into a question, and posts your key back with POST /v1/session/:id/message.
PRESSONE_API_KEY=po_live_... DEVIN_API_KEY=... python integrations/devin/watch.pyCodex and Cursor
Anything that can run a shell step can ask. Drop this in a CI job, a Cursor rule, or a Codex task when the agent reaches a decision it should not make alone.
ANSWER=$(pressone ask "Deploy to prod?" 1:Deploy 2:Abort) # blocks until you press a key
[ "$ANSWER" = 1 ] && ./deploy.shErrors
{ "error": { "code": "unauthorized | bad_json | missing_question | question_too_long | bad_options | bad_callback | not_found | expired | sold_out | not_configured", "message": "plain words" } }Status at /api/health. Operator at joe@joepro.ai.