Skip to main content
When a Firewall rule returns the pending_approval verdict, the gateway holds the tool call and notifies your own approval system out-of-band. That notification is a signed HTTP POST — the firewall webhook payload — and this page documents its exact shape so you can verify the signature, route the event, and post your decision back. This is the async sibling of the in-console approval flow described on the Firewall page. The console path (a reviewer clicks approve/reject) needs no webhook at all. The webhook is for when you want a machine — your own ticketing bot, Slack action, or agent-runtime — to resolve the hold. Both paths are first-writer-wins, so you can run them side by side.
The webhook is a best-effort routing signal, not the authoritative HITL channel. The agent’s own long-poll on GET /api/v1/firewall/approvals/:id is the backstop — if a notification is dropped or your endpoint is briefly down, the held call still surfaces in the console and resolves normally. The webhook just lets a machine react faster than a human would.

1. The firewall webhook payload at a glance

OrcaRouter POSTs a JSON envelope to the URL you configure, signed with a shared secret. Here’s a complete delivery — headers and body:
POST /your-approval-endpoint HTTP/1.1
Content-Type: application/json
X-Orca-Event: firewall.approval.pending
X-Orca-Signature: sha256=9f86d0818988...c2c9a3e1b4d7

{
  "event": "firewall.approval.pending",
  "workspace_id": 42,
  "occurred_at": "2026-06-09T12:00:00.123456789Z",
  "data": {
    "approval_id": "665f1a2b3c4d5e6f7a8b9c0d",
    "tool_name": "db.export",
    "request_id": "req_01J9X...",
    "conversation_id": "conv_8f2a...",
    "policy_id": 7,
    "rule_id": 31
  }
}
The envelope is the same shape OrcaRouter uses for every signed event, so one receiver can route many event types off X-Orca-Event without parsing the body.

2. Envelope fields

Always firewall.approval.pending for an approval hold. Mirrored in the X-Orca-Event header so you can route before parsing the body.
The integer id of the workspace whose policy held the call. Useful when one endpoint receives webhooks from several workspaces.
RFC 3339 / UTC timestamp (nanosecond precision) for when the gateway enqueued the approval. Parseable by any standard event tooling.
The block your callback needs to resolve the gate. Fields in §3.

3. The data payload

The data block carries everything needed to route and resolve the hold — deliberately no tool arguments. The webhook is a routing signal; the full call context (tool, args, the rule that fired) lives in the console Approvals tab and the audit log, where it is access-controlled.
FieldTypeMeaning
approval_idstringThe id you post your decision against.
tool_namestringThe held tool, e.g. db.export.
request_idstringThe relay request that triggered the hold.
conversation_idstringThe agent conversation / session id.
policy_idintThe firewall policy that matched.
rule_idintThe rule that returned pending_approval.
Need the arguments or the matched clause to make the decision? Read them from the console Approvals tab (Developer+), or have your agent poll GET /api/v1/firewall/approvals/:id with its gateway token. The webhook intentionally never carries args over the wire.

4. Verifying the signature

Every delivery is signed so you can reject forgeries. The signature header is:
X-Orca-Signature: sha256=<hex HMAC-SHA256(secret, raw_body)>
where secret is the approval-webhook secret you set on the workspace and raw_body is the exact bytes of the request body. Compute the HMAC over the raw bytes — re-serializing the parsed JSON will change whitespace and break the comparison. Verify in constant time:
import hmac, hashlib

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header)
The outbound webhook signature covers the body only. The inbound callback you post back (§6) signs approval_id + a newline + body — a different construction, on purpose, so a captured signature can’t be replayed across approvals. Don’t reuse one signing routine for both directions.

5. Configuring the webhook

The destination URL and shared secret are workspace settings — set them once in the console (or via the settings API; Developer+). There’s no operator involvement and nothing to deploy.
1

Set the URL and secret

In the Firewall settings, set your HTTPS endpoint as the approval webhook URL and a strong shared secret. The URL must be https:// — plaintext destinations are rejected — and the secret is write-only (it’s never returned on read; the settings response only reports whether one is set).
2

Author a pending_approval rule

Add a Firewall rule whose verdict is pending_approval (or use the HITL preset). See Firewall rules.
3

Receive and verify

Your endpoint receives the signed POST on the next held call. Verify the signature (§4), then either resolve via the callback or surface it for a human.
A held call still works with no webhook configured — it just shows up in the console for a human to resolve. And if you set a URL but no secret, the gateway skips the dispatch entirely, because the callback endpoint only accepts signed requests: a notification you couldn’t authenticate back would be useless.

6. The callback: resolving the hold

To approve or reject by machine, POST back to:
POST /api/v1/firewall/approvals/:id/callback
with the same :id you received as approval_id, signed with the same shared secret. The body is a decision:
BODY='{"decision":"approved","reason":"ticket OPS-4821 approved by on-call"}'
SIG="sha256=$(printf '%s\n%s' "$APPROVAL_ID" "$BODY" \
  | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')"

curl https://api.orcarouter.ai/api/v1/firewall/approvals/$APPROVAL_ID/callback \
  -H "Content-Type: application/json" \
  -H "X-Orca-Signature: $SIG" \
  -d "$BODY"
Body fieldRequiredValues
decisionyesapproved or rejected
reasonnoFree-text note, recorded in the audit log.
An approved decision lets the agent’s next attempt through once — the agent re-submits the original call with a single-use X-OrcaRouter-Firewall-Approval header. A rejected decision keeps the call blocked.
Resolution is idempotent and first-writer-wins. If a human already resolved the hold from the console — or a duplicate callback arrives — the endpoint returns 200 with already_resolved: true and the original decision stands. Safe to retry.

7. Approval states

A held call moves through these states; your callback drives the transition out of pending:
StateMeaning
pendingAwaiting a decision (the state at webhook time).
approvedResolved — the gated call may proceed once.
rejectedResolved — the gated call stays blocked.
expiredThe hold aged out without a decision.

Firewall — HITL flow

How pending_approval holds a call and the agent re-submits with the single-use approval header.

Error codes

firewall_approval_pending and the other firewall HTTP responses.

Verdict glossary

Every firewall verdict, including pending_approval.

Firewall API

The full console + gateway route reference.
For how holds fit the broader control model, see Enforcement modes and Dangerous tool calls.