Skip to content

10.2 Gateway API Reference

Nikolay Vyahhi edited this page Feb 19, 2026 · 2 revisions

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.


Overview

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 in config.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


Connection and Transport

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
Loading

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


Endpoint Reference

GET /health

Health check endpoint (always public, no authentication required).

Request:

GET /health HTTP/1.1
Host: localhost:3000

Response: 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 from crate::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


GET /metrics

Prometheus text exposition format metrics (public, no authentication required).

Request:

GET /metrics HTTP/1.1
Host: localhost:3000

Response: 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 OK with appropriate content type

Implementation: src/gateway/mod.rs:524-542

Configuration:

[observability]
backend = "prometheus"  # Required for metrics export

Sources: src/gateway/mod.rs:522-542, src/gateway/mod.rs:1022-1087


POST /pair

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: 123456

Headers:

  • 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
Loading

Implementation: src/gateway/mod.rs:544-604

Token Storage:

  • Bearer token hashed with SHA-256 before storage
  • Persisted to config.toml under [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


POST /webhook

Main webhook endpoint for processing arbitrary messages through the LLM.

Authentication:

  1. Bearer token (required if pairing enabled): Authorization: Bearer <token>
  2. 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 if gateway.require_pairing = true)
  • X-Webhook-Secret (conditional): Webhook secret (required if channels_config.webhook.secret configured)
  • 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
Loading

Implementation: src/gateway/mod.rs:620-804

Memory Auto-Save:

  • If memory.auto_save = true, stores message with key webhook_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


GET /whatsapp

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:3000

Query Parameters:

  • hub.mode: Must be "subscribe"
  • hub.verify_token: Must match channels_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


POST /whatsapp

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
Loading

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:

  1. ZEROCLAW_WHATSAPP_APP_SECRET environment variable
  2. channels_config.whatsapp.app_secret in config

Message Processing:

  • Parses webhook payload using WhatsAppChannel::parse_webhook_payload()
  • Filters messages by allowed_numbers allowlist
  • Auto-saves to memory if memory.auto_save = true
  • Sends reply via WhatsApp Business API

Memory Auto-Save:

Sources: src/gateway/mod.rs:806-868, src/gateway/mod.rs:870-968


Common Response Headers

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


Rate Limiting

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
Loading

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-IP

Client Key Resolution:

  1. If trust_forwarded_headers = true: Parse X-Forwarded-For or X-Real-IP
  2. Otherwise: Use peer socket address
  3. 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 = 300 at 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


Idempotency Support

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
Loading

Configuration:

[gateway]
idempotency_ttl_secs = 300      # 5 minutes
idempotency_max_keys = 10000    # Max keys retained in memory

Behavior:

  • Keys expire after idempotency_ttl_secs (default: 300s)
  • Bounded cardinality: evicts oldest key when limit exceeded
  • Returns 200 OK with {"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


Error Codes Summary

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


Example Client Integration

cURL Examples

Health Check:

curl http://localhost:3000/health

Pairing:

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/metrics

Python Example

import 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"])

JavaScript/TypeScript Example

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


Configuration Reference

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 numbers

Sources: src/gateway/mod.rs:282-505, src/config.rs


Security Best Practices

  1. Always use pairing authentication in production (require_pairing = true)
  2. Enable webhook secret for defense-in-depth
  3. Deploy behind a tunnel (Tailscale/Cloudflare/ngrok) instead of public binding
  4. Configure rate limits appropriate to your traffic patterns
  5. Use idempotency keys for critical operations
  6. Rotate bearer tokens periodically (re-pair clients)
  7. Monitor metrics endpoint for anomalies
  8. Verify WhatsApp signatures (app_secret must be configured)
  9. Set trust_forwarded_headers = true only behind trusted reverse proxy
  10. 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


Clone this wiki locally