Skip to main content
WebhookApprovalProvider sends one signed POST to a URL you own for every approval request, and takes the answer back through a signed POST to the gateway’s /ctrlrun/approvals/ endpoint. Your side is a small service that turns the request into a Slack message with two buttons and turns the click into the answer. The grant is written by the same call ctrlrun approve makes; there is no second approval path. Prerequisites: pip install ctrlrun (the outbound half is core); pip install "ctrlrun[gateway]" for the inbound endpoint, which the gateway serves; a shared secret; a URL that can receive the POST.
1

Configure the provider

In a process using the decorator:
With the gateway, the same thing is two flags:
The secret comes from $CTRLRUN_WEBHOOK_SECRET or a file, never from a flag value: a secret on a command line is in every process listing on the host. An http:// URL is refused unless --allow-insecure-webhook, and then only on loopback.
2

Receive the request

One POST per APPROVAL_REQUESTED, with the CTRLRun-Signature header, an HMAC-SHA256 over timestamp.body computed on the exact bytes sent. Verify it before you read the body.
Show the human exactly what is in action: the name, every argument, who is asking. The approval will be bound to that hash, so what they see is what will run.
3

Send the answer back

POST to respond_to, signed the same way, with a timestamp within the replay window (five minutes by default):
decision is grant or deny; approver is a non-empty string recorded on the receipt. A replayed grant inside the window is idempotent, because the record is already granted; outside it, the timestamp check refuses. The endpoint answers 200 on success and a 4xx with the reason on a bad signature, an unknown request id, an expired request, or a request already answered the other way.
4

Watch it land

If the human never answers, the request expires at expires_at (the approval TTL, fifteen minutes by default) and a waiting call raises ApprovalTimeout. Nothing runs.

What the provider does not do

It does not build the Slack message or the buttons; that is your service, which knows your workspace. It does not authenticate the approver: approver is recorded as given, which is the threat model’s stated limit. It does not retry into the future: delivery is two retries with a short backoff, and an undeliverable request is logged and left pending for ctrlrun approve.

If it didn’t work

  • signature does not verify: the secret differs, or your side re-serialized the body before signing. Sign the exact bytes.
  • timestamp outside the replay window: clocks differ by more than five minutes.
  • ApprovalTimeout: nobody answered within the TTL; the request is expired, not lost.
  • The webhook never fires: the provider is not on the Control the protected function uses. With the decorator, pass control=control.

Next