Back to Blog
Architecture 2026-01-28

Webhook Approval Flow

How iddio routes escalation decisions to external systems. HMAC-signed webhook notifications, HTTP callback handlers, Slack integration, and the Approver interface.

The Approval Problem

Phase 1 of iddio uses terminal-based approval: when an agent’s command is escalated, the proxy prints a prompt in the operator’s terminal and waits for y/N. This works for a single operator watching a single proxy, but it doesn’t scale.

Real teams need escalation decisions routed to the systems they already use — Slack, PagerDuty, Microsoft Teams, or custom internal tools. The operator should see the escalation request in context, make a decision, and have that decision flow back to the proxy automatically.

The Approver Interface

Iddio defines an Approver interface that decouples the approval mechanism from the proxy core:

type Approver interface {
    RequestApproval(ctx context.Context, req ApprovalRequest) (ApprovalResult, error)
}

type ApprovalRequest struct {
    ID        string
    Agent     string
    Method    string
    Path      string
    Namespace string
    Resource  string
    Tier      int
    Timestamp time.Time
}

type ApprovalResult struct {
    Approved  bool
    Approver  string
    Reason    string
    Timestamp time.Time
}

The proxy doesn’t know or care which approver is active. It calls RequestApproval, which blocks until a decision arrives or the timeout expires (default: 60 seconds). If the timeout expires, the request is denied — fail-closed.

Webhook Notification Flow

The WebhookApprover sends an HTTP POST to a configured endpoint when an escalation occurs. The payload includes all the information needed to make a decision:

{
  "id": "esc_a1b2c3d4",
  "agent": "claude-code",
  "method": "DELETE",
  "path": "/api/v1/namespaces/payments/pods/api-7d4b8f6c9",
  "namespace": "payments",
  "resource": "pods",
  "tier": 3,
  "tier_label": "SENSITIVE",
  "timestamp": "2026-01-28T10:15:30Z",
  "callback_url": "https://proxy.internal:6443/api/v1/approvals/esc_a1b2c3d4",
  "expires_at": "2026-01-28T10:16:30Z"
}

The notification is signed with HMAC-SHA256 using a shared secret, sent in the X-Iddio-Signature header. This prevents forged notifications from triggering bogus approval UIs.

Callback Handler

The external system makes its decision (human clicks “Approve” in Slack, or an automated policy engine evaluates it) and sends an HTTP POST back to the callback_url:

{
  "id": "esc_a1b2c3d4",
  "approved": true,
  "approver": "alice@company.com",
  "reason": "Routine pod cleanup"
}

The callback is also HMAC-signed. The proxy verifies both the signature and the escalation ID before accepting the decision.

HMAC Signature Verification

Every webhook notification and callback is signed:

func signPayload(secret []byte, body []byte) string {
    mac := hmac.New(sha256.New, secret)
    mac.Write(body)
    return hex.EncodeToString(mac.Sum(nil))
}

func verifySignature(secret []byte, body []byte, sig string) bool {
    expected := signPayload(secret, body)
    return hmac.Equal([]byte(expected), []byte(sig))
}

Note the use of hmac.Equal for constant-time comparison — this prevents timing attacks where an attacker probes the signature byte-by-byte.

Slack Integration

The most common webhook target is Slack. Iddio includes a built-in Slack message formatter that converts the webhook payload into a Block Kit message with Approve/Deny buttons:

# ~/.iddio/policy.yaml
approval:
  mode: webhook
  webhook:
    url: https://hooks.slack.com/services/T00/B00/xxx
    secret: ${IDDIO_WEBHOOK_SECRET}
    timeout: 60s
    format: slack

When format: slack is set, the webhook payload is wrapped in Slack Block Kit JSON. The operator sees a formatted message with the agent name, command, tier classification, and two buttons. Clicking a button sends the callback back to the proxy.

Configuration

The webhook approver is configured in policy.yaml:

approval:
  mode: webhook # or "terminal" for Phase 1 behavior
  webhook:
    url: https://your-endpoint.example.com/iddio/escalations
    secret: ${IDDIO_WEBHOOK_SECRET} # env var expansion
    timeout: 60s # max wait for callback
    retry:
      max_attempts: 3
      backoff: 1s

The mode field selects the approver implementation. Both terminal and webhook can be active simultaneously — use mode: hybrid to prompt in the terminal AND send a webhook, accepting whichever response arrives first.

Timeout Behavior

If no callback arrives within the configured timeout:

  1. The pending request is denied (fail-closed)
  2. The denial is recorded in the audit log with decision: "timeout"
  3. The agent receives an HTTP 403 with a message explaining the timeout

This ensures that network failures or unresponsive external systems never result in an unintended allow. The proxy is fail-closed at every decision point.

Try It Yourself

Iddio is open source. Deploy a zero-trust command proxy for your AI agents in minutes.