HITL Protocol
HITL Protocol is to human decisions what OAuth is to authentication — an open standard connecting Services, Agents, and Humans. When a service needs human input, it returns HTTP 202 with a review URL. The agent forwards the URL to the human. The human opens it in a browser, gets a rich UI, and makes an informed decision. The agent polls for the structured result and continues.
For simple decisions (confirm/cancel, approve/reject), agents can render native messaging buttons directly in Telegram, Slack, Discord, WhatsApp, or Teams — no browser needed. The service opts in via submit_url.
No SDK required. No UI framework mandated. Just HTTP + URL + polling.
Interactive Playground — try all review types, transports, and inline actions live in your browser.
Who Are You?
| Your role | You want to... | Read |
|---|---|---|
| Service/website builder | Add HITL endpoints to your API so agents can request human input | Service Integration Guide |
| Agent developer | Handle HTTP 202 + HITL responses from services | Agent Integration Guide |
| Both / Learning | Understand the full protocol | Continue reading below |
The Flow
Standard flow (all review types):
1. Human → Agent: "Find me jobs in Berlin"
2. Agent → Service: POST /api/search {query: "Senior Dev Berlin"}
3. Service → Agent: HTTP 202 + hitl object (review_url, poll_url, type, prompt)
4. Agent → Human: "Found 5 jobs. Review here: {review_url}"
5. Human → Browser: Opens review_url → rich UI (cards, forms, buttons)
6. Human → Service: Makes selection, clicks Submit
7. Agent → Service: GET {poll_url} → {status: "completed", result: {action, data}}
8. Agent → Human: "Applied to 2 selected jobs."
Inline flow (v0.6 — simple decisions only, when submit_url present):
1. Human → Agent: "Send my application emails"
2. Agent → Service: POST /api/send {emails: [...]}
3. Service → Agent: HTTP 202 + hitl object (incl. submit_url, submit_token, inline_actions)
4. Agent → Human: Native buttons in chat: [Confirm] [Cancel] [Details →]
5. Human → Agent: Taps [Confirm] in chat
6. Agent → Service: POST {submit_url} {action: "confirm", submitted_via: "telegram"}
7. Service → Agent: 200 OK {status: "completed"}
8. Agent → Human: Updates message: "Confirmed — 3 emails sent."
The agent never renders UI. The service hosts the review page. Sensitive data stays in the browser — never passes through the agent. The inline flow is an optional shortcut for simple decisions.
Feature Matrix
| Feature | Details |
|---|---|
| Review types | approval, selection, input, confirmation, escalation |
| Form field types | text, textarea, number, date, email, url, boolean, select, multiselect, range, custom x-* |
| Transport | Polling (required), SSE (optional), Callback/Webhook (optional) |
| Inline submit | submit_url + native messaging buttons (Telegram, Slack, Discord, WhatsApp, Teams) — service opt-in |
| States | pending → opened → in_progress → completed / expired / cancelled |
| Security | Opaque tokens (43 chars, base64url, 256-bit entropy), SHA-256 hash storage, timing-safe comparison, HTTPS only |
| Multi-round | previous_case_id / next_case_id for iterative edit cycles (Approval type) |
| Forms | Single-step fields, multi-step wizard, conditional visibility, validation rules, progress tracking |
| Timeouts | ISO 8601 duration, default_action: skip / approve / reject / abort |
| Discovery | .well-known/hitl.json, SKILL.md metadata.hitl extension |
| Reminders | reminder_at timestamps, review.reminder SSE event |
| Rate limiting | 60 requests/min per case on poll endpoint, Retry-After header |
Five Review Types
| Type | Actions | Multi-round | Form fields | Use case |
|---|---|---|---|---|
| Approval | approve, edit, reject |
Yes | No | Artifact review (CV, email, deployment plan) |
| Selection | select |
No | No | Choose from options (job listings, targets) |
| Input | submit |
No | Yes | Structured data entry (salary, dates, preferences) |
| Confirmation | confirm, cancel |
No | No | Irreversible action gate (send emails, deploy) |
| Escalation | retry, skip, abort |
No | No | Error recovery (deployment failed, API error) |
HITL Object (HTTP 202 Response Body)
When a service needs human input, it returns HTTP 202 with this structure:
{
"status": "human_input_required",
"message": "5 matching jobs found. Please select which ones to apply for.",
"hitl": {
"spec_version": "0.6",
"case_id": "review_abc123",
"review_url": "https://service.example.com/review/abc123?token=K7xR2mN4pQ...",
"poll_url": "https://api.service.example.com/v1/reviews/abc123/status",
"type": "selection",
"prompt": "Select which jobs to apply for",
"timeout": "24h",
"default_action": "skip",
"created_at": "2026-02-22T10:00:00Z",
"expires_at": "2026-02-23T10:00:00Z",
"context": {
"total_options": 5,
"query": "Senior Dev Berlin"
}
}
}
Required Fields
| Field | Type | Description |
|---|---|---|
spec_version |
"0.6" |
Protocol version |
case_id |
string | Unique, URL-safe identifier (pattern: review_{random}) |
review_url |
URL | HTTPS URL to review page with opaque bearer token |
poll_url |
URL | Status polling endpoint |
type |
enum | approval / selection / input / confirmation / escalation / x-* |
prompt |
string | What the human needs to decide (max 500 chars) |
created_at |
datetime | ISO 8601 creation timestamp |
expires_at |
datetime | ISO 8601 expiration timestamp |
Optional Fields
| Field | Type | Description |
|---|---|---|
timeout |
duration | How long the review stays open (24h, PT24H, P7D) |
default_action |
enum | skip / approve / reject / abort — action on expiry |
callback_url |
URL / null | Echoed callback URL if agent provided one |
events_url |
URL | SSE endpoint for real-time status events |
context |
object | Arbitrary data for the review page (not processed by agent) |
reminder_at |
datetime / datetime[] | When to re-send the review URL |
previous_case_id |
string | Links to prior case in multi-round chain |
surface |
object | UI format declaration (format, version) |
submit_url |
URL | Agent-submit endpoint for channel-native inline buttons (v0.6) |
submit_token |
string | Bearer token for submit_url authentication (required if submit_url set) |
inline_actions |
string[] | Actions permitted via submit_url (e.g. ["confirm", "cancel"]). If absent, all actions for the type are allowed. |
Poll Response (Completed)
{
"status": "completed",
"case_id": "review_abc123",
"completed_at": "2026-02-22T10:15:00Z",
"result": {
"action": "select",
"data": {
"selected_jobs": ["job-123", "job-456"],
"note": "Only remote positions"
}
}
}
The result object is present only when status is "completed". It always contains action (string) and data (object with type-dependent content).
Poll Response Statuses
| Status | Terminal | Description | Key fields |
|---|---|---|---|
pending |
No | Case created, human hasn't opened URL | expires_at |
opened |
No | Human opened the review URL | opened_at |
in_progress |
No | Human is interacting with the form | progress (optional) |
completed |
Yes | Human submitted response | result, completed_at, responded_by |
expired |
Yes | Timeout reached | expired_at, default_action |
cancelled |
Yes | Human clicked cancel | cancelled_at, reason |
State Machine
+---------------------------------------------+
| v
[created] -> pending -> opened -> in_progress -> completed [terminal]
| | |
| | +---------> cancelled [terminal]
| |
| +--> completed [terminal]
| +--> expired [terminal]
| +--> cancelled [terminal]
|
+----------> expired [terminal]
+----------> cancelled [terminal]
Terminal states (completed, expired, cancelled) are immutable — no further transitions.
For Services: Quick Start
Return HTTP 202 when human input is needed:
// Express / Hono / any HTTP framework
app.post('/api/search', async (req, res) => {
const results = await searchJobs(req.body.query);
// Create review case with opaque token
const caseId = `review_${crypto.randomBytes(16).toString('hex')}`;
const token = crypto.randomBytes(32).toString('base64url'); // 43 chars
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
store.set(caseId, {
status: 'pending',
tokenHash,
results,
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 86400000).toISOString(),
});
res.status(202).json({
status: 'human_input_required',
message: `${results.length} jobs found. Please select which ones to apply for.`,
hitl: {
spec_version: '0.6',
case_id: caseId,
review_url: `https://yourservice.com/review/${caseId}?token=${token}`,
poll_url: `https://api.yourservice.com/v1/reviews/${caseId}/status`,
type: 'selection',
prompt: 'Select which jobs to apply for',
timeout: '24h',
default_action: 'skip',
created_at: store.get(caseId).created_at,
expires_at: store.get(caseId).expires_at,
},
});
});
You also need: a review page (any web framework), a poll endpoint (GET /reviews/:caseId/status), and a response endpoint (POST /reviews/:caseId/respond). See Service Integration Guide for full details.
For Agents: Quick Start
Handle HTTP 202 responses — ~15 lines:
import time, httpx
response = httpx.post("https://api.jobboard.com/search", json=query)
if response.status_code == 202:
hitl = response.json()["hitl"]
# v0.6: Check for inline submit support
if "submit_url" in hitl and "submit_token" in hitl:
# Render native buttons in messaging platform (e.g. Telegram, Slack)
send_inline_buttons(hitl["prompt"], hitl["inline_actions"], hitl["review_url"])
# When human taps button → POST to submit_url (see Agent Integration Guide)
else:
# Standard flow: forward URL to human
send_to_user(f"{hitl['prompt']}\n{hitl['review_url']}")
# Poll for result (standard flow or fallback)
while True:
time.sleep(30)
poll = httpx.get(hitl["poll_url"], headers=auth).json()
if poll["status"] == "completed":
result = poll["result"] # {action: "select", data: {...}}
break
if poll["status"] in ("expired", "cancelled"):
break
No SDK. No UI rendering. Just HTTP + URL forwarding + polling. See Agent Integration Guide for inline submit, SSE, callbacks, multi-round, and edge cases.
Three Transport Modes
| Transport | Agent needs public endpoint? | Real-time? | Complexity |
|---|---|---|---|
| Polling (default) | No | No | Minimal |
| SSE (optional) | No | Yes | Low |
| Callback (optional) | Yes | Yes | Medium |
Polling is the baseline — every HITL-compliant service MUST support it. SSE and callbacks are optional enhancements.
Channel-Native Inline Actions (v0.6)
For simple decisions, agents can render native messaging buttons instead of sending a URL. The human taps a button directly in the chat — no browser switch needed.
How it works: The service includes submit_url + submit_token in the HITL object. The agent detects these fields and renders platform-native buttons. When the human taps a button, the agent POSTs the action to submit_url. The messaging platform is passive — it renders whatever the agent sends. No messenger auto-detects HITL support.
When to use inline buttons:
| Review type | Inline possible? | Reason |
|---|---|---|
| Confirmation | Yes | 2 buttons: Confirm / Cancel |
| Escalation | Yes | 3 buttons: Retry / Skip / Abort |
| Approval (simple) | Yes | 2 buttons: Approve / Reject (without edit) |
| Approval (with edit) | URL only | Edit requires rich UI |
| Selection | URL only | Needs list/cards UI |
| Input | URL only | Needs form fields |
Always include a URL fallback button (e.g. "Details →") linking to review_url — the human can always switch to the full review page.
Platform requirements: The agent must be a platform bot (Telegram Bot via BotFather, Slack App, Discord Bot, WhatsApp Business API, Teams Bot) to send native buttons. See Agent Integration Guide for platform-specific rendering patterns.
Non-Goals
- Does NOT render review UI — the service hosts and renders the review page. The agent is a messenger.
- Does NOT define the review page framework — any web technology works (React, plain HTML, etc.).
- Does NOT replace OAuth — HITL is for decisions, not authentication.
- Does NOT submit on behalf of the human — unless the human explicitly triggers an inline action button in a messaging platform (v0.6
submit_url).
SKILL.md Extension for Services
Services that use HITL can declare support in their own SKILL.md frontmatter:
metadata:
hitl:
supported: true
types: [selection, confirmation]
supports_inline_submit: true
review_base_url: "https://yourservice.com/review"
timeout_default: "24h"
info: "May ask user to select preferred jobs or confirm applications."
See spec Section 12 for the full field reference.
Resources
- Full Specification (v0.6)
- OpenAPI 3.1 Spec — all endpoints documented
- JSON Schemas — HITL object, poll response, form field, submit request definitions
- Reference Implementations — Express 5, Hono, Next.js, FastAPI
- Review Page Templates — HTML templates for all 5 review types
- Examples — 12 end-to-end flows (incl. inline confirmation, escalation, hybrid approval)
- Agent Implementation Checklist — detailed agent guide with pseudocode
- Interactive Playground
- SDK Design Guide — build a community SDK