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:X-Orca-Event without parsing
the body.
2. Envelope fields
event — the event name
event — the event name
Always
firewall.approval.pending for an approval hold. Mirrored in
the X-Orca-Event header so you can route before parsing the body.workspace_id — the originating workspace
workspace_id — the originating workspace
The integer id of the workspace whose policy held the call. Useful when
one endpoint receives webhooks from several workspaces.
occurred_at — when the hold was created
occurred_at — when the hold was created
RFC 3339 / UTC timestamp (nanosecond precision) for when the gateway
enqueued the approval. Parseable by any standard event tooling.
data — the approval payload
data — the approval payload
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.
| Field | Type | Meaning |
|---|---|---|
approval_id | string | The id you post your decision against. |
tool_name | string | The held tool, e.g. db.export. |
request_id | string | The relay request that triggered the hold. |
conversation_id | string | The agent conversation / session id. |
policy_id | int | The firewall policy that matched. |
rule_id | int | The rule that returned pending_approval. |
4. Verifying the signature
Every delivery is signed so you can reject forgeries. The signature header is: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:
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.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).Author a pending_approval rule
Add a Firewall rule whose verdict is
pending_approval (or use the
HITL preset). See Firewall rules.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::id you received as approval_id, signed with the same
shared secret. The body is a decision:
| Body field | Required | Values |
|---|---|---|
decision | yes | approved or rejected |
reason | no | Free-text note, recorded in the audit log. |
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 ofpending:
| State | Meaning |
|---|---|
pending | Awaiting a decision (the state at webhook time). |
approved | Resolved — the gated call may proceed once. |
rejected | Resolved — the gated call stays blocked. |
expired | The hold aged out without a decision. |
8. Related references
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.
