wakeprotocol.com / spec / v1

WAKE Protocol
Specification

v1.0 Draft MIT Licence
Published March 2026
Schema v1.json

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.

New to WAKE? Jump to the Quickstart to see a working implementation in under 5 minutes, or the System Prompt Template to make any agent WAKE-compliant immediately.

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

#PrincipleMeaning
01Asynchronous by defaultAgents never block waiting for a response. They deliver, record the ID, and continue.
02Pull, not pushThe human decides when to tend to deliveries. Agents deliver to a place the human visits.
03Minimal surface areaTwo endpoints. One schema. No dependencies. Value comes from focus, not comprehensiveness.
04Human experience firstThe payload schema includes fields designed for humans: positive headlines, plain summaries, structured responses.
05Provider agnosticWorks 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.

POST /wake/v1/deliver
// 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

FieldTypeRequiredDescription
agent_idstringYesUnique agent identifier. Chosen by implementer. Max 128 chars.
providerstringYesAI provider name. Used for display only. Example: "claude"
typeenumYesSee Delivery Types.
headlinestringYesHuman-readable title. Frame positively. Max 120 chars.
summarystringYesOne sentence plain language description. Max 280 chars.
detailsobject | string | nullNoFull delivery content. Treat as untrusted — never evaluate as code.
callback_webhookstring | nullNoHTTPS URL for push response delivery. Must match allowlist.
timeout_secondsinteger | nullNoSeconds before agent proceeds with assumptions. Min 60, max 604800.

Error responses

StatusMeaning
400Malformed body or missing required fields
401Authentication failed
422Validation error — unknown type, headline too long, invalid webhook URL
429Rate 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.

GET /wake/v1/response/{delivery_id}
// 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

ParameterTypeRequiredDescription
agent_idstringNoFilter to deliveries from a specific agent.
statusenumNoComma-separated statuses to include: pending, approved, rejected, redirected.
sinceISO 8601NoReturn only deliveries created or responded to after this timestamp. Use next_since from the previous response to paginate.
limitintegerNoMax results. Default: 50. Maximum: 200.
GET /wake/v1/responses?agent_id=research-agent-01&status=approved,rejected,redirected
// 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

Python — single loop for all concurrent deliveries
# 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.

Webhook request headers
X-Wake-Signature:  sha256={HMAC_SHA256_HEX}
X-Wake-Delivery-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type:       application/json
Always verify X-Wake-Signature before processing a webhook callback. Reject any request where the signature does not match.

Delivery Types

TypeDescriptionResponse required
updateAgent reporting progress on an ongoing task.No — but one may be given
questionAgent has reached a decision point and needs human input before proceeding.Yes
outputAgent has completed a piece of work and is presenting it for review or approval.Recommended
alertAgent has encountered an unexpected condition the human should be aware of.Depends on severity

Response Statuses

StatusMeaningRequired 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
1Post a delivery payload before taking any action that warrants human oversight.
2Record the returned delivery_id for later response retrieval.
3Not block indefinitely. Proceed after timeout_seconds with documented assumptions.
4On rejected: not proceed with the originally proposed action.
5On redirected: incorporate edited_content and feedback fully before proceeding.
6Verify each retrieved delivery_id matches a delivery it submitted.
7Log all deliveries and responses locally for audit.
8Use HTTPS for all protocol traffic.
9Never 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.

bash
# 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", ... }'
Python
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)
Node.js
// 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)
});
One key per agent. If one key is compromised, no other agent is affected. Store all keys in environment variables or a secrets manager — never in source code. Keys prefixed 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.

Webhook headers
X-Wake-Signature:  sha256=d4c51f6b8a3e2f1a...
X-Wake-Delivery-Id: 550e8400-e29b-41d4-a716-446655440000
Content-Type:       application/json
Python — verify webhook
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
Node.js — verify webhook (Express)
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...
});
Always use constant-time comparison (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.

Python — handle 429 correctly
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 typeDeliveries / hourBurst
wk_test_205
wk_live_50050

4. Secrets Management

Never put API keys or webhook secrets in source code or commit them to a repository.

bash — .env (development)
# .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

ThreatMitigationPattern
Unauthenticated deliveryBearer token required on all endpointsRFC 6750
Transport interceptionHTTPS only — HTTP rejected, not redirectedRFC 8446
Webhook payload tamperingHMAC-SHA256 signature + constant-time verifySame as Stripe, GitHub
Delivery ID enumerationUUID v4 (cryptographically random)RFC 4122
Agent flooding / loopsPer-key rate limiting + 429 + Retry-AfterRFC 6585
Credential exposureEnv vars / secrets manager — never in source12-factor app
Agent impersonationIdentity bound to API key, not agent_id field
Prompt injection via detailsTreat 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.

Python — complete implementation
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.

System prompt — copy verbatim
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
Replace [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.

RFCTitleStatus
RFC-001Batched deliveriesOpen
RFC-002Priority signallingOpen
RFC-003Rich media schema for details fieldOpen
RFC-004Multi-human routingOpen
RFC-005Agent-to-agent deliveryOpen
RFC-006Cryptographic agent identity and trust tiersOpen