-
Notifications
You must be signed in to change notification settings - Fork 8
Webhook Examples
Kai has two webhook endpoints for receiving notifications from external services:
-
/webhook/github— GitHub-specific with HMAC-SHA256 signature validation -
/webhook— Generic endpoint that accepts any JSON payload
Both require the X-Webhook-Secret header. The generic endpoint looks for a message field in the JSON body, or dumps the full payload if none is found (truncated to Telegram's 4096-char limit). Messages are forwarded to your Telegram chat.
The GitHub integration is covered in Exposing Kai to the Internet. This page focuses on the generic webhook and what you can do with it.
Read this before setting up external webhooks. If you're only using webhooks from the same machine or LAN (Home Assistant, local Docker, cron scripts), many of these risks don't apply.
Accepting webhooks from external services requires Kai's HTTP server to be reachable from the internet. This means the scheduling API (/api/schedule), jobs API (/api/jobs), and service proxy (/api/services) are also reachable on the same port. The shared secret protects them, but the attack surface is larger than a purely local setup.
Recommendations:
- Never expose port 8080 directly to the internet. Use a reverse proxy (nginx, Caddy) with HTTPS termination, or a tunnel (Tailscale, Cloudflare Tunnel, WireGuard).
- If using a reverse proxy, consider only forwarding
/webhookand/webhook/githubpaths to Kai, and blocking/api/*from external access entirely.
The X-Webhook-Secret header is sent in plaintext over HTTP. Over the public internet, anyone on the network path can read it.
Recommendation: Always use HTTPS for internet-facing webhooks. A reverse proxy with Let's Encrypt (Caddy does this automatically) is the simplest approach.
The generic webhook endpoint uses a single shared secret for all sources. If any one source leaks the secret (a compromised CI runner, a misconfigured Home Assistant instance, a script checked into a public repo), all integrations are compromised. An attacker with the secret can:
- Send arbitrary messages to your Telegram chat
- Create scheduled jobs via
/api/schedule - Query existing jobs via
/api/jobs - Call external services via
/api/services
The GitHub endpoint is stronger here — it uses HMAC-SHA256 signature validation with a separate secret, so a leak of the generic webhook secret doesn't compromise GitHub integration (and vice versa).
Recommendations:
- Treat the webhook secret like a password. Don't hardcode it in scripts checked into version control — use environment variables or CI secret stores.
- A future improvement could support per-source secrets on the generic endpoint.
Webhook payloads are forwarded to Telegram as plain text messages. They are not processed by Claude directly. However, if you reply to or reference a webhook notification in conversation with Kai, the payload content enters Claude's context window. A malicious or compromised service could craft a payload containing adversarial instructions.
Recommendations:
- Only accept webhooks from services you trust.
- If you notice unusual content in webhook messages, don't ask Claude to act on it.
- Claude Code's sandbox provides a layer of protection, but should not be relied upon as the sole defense.
Webhook payloads often contain sensitive information: repository names, branch names, commit messages, usernames, IP addresses, server hostnames, error messages with stack traces, payment amounts. All of this gets forwarded to your Telegram chat and stored in Kai's local JSONL history logs.
Recommendations:
- Ensure your Telegram account has two-factor authentication enabled.
- Be aware of what data each integration sends.
| Risk | Severity | Mitigation |
|---|---|---|
| HTTP exposes secret in transit | High | Always use HTTPS (reverse proxy + TLS) |
| Port exposure to internet | Medium | Reverse proxy, restrict paths, or use tunnels |
| Single shared secret for all sources | Medium | Treat secret like a password, use env vars |
| Prompt injection via payloads | Medium | Only connect trusted services |
| Information disclosure in chat | Low | Enable Telegram 2FA, be aware of payload content |
| Relay scripts as attack surface | Low | Minimize relay usage, keep them simple |
Any service that can POST JSON can notify you through Kai. The minimum request:
curl -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_SECRET" \
-d '{"message": "Something happened!"}'Add to .gitlab-ci.yml:
notify:
stage: deploy
script:
- |
STATUS=$([[ "$CI_JOB_STATUS" == "success" ]] && echo "succeeded" || echo "failed")
curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: $WEBHOOK_SECRET" \
-d "{\"message\": \"Pipeline $STATUS for $CI_PROJECT_NAME ($CI_COMMIT_REF_NAME)\"}"
when: alwaysUse the HTTP Request plugin in a post-build step, or add to a Jenkinsfile:
post {
always {
sh """
curl -s -X POST http://your-server:8080/webhook \
-H 'Content-Type: application/json' \
-H 'X-Webhook-Secret: ${WEBHOOK_SECRET}' \
-d '{"message": "Build #${BUILD_NUMBER} ${currentBuild.result} - ${JOB_NAME}"}'
"""
}
}For repos where you don't want full GitHub webhook integration, add a notification step:
- name: Notify Kai
if: always()
run: |
STATUS=${{ job.status }}
curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: ${{ secrets.KAI_WEBHOOK_SECRET }}" \
-d "{\"message\": \"Workflow $STATUS for ${{ github.repository }} (${{ github.ref_name }})\"}"In the notification settings, add a webhook with:
- URL:
http://your-server:8080/webhook - Method: POST
- Header:
X-Webhook-Secret: YOUR_SECRET - Body:
{"message": "{{ msg }}"}
Add to crontab on any server:
# Check disk usage every hour, alert if over 90%
0 * * * * USAGE=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%'); \
[ "$USAGE" -gt 90 ] && curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_SECRET" \
-d "{\"message\": \"Disk usage alert: ${USAGE}% on $(hostname)\"}"Configure a webhook integration pointing to your Kai endpoint. Healthchecks sends JSON payloads when checks fail or recover.
Add a REST command in configuration.yaml:
rest_command:
notify_kai:
url: "http://your-server:8080/webhook"
method: POST
headers:
Content-Type: "application/json"
X-Webhook-Secret: "YOUR_SECRET"
payload: '{"message": "{{ message }}"}'Then use it in automations:
automation:
- alias: "Notify when washing machine finishes"
trigger:
- platform: state
entity_id: sensor.washing_machine_power
to: "0"
for: "00:05:00"
action:
- service: rest_command.notify_kai
data:
message: "Washing machine is done!"
- alias: "Notify on front door open"
trigger:
- platform: state
entity_id: binary_sensor.front_door
to: "on"
action:
- service: rest_command.notify_kai
data:
message: "Front door opened at {{ now().strftime('%H:%M') }}"In Settings > Notifications, add a webhook notification endpoint. Portainer will POST container events (start, stop, crash, restart) to your endpoint.
Monitor container events and forward to Kai:
#!/bin/bash
docker events --filter 'event=die' --format '{{.Actor.Attributes.name}}' | while read name; do
curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_SECRET" \
-d "{\"message\": \"Container crashed: ${name}\"}"
doneIn Site settings > Build & deploy > Deploy notifications, add an outgoing webhook:
- URL:
http://your-server:8080/webhook - Event: "Deploy succeeded" (or "Deploy failed")
Note: Netlify doesn't support custom headers natively in their webhook UI. You may need to use a Netlify function or build plugin to add the X-Webhook-Secret header, or set up a small proxy.
Use a project webhook in Settings > Git > Deploy Hooks, then forward via a simple script or integration.
Stripe uses its own signature scheme that doesn't match Kai's X-Webhook-Secret header. You need a small relay script that validates the Stripe signature and forwards to Kai:
# stripe_relay.py - minimal example
import stripe, requests, json
from flask import Flask, request
app = Flask(__name__)
@app.route("/stripe", methods=["POST"])
def handle():
event = stripe.Webhook.construct_event(
request.data, request.headers["Stripe-Signature"], "whsec_..."
)
msg = f"Stripe: {event['type']} - {event['data']['object'].get('amount', '')/100:.2f} {event['data']['object'].get('currency', '').upper()}"
requests.post("http://localhost:8080/webhook",
headers={"Content-Type": "application/json", "X-Webhook-Secret": "YOUR_SECRET"},
json={"message": msg})
return "ok"Note: Relay scripts add attack surface — each one runs its own HTTP server and holds credentials. Only add relays for services you genuinely need.
Any script or cron job can notify you. A few one-liners:
# Notify when a long-running job finishes
./train_model.py && curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_SECRET" \
-d '{"message": "Model training complete!"}'
# Notify on SSH login (add to /etc/profile or ~/.bashrc on a remote server)
# Note: requires the webhook endpoint to be reachable from the remote server
curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_SECRET" \
-d "{\"message\": \"SSH login: $(whoami)@$(hostname) from ${SSH_CLIENT%% *}\"}"
# Notify when a backup finishes
rsync -a /data /backup && curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_SECRET" \
-d "{\"message\": \"Backup of /data completed at $(date)\"}"