Inbound webhooks
orch webhook serve: a local HMAC-verified receiver that turns a signed POST into a task, from n8n/Zapier, GitHub or Slack.
Goal#
Let an outside trigger — an n8n/Zapier flow, a GitHub issue or comment, a
Slack slash command — create an Orchemax task and, once approved, dispatch it.
orch webhook serve is a small local HTTP server, loopback-only by default,
with three routes: /task, /github, /slack. This is the inbound
counterpart of orch notify (see "Notifications and approvals"), which only
sends events out.
Steps#
-
Configure a shared secret and the routes you want in
orch.yaml:webhook: secret_env: ORCH_WEBHOOK_SECRET allow: [task, github, slack] # omit = all three require_approval: true # default; false lets a signed request dispatch immediately templates: cursor: agent: cursor prompt: "Read the open PRs and summarize." timeout_sec: 300 github: # used by GitHub "issue opened/labelled orch" events agent: billingORCH_WEBHOOK_SECRETmust be set in the environmentorch webhook serveruns under. Every route verifies a signature computed from that same secret, in its own scheme (below). -
Start the server:
orch webhook serve --addr 127.0.0.1:8790The listener refuses a non-loopback address (
0.0.0.0, a bare:8790, a LAN IP) unless you pass--allow-remote— put it behind your own reverse proxy/tunnel if it needs to be reachable from GitHub or Slack's servers. -
Check the wiring with a signed sample request:
orch webhook test --route task --agent cursor --prompt "reply with ok" --dispatch --approve
POST /task#
Body (JSON): {"template": "cursor", "project": "...", "dispatch": true, "approve": false}
(or agent + prompt instead of template). Headers:
X-Orch-Timestamp (unix seconds) and X-Orch-Signature — hex HMAC-SHA256 of
"<timestamp>.<raw body>" under the shared secret.
Response: {"task_id", "status", "dispatched", "session_id"}. The task is
always created; it only dispatches when dispatch:true and either
approve:true or webhook.require_approval: false — otherwise a
needs_approval event goes out through orch notify (webhooks/Telegram),
and the task waits for orch task run <id> or a future approval action.
POST /github#
Verifies X-Hub-Signature-256 (GitHub's own HMAC-SHA256-over-raw-body
scheme). Two events:
issues(opened, orlabeledwith theorchlabel): creates a task from the issue title/body usingwebhook.templates.github(agent/project come from that template — configure it, or the event is ignored).issue_comment(a comment starting with/orch <template> <extra prompt>, on an issue or a PR): creates a task fromwebhook.templates.<template>, the rest of the line replacing the template's default prompt.
Both dispatch under the same require_approval rule as /task — a raw
GitHub event carries no approve field, so it only runs immediately when
webhook.require_approval: false.
POST /slack#
Verifies Slack's v0 signing scheme: X-Slack-Request-Timestamp +
X-Slack-Signature: v0=<hex hmac(secret, "v0:"+timestamp+":"+body)>. Body is
Slack's own application/x-www-form-urlencoded slash-command payload — the
text field is <template> <prompt>. Replies 200 immediately with an
ephemeral ack ({"response_type":"ephemeral","text":"..."}) naming the task
id and whether it dispatched or is waiting for approval.
n8n#
Use an HTTP Request node (not the Webhook trigger — that's for receiving, you
are calling out to Orchemax). Point it at http://<host>:8790/task, method
POST, JSON body, and add two headers computed in a Function/Code node
before it: X-Orch-Timestamp = current unix time, X-Orch-Signature = hex
HMAC-SHA256 of "<timestamp>.<body>" with your ORCH_WEBHOOK_SECRET. Most
crypto/HMAC nodes or a one-line Code node cover this.
Zapier#
Use the "Webhooks by Zapier" action, "Custom Request" (POST), same URL and
JSON body. Compute the two headers with a "Code by Zapier" step (Node.js
crypto.createHmac('sha256', secret)) immediately before the webhook step,
since Zapier has no built-in HMAC action.
What never leaves the machine#
Every response is status/summary JSON only — no code, no file contents, no diff. The 256 KB body cap, 5-minute timestamp skew and a 10-minute replay cache (by signature) apply to every route.