Webhooks
Trigger agents from external events. GitHub PR opened? Run code-reviewer. New Stripe charge? Update the CRM. Whale alert? Investigate.
How it works
- External service POSTs to
https://<gateway>/webhook/<path> - Gateway verifies HMAC signature using the shared secret
- Payload is parsed as JSON
- Filter conditions checked (e.g., only PR
openedevents) - Agent is invoked with the expanded prompt template
- Result delivered to configured target
Configuration
Define webhooks in config/webhooks.yaml:
webhooks:
- path: /webhook/github-pr
secret_env: GITHUB_WEBHOOK_SECRET
signature_header: x-hub-signature-256
signature_algo: sha256
signature_prefix: "sha256="
agent: code-reviewer
filter:
action: opened
prompt_template: |
A new PR was opened: {{event.pull_request.title}}
Repo: {{event.repository.full_name}}
Author: {{event.pull_request.user.login}}
URL: {{event.pull_request.html_url}}
Review it.
deliver_to: "slack:#engineering"
enabled: falseFields
| Field | Required | Description |
|---|---|---|
path | Yes | URL path (must start with /) |
agent | Yes | Agent mode to invoke |
prompt_template | Yes | Prompt with {{event.path}} interpolation |
secret_env | Recommended | Env var holding HMAC secret |
signature_header | No | Header containing signature (default: x-hub-signature-256) |
signature_algo | No | sha1 or sha256 (default: sha256) |
signature_prefix | No | Prefix on signature (default: "sha256=") |
filter | No | Map of dot-path โ expected value |
deliver_to | No | Where to send agent output |
enabled | No | Default: true |
Signature verification
For every inbound request, the gateway:
- Reads the raw request body
- Computes
HMAC-<algo>(secret, body) - Compares (timing-safe) against the value in the signature header
- Rejects with 401 if invalid
โ ๏ธ If you omit secret_env, the webhook accepts any caller who knows the URL. Don't do this in production.
Prompt template variables
Use {{event.<json.path>}} to reference fields in the webhook payload:
# For a GitHub PR opened event:
{{event.pull_request.title}} โ "Fix race condition in scheduler"
{{event.pull_request.user.login}} โ "alice"
{{event.pull_request.html_url}} โ "https://github.com/.../pull/42"
{{event.repository.full_name}} โ "openvesper/openvesper"Filters
Use filter to invoke the agent only when conditions match:
# Only on PR opened (skip closed, edited, synchronized)
filter:
action: opened
# Only on charges over $100 (path traversal)
filter:
type: charge.succeededFilter values are matched as strings. For complex conditions, do filtering inside the prompt and let the agent decide.
GitHub setup example
- Generate a webhook secret (e.g.,
openssl rand -hex 32) - Add to
~/.openvesper/.env:GITHUB_WEBHOOK_SECRET=... - In repo settings โ Webhooks โ Add webhook:
- Payload URL:
https://your-gateway.com/webhook/github-pr - Content type:
application/json - Secret: paste the secret
- Events: select "Pull requests"
- Payload URL:
- Add to
config/webhooks.yaml - Restart the daemon:
vesper daemon restart
Async execution
Webhook requests return 202 Accepted immediately. The agent runs in the background. This prevents external services from timing out while the agent thinks.
For long-running agents, ensure your deliver_to target can receive the result later (Telegram, Slack, email).
Local testing
Use ngrok or similar to expose your local gateway:
ngrok http 18789
# โ https://abc123.ngrok.io
# Use https://abc123.ngrok.io/webhook/github-pr in GitHub settingsOr test the webhook handler directly:
curl -X POST http://localhost:18789/webhook/github-pr \
-H "Content-Type: application/json" \
-H "x-hub-signature-256: sha256=<computed>" \
-d @sample-pr-opened.jsonPrivacy
Webhook payloads are processed in-memory and never persisted unless your deliver_to target writes to disk. The gateway never forwards payloads to OpenVesper servers. See Security.