Webhooks

Trigger agents from external events. GitHub PR opened? Run code-reviewer. New Stripe charge? Update the CRM. Whale alert? Investigate.

How it works

  1. External service POSTs to https://<gateway>/webhook/<path>
  2. Gateway verifies HMAC signature using the shared secret
  3. Payload is parsed as JSON
  4. Filter conditions checked (e.g., only PR opened events)
  5. Agent is invoked with the expanded prompt template
  6. 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: false

Fields

FieldRequiredDescription
pathYesURL path (must start with /)
agentYesAgent mode to invoke
prompt_templateYesPrompt with {{event.path}} interpolation
secret_envRecommendedEnv var holding HMAC secret
signature_headerNoHeader containing signature (default: x-hub-signature-256)
signature_algoNosha1 or sha256 (default: sha256)
signature_prefixNoPrefix on signature (default: "sha256=")
filterNoMap of dot-path โ†’ expected value
deliver_toNoWhere to send agent output
enabledNoDefault: true

Signature verification

For every inbound request, the gateway:

  1. Reads the raw request body
  2. Computes HMAC-<algo>(secret, body)
  3. Compares (timing-safe) against the value in the signature header
  4. 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.succeeded

Filter values are matched as strings. For complex conditions, do filtering inside the prompt and let the agent decide.

GitHub setup example

  1. Generate a webhook secret (e.g., openssl rand -hex 32)
  2. Add to ~/.openvesper/.env: GITHUB_WEBHOOK_SECRET=...
  3. 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"
  4. Add to config/webhooks.yaml
  5. 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 settings

Or 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.json

Privacy

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.