-
Notifications
You must be signed in to change notification settings - Fork 4.4k
10.2 Gateway API Reference
Relevant source files
The following files were used as context for generating this wiki page:
This page provides a complete reference for all HTTP endpoints exposed by the ZeroClaw Gateway. The gateway provides webhook integration, health monitoring, metrics collection, and WhatsApp Business API support. For security configuration including pairing, bearer tokens, and rate limiting, see Gateway Security. For overall gateway architecture and deployment, see Gateway.
The Gateway is an Axum-based HTTP server that exposes RESTful endpoints for external integrations. It implements proper HTTP/1.1 compliance with body size limits (64KB), request timeouts (30s), and layered security including pairing authentication, webhook secrets, and signature verification.
Base Configuration:
- Default bind address:
127.0.0.1:3000(localhost only) - Configurable via
[gateway]section inconfig.toml - Requires tunnel (Tailscale/Cloudflare/ngrok) or explicit opt-in for public binding
Key Features:
- Pairing-based authentication with bearer tokens
- Optional webhook secret for additional security
- Idempotency support for duplicate request prevention
- Rate limiting per client IP with configurable thresholds
- WhatsApp Business API webhook integration
- Prometheus metrics export
Sources: src/gateway/mod.rs:1-505
graph TB
subgraph "Network Layer"
Client["External Client"]
Tunnel["Tunnel<br/>(Tailscale/Cloudflare/ngrok)"]
Bind["TCP Listener<br/>127.0.0.1:3000"]
end
subgraph "Middleware Stack"
BodyLimit["RequestBodyLimitLayer<br/>MAX_BODY_SIZE=64KB"]
Timeout["TimeoutLayer<br/>REQUEST_TIMEOUT_SECS=30s"]
end
subgraph "Router"
Routes["Axum Router<br/>/health, /metrics, /pair<br/>/webhook, /whatsapp"]
end
subgraph "Handlers"
Health["handle_health"]
Metrics["handle_metrics"]
Pair["handle_pair"]
Webhook["handle_webhook"]
WAVerify["handle_whatsapp_verify"]
WAMessage["handle_whatsapp_message"]
end
Client -->|HTTPS| Tunnel
Tunnel -->|HTTP| Bind
Bind --> BodyLimit
BodyLimit --> Timeout
Timeout --> Routes
Routes -->|"GET /health"| Health
Routes -->|"GET /metrics"| Metrics
Routes -->|"POST /pair"| Pair
Routes -->|"POST /webhook"| Webhook
Routes -->|"GET /whatsapp"| WAVerify
Routes -->|"POST /whatsapp"| WAMessage
Transport Constants:
-
MAX_BODY_SIZE: 65,536 bytes (64KB) - prevents memory exhaustion -
REQUEST_TIMEOUT_SECS: 30 seconds - prevents slow-loris attacks -
RATE_LIMIT_WINDOW_SECS: 60 seconds - sliding window for rate limiting
Sources: src/gateway/mod.rs:37-44, src/gateway/mod.rs:282-505
Health check endpoint (always public, no authentication required).
Request:
GET /health HTTP/1.1
Host: localhost:3000Response: 200 OK
{
"status": "ok",
"paired": true,
"runtime": {
"components": {
"gateway": "ok",
"channels": "ok",
"scheduler": "ok"
},
"uptime_secs": 3600
}
}Fields:
-
status: Always"ok"if server is responding -
paired: Boolean indicating if at least one client is paired -
runtime: Live health snapshot fromcrate::health::snapshot_json()
Implementation: src/gateway/mod.rs:512-519
Use Cases:
- Load balancer health checks
- Monitoring system probes
- Container orchestration readiness checks
Sources: src/gateway/mod.rs:512-519
Prometheus text exposition format metrics (public, no authentication required).
Request:
GET /metrics HTTP/1.1
Host: localhost:3000Response: 200 OK
Content-Type: text/plain; version=0.0.4; charset=utf-8
# HELP zeroclaw_heartbeat_ticks_total Heartbeat tick count
# TYPE zeroclaw_heartbeat_ticks_total counter
zeroclaw_heartbeat_ticks_total 42
# HELP zeroclaw_llm_requests_total LLM request count by provider
# TYPE zeroclaw_llm_requests_total counter
zeroclaw_llm_requests_total{provider="openrouter",model="claude-sonnet-4"} 15
# HELP zeroclaw_tool_calls_total Tool execution count by tool name
# TYPE zeroclaw_tool_calls_total counter
zeroclaw_tool_calls_total{tool="shell"} 8
zeroclaw_tool_calls_total{tool="file_read"} 12
Behavior:
- Returns hint message if Prometheus backend not enabled in config
- Otherwise encodes all metrics from
PrometheusObserver - Always returns
200 OKwith appropriate content type
Implementation: src/gateway/mod.rs:524-542
Configuration:
[observability]
backend = "prometheus" # Required for metrics exportSources: src/gateway/mod.rs:522-542, src/gateway/mod.rs:1022-1087
Exchange one-time pairing code for bearer token.
Rate Limiting: Configurable via gateway.pair_rate_limit_per_minute (default: unlimited)
Request:
POST /pair HTTP/1.1
Host: localhost:3000
X-Pairing-Code: 123456Headers:
-
X-Pairing-Code(required): 6-digit one-time code displayed in gateway startup logs
Response (Success): 200 OK
{
"paired": true,
"persisted": true,
"token": "a1b2c3d4e5f6...",
"message": "Save this token — use it as Authorization: Bearer <token>"
}Response (Invalid Code): 403 Forbidden
{
"error": "Invalid pairing code"
}Response (Rate Limited): 429 Too Many Requests
{
"error": "Too many pairing requests. Please retry later.",
"retry_after": 60
}Response (Lockout): 429 Too Many Requests
{
"error": "Too many failed attempts. Try again in 120s.",
"retry_after": 120
}Pairing Flow:
sequenceDiagram
participant Client
participant Gateway as "POST /pair Handler"
participant RateLimit as "GatewayRateLimiter"
participant PairingGuard
participant Config as "Config Storage"
Client->>Gateway: X-Pairing-Code: 123456
Gateway->>RateLimit: allow_pair(client_ip)
alt Rate limit exceeded
RateLimit-->>Gateway: false
Gateway-->>Client: 429 Too Many Requests
else Within rate limit
RateLimit-->>Gateway: true
Gateway->>PairingGuard: try_pair(code)
alt Valid code
PairingGuard-->>Gateway: Ok(Some(token))
Gateway->>Config: persist_pairing_tokens()
Config-->>Gateway: Ok
Gateway-->>Client: 200 OK {token}
else Invalid code
PairingGuard-->>Gateway: Ok(None)
Gateway-->>Client: 403 Forbidden
else Locked out
PairingGuard-->>Gateway: Err(lockout_secs)
Gateway-->>Client: 429 Too Many Requests
end
end
Implementation: src/gateway/mod.rs:544-604
Token Storage:
- Bearer token hashed with SHA-256 before storage
- Persisted to
config.tomlunder[gateway] paired_tokens - Function:
persist_pairing_tokens()at src/gateway/mod.rs:606-612
Security Notes:
- Pairing code expires after first successful use
- Lockout triggered after 5 failed attempts (60-second exponential backoff)
- Constant-time comparison prevents timing attacks
Sources: src/gateway/mod.rs:544-612, src/security/pairing.rs:1-300
Main webhook endpoint for processing arbitrary messages through the LLM.
Authentication:
- Bearer token (required if pairing enabled):
Authorization: Bearer <token> - Webhook secret (optional):
X-Webhook-Secret: <secret>
Rate Limiting: Configurable via gateway.webhook_rate_limit_per_minute (default: unlimited)
Request:
POST /webhook HTTP/1.1
Host: localhost:3000
Authorization: Bearer a1b2c3d4e5f6...
X-Webhook-Secret: my-webhook-secret
X-Idempotency-Key: req-uuid-12345
Content-Type: application/json
{
"message": "What is the weather like today?"
}Headers:
-
Authorization(conditional): Bearer token from pairing (required ifgateway.require_pairing = true) -
X-Webhook-Secret(conditional): Webhook secret (required ifchannels_config.webhook.secretconfigured) -
X-Idempotency-Key(optional): Unique key for duplicate prevention (TTL: 300s) -
Content-Type(required):application/json
Request Body:
{
"message": "string (required)"
}Response (Success): 200 OK
{
"response": "The weather is sunny today with temperatures around 75°F.",
"model": "anthropic/claude-sonnet-4"
}Response (Unauthorized - Missing Bearer): 401 Unauthorized
{
"error": "Unauthorized — pair first via POST /pair, then send Authorization: Bearer <token>"
}Response (Unauthorized - Invalid Secret): 401 Unauthorized
{
"error": "Unauthorized — invalid or missing X-Webhook-Secret header"
}Response (Rate Limited): 429 Too Many Requests
{
"error": "Too many webhook requests. Please retry later.",
"retry_after": 60
}Response (Duplicate): 200 OK
{
"status": "duplicate",
"idempotent": true,
"message": "Request already processed for this idempotency key"
}Response (Bad Request): 400 Bad Request
{
"error": "Invalid JSON body. Expected: {\"message\": \"...\"}"
}Response (LLM Error): 500 Internal Server Error
{
"error": "LLM request failed"
}Authentication and Processing Flow:
sequenceDiagram
participant Client
participant Handler as "handle_webhook"
participant RateLimit as "GatewayRateLimiter"
participant Pairing as "PairingGuard"
participant SecretCheck as "Webhook Secret Hash"
participant Idempotency as "IdempotencyStore"
participant Memory as "Memory Backend"
participant Provider as "LLM Provider"
Client->>Handler: POST /webhook {message}
Handler->>RateLimit: allow_webhook(client_ip)
alt Rate limit exceeded
RateLimit-->>Handler: false
Handler-->>Client: 429 Too Many Requests
else Within limit
RateLimit-->>Handler: true
alt Pairing required
Handler->>Pairing: is_authenticated(bearer_token)
alt Invalid token
Pairing-->>Handler: false
Handler-->>Client: 401 Unauthorized
else Valid token
Pairing-->>Handler: true
end
end
alt Webhook secret configured
Handler->>SecretCheck: constant_time_eq(header_hash, stored_hash)
alt Invalid secret
SecretCheck-->>Handler: false
Handler-->>Client: 401 Unauthorized
else Valid secret
SecretCheck-->>Handler: true
end
end
alt Idempotency key present
Handler->>Idempotency: record_if_new(key)
alt Duplicate
Idempotency-->>Handler: false
Handler-->>Client: 200 OK {duplicate}
else New request
Idempotency-->>Handler: true
end
end
Handler->>Memory: store(auto_save)
Handler->>Provider: simple_chat(message, model, temp)
Provider-->>Handler: response
Handler-->>Client: 200 OK {response, model}
end
Implementation: src/gateway/mod.rs:620-804
Memory Auto-Save:
- If
memory.auto_save = true, stores message with keywebhook_msg_<uuid> - Category:
MemoryCategory::Conversation - Function:
webhook_memory_key()at src/gateway/mod.rs:48-50
Webhook Secret Verification:
- Secret is SHA-256 hashed (never stored plaintext)
- Hash comparison uses constant-time equality to prevent timing attacks
- Function:
hash_webhook_secret()at src/gateway/mod.rs:56-61
Sources: src/gateway/mod.rs:620-804, src/gateway/mod.rs:48-61
WhatsApp webhook verification (Meta Platform requirement).
Request:
GET /whatsapp?hub.mode=subscribe&hub.verify_token=my-verify-token&hub.challenge=challenge-string HTTP/1.1
Host: localhost:3000Query Parameters:
-
hub.mode: Must be"subscribe" -
hub.verify_token: Must matchchannels_config.whatsapp.verify_token -
hub.challenge: Random string from Meta
Response (Success): 200 OK
challenge-string
Response (Invalid Token): 403 Forbidden
Forbidden
Response (Missing Challenge): 400 Bad Request
Missing hub.challenge
Response (Not Configured): 404 Not Found
WhatsApp not configured
Implementation: src/gateway/mod.rs:817-841
Token Verification:
- Uses constant-time comparison to prevent timing attacks
- Function:
constant_time_eq()from src/security/pairing.rs
Sources: src/gateway/mod.rs:806-841
WhatsApp incoming message webhook.
Authentication: HMAC-SHA256 signature verification via X-Hub-Signature-256 header
Request:
POST /whatsapp HTTP/1.1
Host: localhost:3000
X-Hub-Signature-256: sha256=abc123def456...
Content-Type: application/json
{
"object": "whatsapp_business_account",
"entry": [{
"id": "phone-number-id",
"changes": [{
"value": {
"messaging_product": "whatsapp",
"messages": [{
"from": "1234567890",
"id": "wamid.xxx",
"timestamp": "1234567890",
"text": { "body": "Hello" },
"type": "text"
}]
}
}]
}]
}Headers:
-
X-Hub-Signature-256(required): HMAC-SHA256 signature of request body -
Content-Type(required):application/json
Response (Success): 200 OK
{
"status": "ok"
}Response (Invalid Signature): 401 Unauthorized
{
"error": "Invalid signature"
}Response (Not Configured): 404 Not Found
{
"error": "WhatsApp not configured"
}Response (Bad Request): 400 Bad Request
{
"error": "Invalid JSON payload"
}Signature Verification Flow:
graph TB
Request["POST /whatsapp<br/>with body + signature"]
ExtractSig["Extract X-Hub-Signature-256"]
CheckSecret{App secret<br/>configured?}
ComputeHMAC["Compute HMAC-SHA256<br/>of request body"]
CompareSignature["Constant-time comparison<br/>expected vs actual"]
Valid{Valid?}
ParsePayload["Parse JSON payload"]
ExtractMessages["WhatsAppChannel::parse_webhook_payload()"]
ProcessMessages["For each message:<br/>LLM + send reply"]
Accept["200 OK"]
Reject["401 Unauthorized"]
Request --> ExtractSig
ExtractSig --> CheckSecret
CheckSecret -->|No secret| ParsePayload
CheckSecret -->|Yes| ComputeHMAC
ComputeHMAC --> CompareSignature
CompareSignature --> Valid
Valid -->|Yes| ParsePayload
Valid -->|No| Reject
ParsePayload --> ExtractMessages
ExtractMessages --> ProcessMessages
ProcessMessages --> Accept
Implementation: src/gateway/mod.rs:870-968
Signature Verification:
- Function:
verify_whatsapp_signature()at src/gateway/mod.rs:846-868 - Uses HMAC-SHA256 with app secret from config or environment variable
- Constant-time comparison prevents timing attacks
- Format:
sha256=<hex-encoded-hmac>
App Secret Priority:
-
ZEROCLAW_WHATSAPP_APP_SECRETenvironment variable -
channels_config.whatsapp.app_secretin config
Message Processing:
- Parses webhook payload using
WhatsAppChannel::parse_webhook_payload() - Filters messages by
allowed_numbersallowlist - Auto-saves to memory if
memory.auto_save = true - Sends reply via WhatsApp Business API
Memory Auto-Save:
- Key format:
whatsapp_<sender>_<message_id> - Function:
whatsapp_memory_key()at src/gateway/mod.rs:52-54
Sources: src/gateway/mod.rs:806-868, src/gateway/mod.rs:870-968
All successful responses include:
Content-Type: application/json; charset=utf-8
Metrics endpoint uses:
Content-Type: text/plain; version=0.0.4; charset=utf-8
Timeout responses (after 30s) return:
HTTP/1.1 408 Request Timeout
Sources: src/gateway/mod.rs:492-495
The gateway implements sliding-window rate limiting with separate limits for pairing and webhook endpoints.
Rate Limiter Architecture:
graph TB
subgraph "GatewayRateLimiter"
PairLimiter["SlidingWindowRateLimiter<br/>(pair endpoint)"]
WebhookLimiter["SlidingWindowRateLimiter<br/>(webhook endpoint)"]
end
subgraph "SlidingWindowRateLimiter"
Window["window: Duration = 60s"]
Limit["limit_per_window: u32"]
MaxKeys["max_keys: usize (bounded cardinality)"]
Requests["requests: HashMap<String, Vec<Instant>>"]
LastSweep["last_sweep: Instant"]
end
PairLimiter --> Window
PairLimiter --> Limit
PairLimiter --> MaxKeys
PairLimiter --> Requests
PairLimiter --> LastSweep
WebhookLimiter --> Window
WebhookLimiter --> Limit
WebhookLimiter --> MaxKeys
WebhookLimiter --> Requests
WebhookLimiter --> LastSweep
Configuration:
[gateway]
pair_rate_limit_per_minute = 10 # Per client IP
webhook_rate_limit_per_minute = 60 # Per client IP
rate_limit_max_keys = 10000 # Max distinct IPs tracked
trust_forwarded_headers = false # Use X-Forwarded-For / X-Real-IPClient Key Resolution:
- If
trust_forwarded_headers = true: ParseX-Forwarded-FororX-Real-IP - Otherwise: Use peer socket address
- Function:
client_key_from_request()at src/gateway/mod.rs:235-249
Sliding Window Algorithm:
- Records timestamp for each request per client IP
- Prunes timestamps older than window (60s)
- Rejects request if count >= limit within window
- Implementation: src/gateway/mod.rs:66-158
Cardinality Bounding:
- If distinct client count exceeds
max_keys, evicts least-recently-used entry - Periodic sweep every 5 minutes removes stale entries
- Constant:
RATE_LIMITER_SWEEP_INTERVAL_SECS = 300at src/gateway/mod.rs:64
Zero-Limit Bypass:
- If limit configured as
0, rate limiting is disabled - Useful for development or when rate limiting is handled upstream
Sources: src/gateway/mod.rs:64-158, src/gateway/mod.rs:235-257
The webhook endpoint supports idempotency via the X-Idempotency-Key header to prevent duplicate processing.
Idempotency Store Architecture:
graph TB
IdempotencyKey["X-Idempotency-Key: req-123"]
Store["IdempotencyStore"]
Keys["keys: HashMap<String, Instant>"]
RecordCheck["record_if_new(key)"]
CheckExists{Key exists?}
TTLCheck{Within TTL?}
RecordNew["Record key + timestamp"]
ReturnNew["Return true<br/>(process request)"]
ReturnDup["Return false<br/>(skip processing)"]
IdempotencyKey --> Store
Store --> Keys
Store --> RecordCheck
RecordCheck --> CheckExists
CheckExists -->|No| RecordNew
CheckExists -->|Yes| TTLCheck
TTLCheck -->|Yes| ReturnDup
TTLCheck -->|No| RecordNew
RecordNew --> ReturnNew
Configuration:
[gateway]
idempotency_ttl_secs = 300 # 5 minutes
idempotency_max_keys = 10000 # Max keys retained in memoryBehavior:
- Keys expire after
idempotency_ttl_secs(default: 300s) - Bounded cardinality: evicts oldest key when limit exceeded
- Returns
200 OKwith{"status": "duplicate", "idempotent": true}for duplicates - Implementation: src/gateway/mod.rs:160-200
Usage Example:
# First request
curl -X POST http://localhost:3000/webhook \
-H "Authorization: Bearer token" \
-H "X-Idempotency-Key: order-12345" \
-H "Content-Type: application/json" \
-d '{"message": "Process order 12345"}'
# Response: {"response": "Order processed", "model": "..."}
# Duplicate request (within TTL)
curl -X POST http://localhost:3000/webhook \
-H "Authorization: Bearer token" \
-H "X-Idempotency-Key: order-12345" \
-H "Content-Type: application/json" \
-d '{"message": "Process order 12345"}'
# Response: {"status": "duplicate", "idempotent": true, "message": "Request already processed for this idempotency key"}Sources: src/gateway/mod.rs:160-200, src/gateway/mod.rs:684-700
| Status Code | Condition | Example Response |
|---|---|---|
200 OK |
Successful request | {"response": "...", "model": "..."} |
400 Bad Request |
Invalid JSON body or missing required field | {"error": "Invalid JSON body. Expected: {\"message\": \"...\"}"} |
401 Unauthorized |
Missing/invalid bearer token or webhook secret | {"error": "Unauthorized — pair first via POST /pair..."} |
403 Forbidden |
Invalid pairing code | {"error": "Invalid pairing code"} |
404 Not Found |
Endpoint not configured (e.g., WhatsApp) | "WhatsApp not configured" |
408 Request Timeout |
Request exceeded 30-second timeout | (Empty body) |
413 Payload Too Large |
Request body exceeds 64KB | (Rejected by middleware) |
429 Too Many Requests |
Rate limit exceeded or pairing lockout | {"error": "Too many requests...", "retry_after": 60} |
500 Internal Server Error |
LLM provider error or internal failure | {"error": "LLM request failed"} |
Sources: src/gateway/mod.rs:544-968
Health Check:
curl http://localhost:3000/healthPairing:
curl -X POST http://localhost:3000/pair \
-H "X-Pairing-Code: 123456"Webhook with Authentication:
curl -X POST http://localhost:3000/webhook \
-H "Authorization: Bearer <your-token>" \
-H "X-Webhook-Secret: my-secret" \
-H "X-Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"message": "What is 2+2?"}'Metrics:
curl http://localhost:3000/metricsimport requests
# Pair and get token
pairing_response = requests.post(
"http://localhost:3000/pair",
headers={"X-Pairing-Code": "123456"}
)
token = pairing_response.json()["token"]
# Send webhook message
webhook_response = requests.post(
"http://localhost:3000/webhook",
headers={
"Authorization": f"Bearer {token}",
"X-Webhook-Secret": "my-secret",
"Content-Type": "application/json"
},
json={"message": "Summarize this document: ..."}
)
print(webhook_response.json()["response"])const GATEWAY_URL = "http://localhost:3000";
// Pair and get token
async function pair(code: string): Promise<string> {
const response = await fetch(`${GATEWAY_URL}/pair`, {
method: "POST",
headers: { "X-Pairing-Code": code }
});
const data = await response.json();
return data.token;
}
// Send webhook message
async function sendMessage(token: string, message: string): Promise<string> {
const response = await fetch(`${GATEWAY_URL}/webhook`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"X-Webhook-Secret": "my-secret",
"Content-Type": "application/json"
},
body: JSON.stringify({ message })
});
const data = await response.json();
return data.response;
}Sources: src/gateway/mod.rs:544-968
Complete gateway configuration section from config.toml:
[gateway]
# Bind address
host = "127.0.0.1"
port = 3000
# Authentication
require_pairing = true # Force bearer token auth
paired_tokens = [] # SHA-256 hashed tokens (auto-populated)
# Rate Limiting
pair_rate_limit_per_minute = 10 # 0 = unlimited
webhook_rate_limit_per_minute = 60 # 0 = unlimited
rate_limit_max_keys = 10000 # Max distinct IPs tracked
trust_forwarded_headers = false # Trust X-Forwarded-For / X-Real-IP
# Idempotency
idempotency_ttl_secs = 300 # 5 minutes
idempotency_max_keys = 10000 # Max idempotency keys retained
# Security
allow_public_bind = false # Allow binding to 0.0.0.0 without tunnel
[channels_config.webhook]
secret = "webhook-secret-here" # Optional webhook secret (SHA-256 hashed)
[channels_config.whatsapp]
access_token = "EAAxxxx..." # Meta Platform access token
phone_number_id = "123456789" # WhatsApp Business phone number ID
verify_token = "my-verify-token" # Webhook verification token
app_secret = "app-secret-here" # App secret for signature verification
allowed_numbers = ["+1234567890"] # Allowlist of phone numbersSources: src/gateway/mod.rs:282-505, src/config.rs
-
Always use pairing authentication in production (
require_pairing = true) - Enable webhook secret for defense-in-depth
- Deploy behind a tunnel (Tailscale/Cloudflare/ngrok) instead of public binding
- Configure rate limits appropriate to your traffic patterns
- Use idempotency keys for critical operations
- Rotate bearer tokens periodically (re-pair clients)
- Monitor metrics endpoint for anomalies
-
Verify WhatsApp signatures (
app_secretmust be configured) -
Set
trust_forwarded_headers = trueonly behind trusted reverse proxy - Review paired tokens periodically and remove unused ones
For detailed security configuration, see Gateway Security.
Sources: src/gateway/mod.rs:282-292, src/security/pairing.rs:1-300