Ask API
Human-in-the-loop as a single HTTP call. Your script or CI job POSTs a question with a few possible actions. Zenhook pages a human (push, email, desktop) and the HTTP call returns whatever they tap. No SDK, no session, no polling framework required.
Ask a question
Same token as your channel's webhook URL: an ask is just a question-shaped event on the channel. By default the call blocks until a human answers (or ~80 seconds pass, see waiting modes).
curl -X POST https://zenhook.dev/api/ask/abc123def456 \
-H "Content-Type: application/json" \
-d '{
"title": "Deploy api-server to production?",
"message": "3 commits since the last deploy. Tests green.",
"actions": ["Deploy", "Abort"],
"default": "Abort",
"timeout": 600
}'{
"id": "ask_cmr5o5l6",
"status": "answered",
"answer": "Deploy",
"answeredVia": "link",
"answeredAt": "2026-07-04T09:41:12.000Z",
"actions": ["Deploy", "Abort"],
"defaultAction": "Abort",
"askUrl": "https://zenhook.dev/ask/…",
"pollUrl": "https://zenhook.dev/api/ask/…/ask_cmr5o5l6"
}Everyone on the channel gets the question like a normal alert, with an Answer link. The link opens a public answer page. One tap on the phone and the blocked call above returns. Whoever holds the link can answer; no Zenhook account needed.
Request fields
Waiting modes
A single held HTTP request is capped around 80 seconds (proxy limits). For longer decisions, loop on pollUrl: each poll can itself hold up to 80s with ?wait=60, so the loop is cheap:
RES=$(curl -s -X POST "https://zenhook.dev/api/ask/$TOKEN" \
-H 'Content-Type: application/json' \
-d '{"title":"Deploy to prod?","actions":["Deploy","Abort"],"timeout":600}')
while [ "$(echo "$RES" | jq -r .status)" = "pending" ]; do
RES=$(curl -s "$(echo "$RES" | jq -r .pollUrl)?wait=60")
done
if [ "$(echo "$RES" | jq -r .answer)" = "Deploy" ]; then
./deploy.sh
else
echo "Held back by a human." && exit 1
fiOr skip waiting entirely and get the outcome pushed to you:
{
"title": "Refund $420 to customer #1847?",
"actions": ["Refund", "Escalate", "Ignore"],
"timeout": 3600,
"wait": false,
"callbackUrl": "https://your-app.com/hooks/zenhook-answer"
}Answer semantics
- First answer wins. Two people tapping at once can't both win; the loser sees who answered first.
- Expiry never invents an answer.An expired ask has
status: "expired"andanswer: null. Yourdefaultcomes back asdefaultAction; applying it is your call. - Answers are audited. Who answered (when signed in), via which surface, and when.
Plan limits
Free includes 25 asks per month per workspace. Pro raises this to 1,000 asks per month. Asks also count toward your monthly event quota.