Skip to main content
When a firewall rule returns pending_approval, the tool call is held and your agent waits. By default a reviewer clears that hold from the console. The firewall approval webhook wires the same gate into your system: the gateway POSTs a signed notification to your endpoint the moment a call is held, and your system POSTs an HMAC-signed decision back to release it — no console seat, no polling a human. This is the async (callback) half of human-in-the-loop. The held call, the verdict, and the console resolve path are covered in Resolve approvals and the Firewall reference; this page is just the webhook wiring.
The webhook is a fast-path heads-up, not the system of record. The relay’s long-poll on the held call is the authoritative gate — if a notification is dropped or your receiver is down, the hold still stands and a reviewer can clear it from the console. Configuring the webhook only adds a programmatic way to resolve it.

1. When to use a firewall approval webhook

Reach for it when a human-in-the-loop firewall gate has to be resolved by something other than a person clicking a button:

Route to your own approvals UI

Push held tool calls into Slack, PagerDuty, or an internal review queue and resolve them where your team already works.

Programmatic policy

Auto-approve a held db.query against a read replica, auto-reject one against prod — your code decides, the gateway enforces.

2. Configure it in the console

Both halves live on one workspace setting. Open Firewall → Settings and fill in two fields (a Developer+ action — the settings write is role-gated):
The https:// endpoint we POST to when a call is held. HTTP is refused, and the URL is run through an SSRF preflight on save, so a loopback, private-range, or cloud-metadata destination is rejected before it can be stored. Leave it empty to disable the async path entirely.
A write-only HMAC secret (up to 255 characters). It signs our outbound notification and authenticates your inbound callback. The console never echoes it back — once saved you see only that a secret is set; rotate by writing a new value.
The callback endpoint is HMAC-only. Until a shared secret is set, the gateway will not deliver notifications and rejects every callback — the gate can only be cleared from the console. Set the secret to turn the async path on.
Prefer the REST API? The same fields update through the console settings route (UserAuth, Developer+):
curl -X PUT https://api.orcarouter.ai/api/workspace/firewall/settings \
  -H "Authorization: Bearer $ORCA_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "approval_webhook_url": "https://hooks.example.com/orca/firewall",
        "approval_webhook_secret": "whsec_your_shared_secret"
      }'

3. The notification we send you

When a call is held, we POST a signed JSON envelope to your URL:
POST /orca/firewall HTTP/1.1
Content-Type: application/json
X-Orca-Event: firewall.approval.pending
X-Orca-Signature: sha256=<hex>
{
  "event": "firewall.approval.pending",
  "workspace_id": 42,
  "occurred_at": "2026-06-09T17:04:11.482Z",
  "data": {
    "approval_id": "665f1b...",
    "tool_name": "db.query",
    "request_id": "req_9f2c...",
    "conversation_id": "conv_77a1...",
    "policy_id": 7,
    "rule_id": 31
  }
}
The envelope is a routing signal, not the full context — it carries the approval_id you need to resolve and identifiers to correlate, never the tool arguments. The argument detail lives in the Approvals queue and the firewall events log.
Verify the outbound signature before you trust the payload: X-Orca-Signature is sha256= + hex HMAC-SHA256(secret, raw_body) over the exact bytes you received. Compare in constant time. Delivery is at-least-once and retried on transient failures, so make your handler idempotent on approval_id.

4. The callback you POST back

To release (or reject) the hold, POST your decision to the callback endpoint with the approval_id from the notification:
POST /api/v1/firewall/approvals/665f1b.../callback
X-Orca-Signature: sha256=<hex>
Content-Type: application/json

{ "decision": "approved", "reason": "read-replica query, auto-approved" }
decision is approved or rejected — no other value is accepted. reason is optional and shows up on the resolved approval’s audit trail.
The callback signature is bound to the approval id, so a captured signature can’t be replayed against a different held call. Sign the string <approval_id> + a literal newline (\n) + the raw request body:
X-Orca-Signature = "sha256=" + HMAC_SHA256(secret, approval_id + "\n" + body)
This differs from the outbound notification, which signs the body alone. A callback whose signature doesn’t verify gets 401 — and a missing, mismatched, or non-existent approval id returns the same 401, so the endpoint never confirms whether an id exists.
The callback is first-decision-wins and idempotent: whoever resolves first — your webhook or a console reviewer — sets the outcome, and a repeat callback for an already-resolved approval returns 200 so your system stops retrying.

5. Releasing the held call

Resolving the approval doesn’t replay the tool call for you — it lifts the gate so your agent can re-issue it. The agent runtime:
  1. Polls the hold’s state at GET /api/v1/firewall/approvals/:id (a firewall-gateway-scoped key, not your relay key or console session) until it leaves pending.
  2. On approved, re-submits the original tool call carrying a single-use X-OrcaRouter-Firewall-Approval header — the gateway lets that one call through and the token is spent.
If the underlying rule was edited after the call was held, the Approvals queue flags that the rule has since changed and suppresses the now-stale “held because…” clause, so a console reviewer doesn’t act on provenance that no longer matches what held the call.

6. Verify the wiring

A quick end-to-end check before you depend on it:
StepWhat to doExpected
Hold a callTrigger a rule with a pending_approval verdict400 firewall_approval_pending
NotificationWatch your endpointSigned firewall.approval.pending POST arrives
CallbackPOST a signed { "decision": "approved" }200 with the resolved state
Replay guardPOST the callback again200, already resolved (no double-apply)
If the notification never arrives, confirm the secret is set (without it the gateway won’t deliver) and that the URL passed the HTTPS + SSRF checks at save time.

7. Where this fits

Resolve approvals

The console reviewer path and the full held-call lifecycle.

Verdicts

Where pending_approval comes from and how it composes with other verdicts.

Gateway keys

Mint the firewall-gateway-scoped key the poll + re-submit flow needs.

Excessive agency

The threat human-in-the-loop gates are built to contain.
For the verdict model, enforcement surfaces, and the rest of the firewall, see the Firewall reference.