- Notifications
You must be signed in to change notification settings - Fork 18
Webhook Examples
Kai has two webhook endpoints for receiving notifications from external services:
/webhook/github- GitHub-specific, validates theX-Hub-Signature-256HMAC againstGITHUB_WEBHOOK_SECRET; it ignores theX-Webhook-Secretheader entirely/webhook- generic endpoint that accepts any JSON object, authenticated by theX-Webhook-Secretheader matchingGENERIC_WEBHOOK_SECRET
Each endpoint is registered only when its secret is configured; without one, the route returns 404. The generic endpoint looks for a message field in the JSON body, or dumps the full payload if none is found, capped at about four million characters (413 beyond that). A non-object JSON body gets a 400.
Kai records an accepted delivery as a canonical message on your generic notification route channel, and the delivery outbox fans it out to the surfaces you use; the response is structured ({"status", "message_id", "inserted"}), and a failure to record returns 503 so a well-behaved sender retries. Include an Idempotency-Key (or X-Kai-Delivery) header and retries become exactly-once: a replay of the same key reports "inserted": false instead of posting a duplicate.
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.
Kai's HTTP server binds to loopback only, so accepting webhooks from external services means fronting it with a tunnel or reverse proxy (see Exposing Kai to the Internet).
Recommendations:
- Use ingress rules that forward only
/webhook,/webhook/github, and/health; the internal/api/*routes have no business being reachable, even though they authenticate with scoped per-agent credentials rather than the webhook secret. - Terminate TLS at the proxy (Caddy and Cloudflare both handle this for you).
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 it (a compromised CI runner, a misconfigured Home Assistant instance, a script checked into a public repo), every generic integration can send you forged notifications until you rotate it.
The blast radius stops there. The generic secret authenticates the generic endpoint and nothing else: the internal /api/* routes use scoped per-agent credentials, the GitHub endpoint uses its own HMAC secret, and Kai refuses to start if any two ingress secrets match, so no secret can moonlight as another.
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.
- Rotate it by re-running the wizard and updating your callers.
Webhook payloads are recorded as notification messages and delivered to your surfaces as plain text. No agent processes them directly. However, if you reply to or reference a webhook notification in conversation with Kai, the payload content enters the model'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 is recorded in Kai's canonical database and delivered to whatever surfaces you use.
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) |
| Single shared secret for all generic sources | Medium | Treat secret like a password, use env vars; blast radius is forged notifications only |
| 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_GENERIC_WEBHOOK_SECRET" \
-d '{"message": "Something happened!"}'For anything that retries (CI when: always steps, monitoring loops, payment relays), add an idempotency key so a retry can't post the same notification twice:
curl -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_SECRET" \
-H "Idempotency-Key: deploy-$CI_PIPELINE_ID" \
-d '{"message": "Deploy finished"}'Pick a key that identifies the event (a pipeline ID, an alert ID, a Stripe event ID); Kai records each key once and answers replays with "inserted": false.
Add to .gitlab-ci.yml:
notify:
stage: deployscript:
- | 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: $GENERIC_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: ${GENERIC_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 Kaiif: 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_GENERIC_WEBHOOK_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_GENERIC_WEBHOOK_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: POSTheaders:
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: stateentity_id: sensor.washing_machine_powerto: "0"for: "00:05:00"action:
- service: rest_command.notify_kaidata:
message: "Washing machine is done!"
- alias: "Notify on front door open"trigger:
- platform: stateentity_id: binary_sensor.front_doorto: "on"action:
- service: rest_command.notify_kaidata:
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}}'|whileread name;do
curl -s -X POST http://your-server:8080/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: YOUR_GENERIC_WEBHOOK_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 exampleimportstripe, requests, jsonfromflaskimportFlask, requestapp=Flask(__name__)
@app.route("/stripe", methods=["POST"])defhandle():
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_GENERIC_WEBHOOK_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_GENERIC_WEBHOOK_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_GENERIC_WEBHOOK_SECRET" \
-d "{\"message\": \"Backup of /data completed at $(date)\"}"