WAKE Protocol
Specification
WAKE — While Agents Keep Executing — is a lightweight, open protocol that defines how an autonomous AI agent surfaces work to a human, and how the human responds.
It is designed to be trivially easy to implement, provider-agnostic, and built around human needs rather than technical convenience. Any agent that can make an HTTP request can implement WAKE in under an hour.
Core Concepts
WAKE is built on two interactions and five principles. Everything else is an implementation concern.
Delivery
An agent posts a structured payload to a designated endpoint describing completed work, a question, a status update, or an alert. It receives a delivery_id in return and continues operating without blocking.
Response
A human reviews the delivery at their own pace and responds with a structured action. The agent retrieves this response via polling or webhook callback and acts accordingly.
Design Principles
| # | Principle | Meaning |
|---|---|---|
01 | Asynchronous by default | Agents never block waiting for a response. They deliver, record the ID, and continue. |
02 | Pull, not push | The human decides when to tend to deliveries. Agents deliver to a place the human visits. |
03 | Minimal surface area | Two endpoints. One schema. No dependencies. Value comes from focus, not comprehensiveness. |
04 | Human experience first | The payload schema includes fields designed for humans: positive headlines, plain summaries, structured responses. |
05 | Provider agnostic | Works with any agent, any runtime, any AI provider. Claude, OpenAI, Grok, custom — all the same protocol. |
POST /wake/v1/deliver
Submit a delivery from an agent to the human receiver.
// Request body { "agent_id": "research-agent-01", // required "provider": "claude", // required "type": "output", // required — update|question|output|alert "headline": "Market report ready for your review", "summary": "Analysed top 10 competitors in the space.", "details": { "url": "https://...", "word_count": 3200 }, "callback_webhook": "https://your-agent/wake-callback", "timeout_seconds": 3600 } // 201 Created { "delivery_id": "550e8400-e29b-41d4-a716-446655440000", "status": "received", "created_at": "2026-03-07T08:31:00Z" }
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | Yes | Unique agent identifier. Chosen by implementer. Max 128 chars. |
provider | string | Yes | AI provider name. Used for display only. Example: "claude" |
type | enum | Yes | See Delivery Types. |
headline | string | Yes | Human-readable title. Frame positively. Max 120 chars. |
summary | string | Yes | One sentence plain language description. Max 280 chars. |
details | object | string | null | No | Full delivery content. Treat as untrusted — never evaluate as code. |
callback_webhook | string | null | No | HTTPS URL for push response delivery. Must match allowlist. |
timeout_seconds | integer | null | No | Seconds before agent proceeds with assumptions. Min 60, max 604800. |
Error responses
| Status | Meaning |
|---|---|
400 | Malformed body or missing required fields |
401 | Authentication failed |
422 | Validation error — unknown type, headline too long, invalid webhook URL |
429 | Rate limit exceeded |
GET /wake/v1/response/{delivery_id}
Retrieve the human's response to a delivery. Poll this endpoint or use callback_webhook for push delivery. Recommended poll interval: 60 seconds.
// 200 OK — awaiting response { "delivery_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "feedback": null, "edited_content": null, "responded_at": null } // 200 OK — human approved { "delivery_id": "550e8400-e29b-41d4-a716-446655440000", "status": "approved", "feedback": "Great work — focus on Series B next.", "edited_content": null, "responded_at": "2026-03-07T09:14:22Z" } // 200 OK — human redirected with edits { "delivery_id": "550e8400-e29b-41d4-a716-446655440000", "status": "redirected", "feedback": "Good start — cut section 3, expand section 5.", "edited_content": { "updated_brief": "..." }, "responded_at": "2026-03-07T09:14:22Z" }
GET /wake/v1/responses
Retrieve responses for multiple deliveries in a single request. Designed for agents managing concurrent tasks — replaces N parallel polls with a single sweep per agent. Recommended for all orchestrators.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
agent_id | string | No | Filter to deliveries from a specific agent. |
status | enum | No | Comma-separated statuses to include: pending, approved, rejected, redirected. |
since | ISO 8601 | No | Return only deliveries created or responded to after this timestamp. Use next_since from the previous response to paginate. |
limit | integer | No | Max results. Default: 50. Maximum: 200. |
// 200 OK { "deliveries": [ { "delivery_id": "550e8400-e29b-41d4-a716-446655440000", "status": "approved", "feedback": null, "edited_content": null, "responded_at": "2026-03-08T11:42:00Z" }, { "delivery_id": "661f9511-f30c-52e5-b827-557766551111", "status": "redirected", "feedback": "Add a rollback migration before merging.", "edited_content": null, "responded_at": "2026-03-08T11:55:00Z" } ], "total": 2, "has_more": false, "next_since": "2026-03-08T11:55:00Z" }
Use next_since as the since parameter on the next request to retrieve only new responses — this avoids reprocessing items already handled.
Recommended polling pattern for orchestrators
# One HTTP request per interval, regardless of how many deliveries are outstanding since = None while agent_is_running: params = { "agent_id": agent_id, "status": "approved,rejected,redirected", } if since: params["since"] = since r = httpx.get(f"{WAKE_ENDPOINT}/responses", headers=HEADERS, params=params) data = r.json() for item in data["deliveries"]: dispatch_response(item["delivery_id"], item) if data["deliveries"]: since = data["next_since"] time.sleep(poll_interval)
Webhook Callback
When callback_webhook is set, the implementation POSTs the response payload to that URL when the human responds — identical to the GET response body.
X-Wake-Signature: sha256={HMAC_SHA256_HEX} X-Wake-Delivery-Id: 550e8400-e29b-41d4-a716-446655440000 Content-Type: application/json
X-Wake-Signature before processing a webhook callback. Reject any request where the signature does not match.
Delivery Types
| Type | Description | Response required |
|---|---|---|
update | Agent reporting progress on an ongoing task. | No — but one may be given |
question | Agent has reached a decision point and needs human input before proceeding. | Yes |
output | Agent has completed a piece of work and is presenting it for review or approval. | Recommended |
alert | Agent has encountered an unexpected condition the human should be aware of. | Depends on severity |
Response Statuses
| Status | Meaning | Required agent behaviour |
|---|---|---|
| pending | Human has not yet responded. | Continue polling or await webhook. |
| approved | Human approves the delivery as submitted. | Proceed as planned. |
| rejected | Human does not approve the proposed action. | Do not proceed. Review feedback field. |
| redirected | Human has edited content and/or provided specific guidance. | Incorporate edited_content and feedback fully before proceeding. |
Agent Behaviour Requirements
MUST
| # | Requirement |
|---|---|
1 | Post a delivery payload before taking any action that warrants human oversight. |
2 | Record the returned delivery_id for later response retrieval. |
3 | Not block indefinitely. Proceed after timeout_seconds with documented assumptions. |
4 | On rejected: not proceed with the originally proposed action. |
5 | On redirected: incorporate edited_content and feedback fully before proceeding. |
6 | Verify each retrieved delivery_id matches a delivery it submitted. |
7 | Log all deliveries and responses locally for audit. |
8 | Use HTTPS for all protocol traffic. |
9 | Never include sensitive credentials in the details field. |
SHOULD
Frame headline values positively — lead with what has been accomplished. Set timeout_seconds appropriate to urgency. Register a callback_webhook to avoid polling overhead. Continue working on other tasks while awaiting a response. When managing multiple concurrent deliveries, use GET /wake/v1/responses with agent_id and since filters rather than polling each delivery individually.
Security
WAKE does not invent new security primitives. It uses the same patterns you already know from Stripe, GitHub, and Twilio — applied consistently. The examples below are production-ready and can be used verbatim.
1. Authentication — Bearer Tokens (RFC 6750)
Every request to a WAKE endpoint must include a Bearer token in the Authorization header. Your API key is generated by the WAKE implementation — never by the agent itself.
# Every request curl -X POST https://api.gardenroom.ai/wake/v1/deliver \ -H "Authorization: Bearer wk_live_a1b2c3d4e5f6..." \ -H "Content-Type: application/json" \ -d '{ "agent_id": "research-agent-01", ... }'
import os, httpx # Load from environment — never hardcode WAKE_API_KEY = os.environ["WAKE_API_KEY"] HEADERS = { "Authorization": f"Bearer {WAKE_API_KEY}", "Content-Type": "application/json" } httpx.post("https://api.gardenroom.ai/wake/v1/deliver", headers=HEADERS, json=payload)
// Load from environment — never hardcode const WAKE_API_KEY = process.env.WAKE_API_KEY; const response = await fetch("https://api.gardenroom.ai/wake/v1/deliver", { method: "POST", headers: { "Authorization": `Bearer ${WAKE_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify(payload) });
wk_test_ are for development only; wk_live_ are production.2. Webhook Signatures — HMAC-SHA256
When a human responds, GardenRoom POSTs the response to your callback_webhook. Every payload is signed with HMAC-SHA256 — the same pattern used by Stripe and GitHub webhooks. Always verify the signature before processing.
X-Wake-Signature: sha256=d4c51f6b8a3e2f1a... X-Wake-Delivery-Id: 550e8400-e29b-41d4-a716-446655440000 Content-Type: application/json
import hmac, hashlib, os WEBHOOK_SECRET = os.environ["WAKE_WEBHOOK_SECRET"] def verify_wake_webhook(payload_body: bytes, signature_header: str) -> bool: """ payload_body — raw request body as bytes, BEFORE any JSON parsing signature_header — value of X-Wake-Signature header """ expected = "sha256=" + hmac.new( WEBHOOK_SECRET.encode("utf-8"), payload_body, hashlib.sha256 ).hexdigest() # Always use constant-time comparison — never == return hmac.compare_digest(expected, signature_header) # In your handler: if not verify_wake_webhook(request.body, request.headers["X-Wake-Signature"]): return 401 # reject — may have been tampered with
const crypto = require("crypto"); const WEBHOOK_SECRET = process.env.WAKE_WEBHOOK_SECRET; function verifyWakeWebhook(payloadBody, signatureHeader) { // payloadBody must be raw Buffer — not parsed JSON const expected = "sha256=" + crypto .createHmac("sha256", WEBHOOK_SECRET) .update(payloadBody) .digest("hex"); // Always use timingSafeEqual — never === return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signatureHeader) ); } app.post("/wake-callback", express.raw({ type: "application/json" }), (req, res) => { if (!verifyWakeWebhook(req.body, req.headers["x-wake-signature"])) { return res.status(401).send("Invalid signature"); } const response = JSON.parse(req.body); // handle response... });
hmac.compare_digest / crypto.timingSafeEqual). Regular string comparison (== / ===) is vulnerable to timing attacks.
3. Rate Limiting
When a limit is exceeded the endpoint returns 429 Too Many Requests with a Retry-After header. Respect it — an agent hitting rate limits repeatedly is almost always caught in a loop.
def deliver_with_retry(payload, max_retries=3): for attempt in range(max_retries): r = httpx.post(WAKE_ENDPOINT + "/deliver", headers=HEADERS, json=payload) if r.status_code == 429: retry_after = int(r.headers.get("Retry-After", 60)) time.sleep(retry_after) continue r.raise_for_status() return r.json() # Still failing after retries — surface an alert, don't loop raise Exception("Rate limit exceeded after retries — check for agent loop")
| Key type | Deliveries / hour | Burst |
|---|---|---|
wk_test_ | 20 | 5 |
wk_live_ | 500 | 50 |
4. Secrets Management
Never put API keys or webhook secrets in source code or commit them to a repository.
# .env — add to .gitignore WAKE_API_KEY=wk_live_a1b2c3d4e5f6... WAKE_WEBHOOK_SECRET=whsec_x9y8z7... # Production: AWS Secrets Manager, GCP Secret Manager, # HashiCorp Vault, Doppler, or Vercel Environment Variables
Threat Model
| Threat | Mitigation | Pattern |
|---|---|---|
| Unauthenticated delivery | Bearer token required on all endpoints | RFC 6750 |
| Transport interception | HTTPS only — HTTP rejected, not redirected | RFC 8446 |
| Webhook payload tampering | HMAC-SHA256 signature + constant-time verify | Same as Stripe, GitHub |
| Delivery ID enumeration | UUID v4 (cryptographically random) | RFC 4122 |
| Agent flooding / loops | Per-key rate limiting + 429 + Retry-After | RFC 6585 |
| Credential exposure | Env vars / secrets manager — never in source | 12-factor app |
| Agent impersonation | Identity bound to API key, not agent_id field | — |
| Prompt injection via details | Treat details as untrusted — sanitise before render | — |
Quickstart
A complete, production-ready WAKE implementation in Python. Copy it verbatim — replace the four configuration values at the top.
import os, time, hmac, hashlib import httpx # ── Configuration — load from environment, never hardcode ── WAKE_ENDPOINT = os.environ["WAKE_ENDPOINT"] # https://api.gardenroom.ai/wake/v1 WAKE_API_KEY = os.environ["WAKE_API_KEY"] # wk_live_... WAKE_WEBHOOK_SECRET = os.environ.get("WAKE_WEBHOOK_SECRET") AGENT_ID = os.environ["WAKE_AGENT_ID"] # research-agent-01 HEADERS = { "Authorization": f"Bearer {WAKE_API_KEY}", "Content-Type": "application/json" } def deliver(type, headline, summary, details=None, timeout=3600): r = httpx.post(f"{WAKE_ENDPOINT}/deliver", headers=HEADERS, json={ "agent_id": AGENT_ID, "provider": "claude", "type": type, "headline": headline, "summary": summary, "details": details, "timeout_seconds": timeout }) if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", 60))) return deliver(type, headline, summary, details, timeout) # retry once r.raise_for_status() return r.json()["delivery_id"] def await_response(delivery_id, poll=60, max_wait=3600): elapsed = 0 while elapsed < max_wait: data = httpx.get( f"{WAKE_ENDPOINT}/response/{delivery_id}", headers=HEADERS ).json() if data["status"] != "pending": return data time.sleep(poll); elapsed += poll return {"status": "timeout"} def verify_webhook(payload_body: bytes, signature_header: str) -> bool: expected = "sha256=" + hmac.new( WAKE_WEBHOOK_SECRET.encode(), payload_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature_header) # ── Usage ── delivery_id = deliver( type="output", headline="Market research report ready for your review", summary="Completed analysis of the top 10 competitors.", details={"url": "https://...", "word_count": 3200} ) response = await_response(delivery_id) if response["status"] == "approved": proceed() elif response["status"] == "rejected": handle(response["feedback"]) elif response["status"] == "redirected": incorporate(response["edited_content"]) elif response["status"] == "timeout": proceed_with_assumptions()
System Prompt Template
Copy this into any agent's system prompt or instructions. Replace the four placeholder values — the agent handles everything else including secure key loading and webhook verification.
You are working autonomously on behalf of a human. When you complete
significant work, reach a decision point, or encounter something unexpected,
deliver a structured update via WAKE before proceeding.
WAKE endpoint: [YOUR_WAKE_ENDPOINT]/wake/v1/deliver
Your agent ID: [YOUR_AGENT_ID]
API key env var: WAKE_API_KEY ← load from environment, never hardcode
Webhook secret: WAKE_WEBHOOK_SECRET ← load from environment, never hardcode
HOW TO DELIVER
POST [YOUR_WAKE_ENDPOINT]/wake/v1/deliver
Authorization: Bearer $WAKE_API_KEY
Content-Type: application/json
{ "agent_id", "provider", "type", "headline", "summary", "details", "timeout_seconds" }
Store the returned delivery_id.
Poll GET .../wake/v1/response/{delivery_id} every 60s until status != "pending".
HOW TO HANDLE RESPONSES
"approved" → proceed as planned
"rejected" → do not proceed; review feedback field
"redirected" → incorporate edited_content and feedback fully before continuing
timeout → proceed with clearly documented assumptions
HOW TO VERIFY WEBHOOK CALLBACKS
Compute: sha256=HMAC_SHA256(WAKE_WEBHOOK_SECRET, raw_request_body)
Compare with X-Wake-Signature header using constant-time comparison.
Reject the request if they do not match.
SECURITY RULES — YOU MUST FOLLOW THESE
- Load WAKE_API_KEY and WAKE_WEBHOOK_SECRET from environment only. Never hardcode.
- Never include credentials, passwords, or PII in the details field.
- If you receive 401: stop, deliver a "question" asking the human to rotate the key.
- If you receive 429: read Retry-After header, wait, retry once. If still failing,
deliver a single "alert" — do not loop.
- Verify every delivery_id matches one you submitted.
- Use HTTPS only. Reject any WAKE endpoint beginning with http://.
DELIVERY RULES
- Frame headlines positively — lead with what has been accomplished
- One sentence summary in plain language
- Do not block — continue working while awaiting a response
- One delivery per meaningful event — do not flood
The human is your collaborator. Surface your work with confidence.
Accept their guidance with grace.
Full spec: https://wakeprotocol.com/spec/v1.md
Reference implementation: https://gardenroom.ai
[YOUR_WAKE_ENDPOINT] and [YOUR_AGENT_ID] with your values. Using GardenRoom.ai? Your endpoint and a pre-scoped API key are in your account settings — the agent handles the rest.Open RFCs
The following questions are open for community discussion. Contribute at github.com/wakeprotocol/spec/discussions.
| RFC | Title | Status |
|---|---|---|
RFC-001 | Batched deliveries | Open |
RFC-002 | Priority signalling | Open |
RFC-003 | Rich media schema for details field | Open |
RFC-004 | Multi-human routing | Open |
RFC-005 | Agent-to-agent delivery | Open |
RFC-006 | Cryptographic agent identity and trust tiers | Open |